Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -685,9 +685,9 @@ components:
example: "streamable-http"
url:
type: string
description: "URL template for the streamable-http transport. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI."
description: "URL template for the streamable-http transport. Must start with http://, https://, or a template variable (e.g., {baseUrl}). Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI."
example: "https://api.example.com/mcp"
pattern: "^https?://[^\\s]+$"
pattern: "^(https?://[^\\s]+|\\{[a-zA-Z_][a-zA-Z0-9_]*\\}[^\\s]*)$"
headers:
type: array
description: HTTP headers to include
Expand All @@ -707,9 +707,9 @@ components:
example: "sse"
url:
type: string
description: "Server-Sent Events endpoint URL template. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI."
description: "Server-Sent Events endpoint URL template. Must start with http://, https://, or a template variable (e.g., {baseUrl}). Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI."
example: "https://mcp-fs.example.com/sse"
pattern: "^https?://[^\\s]+$"
pattern: "^(https?://[^\\s]+|\\{[a-zA-Z_][a-zA-Z0-9_]*\\}[^\\s]*)$"
headers:
type: array
description: HTTP headers to include
Expand Down
22 changes: 21 additions & 1 deletion docs/reference/server-json/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,27 @@ This section tracks changes that are in development and not yet released. The dr

### Changed

- No changes yet.
#### Transport URL Pattern Now Accepts Template Variables

The `url` field in `StreamableHttpTransport` and `SseTransport` now accepts URLs that start with a template variable (e.g., `{baseUrl}`), in addition to the existing `http://` and `https://` prefixes.

**Example:**
```json
{
"remotes": [{
"type": "streamable-http",
"url": "{baseUrl}/mcp",
"variables": {
"baseUrl": {
"description": "Base URL for the MCP server",
"isRequired": true
}
}
}]
}
```

**Migration:** No changes required. Existing servers continue to work unchanged.

### Notes

Expand Down
8 changes: 4 additions & 4 deletions docs/reference/server-json/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -511,9 +511,9 @@
"type": "string"
},
"url": {
"description": "Server-Sent Events endpoint URL template. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
"description": "Server-Sent Events endpoint URL template. Must start with http://, https://, or a template variable (e.g., {baseUrl}). Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
"example": "https://mcp-fs.example.com/sse",
"pattern": "^https?://[^\\s]+$",
"pattern": "^(https?://[^\\s]+|\\{[a-zA-Z_][a-zA-Z0-9_]*\\}[^\\s]*)$",
"type": "string"
}
},
Expand Down Expand Up @@ -557,9 +557,9 @@
"type": "string"
},
"url": {
"description": "URL template for the streamable-http transport. Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
"description": "URL template for the streamable-http transport. Must start with http://, https://, or a template variable (e.g., {baseUrl}). Variables in {curly_braces} are resolved based on context: In Package context, they reference argument valueHints, argument names, or environment variable names from the parent Package. In Remote context, they reference variables from the transport's 'variables' object. After variable substitution, this should produce a valid URI.",
"example": "https://api.example.com/mcp",
"pattern": "^https?://[^\\s]+$",
"pattern": "^(https?://[^\\s]+|\\{[a-zA-Z_][a-zA-Z0-9_]*\\}[^\\s]*)$",
"type": "string"
}
},
Expand Down
108 changes: 108 additions & 0 deletions internal/validators/schema_regex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package validators_test

import (
"encoding/json"
"os"
"regexp"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const serverSchemaPath = "../../docs/reference/server-json/server.schema.json"

// schemaHelper provides utilities for extracting values from the JSON schema
type schemaHelper struct {
t *testing.T
schema map[string]interface{}
}

func loadSchema(t *testing.T) *schemaHelper {
t.Helper()
data, err := os.ReadFile(serverSchemaPath)
require.NoError(t, err, "Failed to read schema file")

var schema map[string]interface{}
err = json.Unmarshal(data, &schema)
require.NoError(t, err, "Failed to parse schema JSON")

return &schemaHelper{t: t, schema: schema}
}

// getDefinition returns a definition from the schema by name
func (s *schemaHelper) getDefinition(name string) map[string]interface{} {
s.t.Helper()
definitions := s.schema["definitions"].(map[string]interface{})
def, ok := definitions[name].(map[string]interface{})
require.True(s.t, ok, "Definition %q not found in schema", name)
return def
}

// getPropertyPattern extracts a regex pattern from a definition's property
func (s *schemaHelper) getPropertyPattern(definitionName, propertyName string) string {
s.t.Helper()
def := s.getDefinition(definitionName)
props := def["properties"].(map[string]interface{})
prop, ok := props[propertyName].(map[string]interface{})
require.True(s.t, ok, "Property %q not found in %s", propertyName, definitionName)
pattern, ok := prop["pattern"].(string)
require.True(s.t, ok, "Pattern not found for %s.%s", definitionName, propertyName)
return pattern
}

// TestTransportURLPattern validates the URL pattern used by StreamableHttpTransport and SseTransport.
// URLs must start with http://, https://, or a template variable like {baseUrl}.
func TestTransportURLPattern(t *testing.T) {
schema := loadSchema(t)

streamablePattern := schema.getPropertyPattern("StreamableHttpTransport", "url")
ssePattern := schema.getPropertyPattern("SseTransport", "url")

// Verify both transport types use the same pattern
assert.Equal(t, streamablePattern, ssePattern,
"StreamableHttpTransport and SseTransport should use identical URL patterns")

t.Logf("Pattern: %s", streamablePattern)

re, err := regexp.Compile(streamablePattern)
require.NoError(t, err, "Pattern should be valid regex")

// Test cases that SHOULD match
validCases := []string{
// Standard URLs
"https://api.example.com/mcp",
"http://localhost:8080/sse",
"https://example.com/path?query=value",
"https://api.example.com/v1/mcp",
// Template variables
"{baseUrl}",
"{baseUrl}/mcp",
"{server_url}/api/v1",
"{API_ENDPOINT}",
"{a}",
"{_private}/endpoint",
}

for _, tc := range validCases {
assert.True(t, re.MatchString(tc), "Expected %q to match pattern", tc)
}

// Test cases that should NOT match
invalidCases := []string{
"ftp://example.com", // wrong protocol
"example.com", // missing protocol or variable
"/relative/path", // relative path
"{invalid-name}/path", // hyphen in variable name
"{123invalid}", // variable starts with number
"", // empty string
"mailto:test@example.com", // wrong protocol
"file:///path/to/file", // wrong protocol
"{}/empty", // empty variable name
"{{nested}}/path", // nested braces
}

for _, tc := range invalidCases {
assert.False(t, re.MatchString(tc), "Expected %q to NOT match pattern", tc)
}
}
Loading