Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
60 changes: 60 additions & 0 deletions docs/reference/server-json/generic-server-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,66 @@ For MCP servers that follow a custom installation path or are embedded in applic
}
```

### Remote Server with URL Templating

This example demonstrates URL templating for remote servers, useful for multi-tenant deployments where each instance has its own endpoint. Unlike Package transports (which reference parent arguments/environment variables), Remote transports define their own variables:

```json
{
"name": "io.modelcontextprotocol.anonymous/multi-tenant-server",
"description": "MCP server with configurable remote endpoint",
"version": "1.0.0",
"remotes": [
{
"type": "streamable-http",
"url": "https://anonymous.modelcontextprotocol.io/mcp/{tenant_id}",
"variables": {
"tenant_id": {
"description": "Tenant identifier (e.g., 'us-cell1', 'emea-cell1')",
"isRequired": true
}
}
}
]
}
```

Clients configure the tenant identifier, and the `{tenant_id}` variable in the URL gets replaced with the provided variable value to connect to the appropriate tenant endpoint (e.g., `https://anonymous.modelcontextprotocol.io/mcp/us-cell1` or `https://anonymous.modelcontextprotocol.io/mcp/emea-cell1`).

### Local Server with URL Templating

This example demonstrates URL templating for local/package servers, where variables reference parent Package arguments or environment variables:

```json
{
"name": "io.github.example/configurable-server",
"description": "Local MCP server with configurable port",
"version": "1.0.0",
"packages": [
{
"registryType": "npm",
"identifier": "@example/mcp-server",
"version": "1.0.0",
"transport": {
"type": "streamable-http",
"url": "http://localhost:{port}/mcp"
},
"packageArguments": [
{
"type": "named",
"name": "--port",
"description": "Port for the server to listen on",
"default": "3000",
"valueHint": "port"
}
]
}
]
}
```

The `{port}` variable in the URL references either the `--port` argument name or the `port` valueHint from packageArguments. When the package runs with `--port 8080`, the URL becomes `http://localhost:8080/mcp`.

### Deprecated Server Example

```json
Expand Down
82 changes: 61 additions & 21 deletions docs/reference/server-json/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,17 +134,7 @@
]
},
"transport": {
"anyOf": [
{
"$ref": "#/definitions/StdioTransport"
},
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
],
"$ref": "#/definitions/LocalTransport",
"description": "Transport protocol configuration for the package"
},
"runtimeArguments": {
Expand Down Expand Up @@ -377,7 +367,7 @@
},
"url": {
"type": "string",
"description": "URL template for the streamable-http transport. Variables in {curly_braces} reference argument valueHints, argument names, or environment variable names. After variable substitution, this should produce a valid URI.",
"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.",
"example": "https://api.example.com/mcp"
},
"headers": {
Expand Down Expand Up @@ -407,7 +397,7 @@
"url": {
"type": "string",
"format": "uri",
"description": "Server-Sent Events endpoint 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.",
"example": "https://mcp-fs.example.com/sse"
},
"headers": {
Expand All @@ -419,6 +409,63 @@
}
}
},
"LocalTransport": {
"anyOf": [
{
"$ref": "#/definitions/StdioTransport"
},
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
],
"description": "Transport protocol configuration for local/package context"
},
"RemoteTransport": {
"anyOf": [
{
"allOf": [
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"type": "object",
"properties": {
"variables": {
"type": "object",
"description": "Configuration variables that can be referenced in URL template {curly_braces}. The key is the variable name, and the value defines the variable properties.",
"additionalProperties": {
"$ref": "#/definitions/Input"
}
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/definitions/SseTransport"
},
{
"type": "object",
"properties": {
"variables": {
"type": "object",
"description": "Configuration variables that can be referenced in URL template {curly_braces}. The key is the variable name, and the value defines the variable properties.",
"additionalProperties": {
"$ref": "#/definitions/Input"
}
}
}
}
]
}
],
"description": "Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables"
},
"ServerDetail": {
"description": "Schema for a static representation of an MCP server. Used in various contexts related to discovery, installation, and configuration.",
"allOf": [
Expand All @@ -443,14 +490,7 @@
"remotes": {
"type": "array",
"items": {
"anyOf": [
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
]
"$ref": "#/definitions/RemoteTransport"
}
},
"_meta": {
Expand Down
28 changes: 14 additions & 14 deletions internal/validators/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func HasNoSpaces(s string) bool {
func extractTemplateVariables(url string) []string {
re := regexp.MustCompile(`\{([^}]+)\}`)
matches := re.FindAllStringSubmatch(url, -1)

var variables []string
for _, match := range matches {
if len(match) > 1 {
Expand All @@ -55,24 +55,24 @@ func replaceTemplateVariables(rawURL string) string {
"{protocol}": "http",
"{scheme}": "http",
}

result := rawURL
for placeholder, replacement := range templateReplacements {
result = strings.ReplaceAll(result, placeholder, replacement)
}

// Handle any remaining {variable} patterns with generic placeholder
re := regexp.MustCompile(`\{[^}]+\}`)
result = re.ReplaceAllString(result, "placeholder")

return result
}

// IsValidURL checks if a URL is in valid format (basic structure validation)
func IsValidURL(rawURL string) bool {
// Replace template variables with placeholders for parsing
testURL := replaceTemplateVariables(rawURL)

// Parse the URL
u, err := url.Parse(testURL)
if err != nil {
Expand Down Expand Up @@ -131,19 +131,19 @@ func IsValidRemoteURL(rawURL string) bool {
if !IsValidURL(rawURL) {
return false
}

// Parse the URL to check for localhost restriction
u, err := url.Parse(rawURL)
if err != nil {
return false
}

// Reject localhost URLs for remotes (security/production concerns)
hostname := u.Hostname()
if hostname == "localhost" || hostname == "127.0.0.1" || strings.HasSuffix(hostname, ".localhost") {
return false
}

return true
}

Expand All @@ -155,31 +155,31 @@ func IsValidTemplatedURL(rawURL string, availableVariables []string, allowTempla
if !IsValidURL(rawURL) {
return false
}

// Extract template variables from URL
templateVars := extractTemplateVariables(rawURL)

// If no templates are found, it's a valid static URL
if len(templateVars) == 0 {
return true
}

// If templates are not allowed (e.g., for remotes), reject URLs with templates
if !allowTemplates {
return false
}

// Validate that all template variables are available
availableSet := make(map[string]bool)
for _, v := range availableVariables {
availableSet[v] = true
}

for _, templateVar := range templateVars {
if !availableSet[templateVar] {
return false
}
}

return true
}
40 changes: 36 additions & 4 deletions internal/validators/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,20 @@ func collectAvailableVariables(pkg *model.Package) []string {
return variables
}

// collectRemoteTransportVariables extracts available variable names from a remote transport
func collectRemoteTransportVariables(transport *model.Transport) []string {
var variables []string

// Add variable names from the Variables map
for variableName := range transport.Variables {
if variableName != "" {
variables = append(variables, variableName)
}
}

return variables
}

// validatePackageTransport validates a package's transport with templating support
func validatePackageTransport(transport *model.Transport, availableVariables []string) error {
// Validate transport type is supported
Expand Down Expand Up @@ -323,7 +337,7 @@ func validatePackageTransport(transport *model.Transport, availableVariables []s
}
}

// validateRemoteTransport validates a remote transport (no templating allowed)
// validateRemoteTransport validates a remote transport with optional templating
func validateRemoteTransport(obj *model.Transport) error {
// Validate transport type is supported - remotes only support streamable-http and sse
switch obj.Type {
Expand All @@ -332,7 +346,22 @@ func validateRemoteTransport(obj *model.Transport) error {
if obj.URL == "" {
return fmt.Errorf("url is required for %s transport type", obj.Type)
}
// Validate URL format (no templates allowed for remotes, no localhost)

// Collect available variables from the transport's Variables field
availableVariables := collectRemoteTransportVariables(obj)

// Validate URL format with template variable support
if !IsValidTemplatedURL(obj.URL, availableVariables, true) { // true = allow templates for remotes
// Check if it's a template variable issue or basic URL issue
templateVars := extractTemplateVariables(obj.URL)
if len(templateVars) > 0 {
return fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
ErrInvalidRemoteURL, obj.URL, availableVariables)
}
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
}

// Additional check: reject localhost URLs for remotes (like the old IsValidRemoteURL did)
if !IsValidRemoteURL(obj.URL) {
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, obj.URL)
}
Expand Down Expand Up @@ -446,8 +475,11 @@ func validateWebsiteURLNamespaceMatch(serverJSON apiv0.ServerJSON) error {

// validateRemoteURLMatchesNamespace checks if a remote URL's hostname matches the publisher domain from the namespace
func validateRemoteURLMatchesNamespace(remoteURL, namespace string) error {
// Replace template variables with placeholders before parsing
testURL := replaceTemplateVariables(remoteURL)

// Parse the URL to extract the hostname
parsedURL, err := url.Parse(remoteURL)
parsedURL, err := url.Parse(testURL)
if err != nil {
return fmt.Errorf("invalid URL format: %w", err)
}
Expand Down Expand Up @@ -510,4 +542,4 @@ func isValidHostForDomain(hostname, publisherDomain string) bool {
}

return false
}
}
17 changes: 13 additions & 4 deletions pkg/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,20 @@ const (
StatusDeleted Status = "deleted"
)

// Transport represents transport configuration with optional URL templating
// Transport represents transport configuration for Package context
type Transport struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
Headers []KeyValueInput `json:"headers,omitempty"`
Type string `json:"type"`
URL string `json:"url,omitempty"`
Headers []KeyValueInput `json:"headers,omitempty"`
Variables map[string]Input `json:"variables,omitempty"`
}

// RemoteTransport represents transport configuration for Remote context with variables support
type RemoteTransport struct {
Type string `json:"type"`
URL string `json:"url,omitempty"`
Headers []KeyValueInput `json:"headers,omitempty"`
Variables map[string]Input `json:"variables,omitempty"`
}

// Package represents a package configuration
Expand Down
Loading
Loading