Skip to content
Open
37 changes: 27 additions & 10 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,7 @@ components:
description: A hint to help clients determine the appropriate runtime for the package. This field should be provided when `runtimeArguments` are present.
examples: [npx, uvx, docker, dnx]
transport:
anyOf:
- $ref: '#/components/schemas/StdioTransport'
- $ref: '#/components/schemas/StreamableHttpTransport'
- $ref: '#/components/schemas/SseTransport'
$ref: '#/components/schemas/LocalTransport'
description: Transport protocol configuration for the package
runtimeArguments:
type: array
Expand Down Expand Up @@ -474,8 +471,9 @@ components:
example: "streamable-http"
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"
pattern: "^https?://[^\\s]+$"
headers:
type: array
description: HTTP headers to include
Expand All @@ -495,15 +493,36 @@ components:
example: "sse"
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"
pattern: "^https?://[^\\s]+$"
headers:
type: array
description: HTTP headers to include
items:
$ref: '#/components/schemas/KeyValueInput'

LocalTransport:
anyOf:
- $ref: '#/components/schemas/StdioTransport'
- $ref: '#/components/schemas/StreamableHttpTransport'
- $ref: '#/components/schemas/SseTransport'
description: Transport protocol configuration for local/package context

RemoteTransport:
allOf:
- anyOf:
- $ref: '#/components/schemas/StreamableHttpTransport'
- $ref: '#/components/schemas/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: '#/components/schemas/Input'
description: Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables

Icon:
type: object
description: An optionally-sized icon that can be displayed in a user interface.
Expand Down Expand Up @@ -592,9 +611,7 @@ components:
remotes:
type: array
items:
anyOf:
- $ref: '#/components/schemas/StreamableHttpTransport'
- $ref: '#/components/schemas/SseTransport'
$ref: '#/components/schemas/RemoteTransport'
_meta:
type: object
description: "Extension metadata using reverse DNS namespacing for vendor-specific data"
Expand Down
88 changes: 88 additions & 0 deletions docs/reference/server-json/generic-server-json.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,3 +672,91 @@ 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
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "io.modelcontextprotocol.anonymous/multi-tenant-server",
"description": "MCP server with configurable remote endpoint",
"title": "Multi-Tenant Server",
"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`).

The same URL templating works with SSE transport:

```json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "io.modelcontextprotocol.anonymous/events-server",
"description": "MCP server using SSE with tenant-specific endpoints",
"version": "1.0.0",
"remotes": [
{
"type": "sse",
"url": "https://events.anonymous.modelcontextprotocol.io/sse/{tenant_id}",
"variables": {
"tenant_id": {
"description": "Tenant identifier",
"isRequired": true
}
}
}
]
}
```

### Local Server with URL Templating

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

```json
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
"name": "io.github.example/configurable-server",
"description": "Local MCP server with configurable port",
"title": "Configurable Server",
"version": "1.0.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "@example/mcp-server",
"version": "1.0.0",
"transport": {
"type": "streamable-http",
"url": "http://localhost:{port}/mcp"
Copy link
Contributor

@connor4312 connor4312 Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As things are specified currently we take the name literally

Suggested change
"url": "http://localhost:{port}/mcp"
"url": "http://localhost:{--port}/mcp"

},
"packageArguments": [
{
"type": "named",
"name": "--port",
"description": "Port for the server to listen on",
"default": "3000",
"valueHint": "port"
Copy link
Contributor

@connor4312 connor4312 Oct 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

valueHint is only valid on positional arguments, not named arguments (otherwise we would have nothing to key off of except an unstable index)

}
]
}
]
}
```

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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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`.
The `{--port}` variable in the URL references the `--port` argument `name` from packageArguments. For positional arguments, an argument with the `valueHint` of `port` could similarly be referened as `{port}`. When the package runs with `--port 8080`, the URL becomes `http://localhost:8080/mcp`.

69 changes: 47 additions & 22 deletions docs/reference/server-json/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,20 @@
}
]
},
"LocalTransport": {
"anyOf": [
{
"$ref": "#/definitions/StdioTransport"
},
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
],
"description": "Transport protocol configuration for local/package context"
},
"NamedArgument": {
"allOf": [
{
Expand Down Expand Up @@ -262,17 +276,7 @@
"type": "string"
},
"transport": {
"anyOf": [
{
"$ref": "#/definitions/StdioTransport"
},
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
],
"$ref": "#/definitions/LocalTransport",
"description": "Transport protocol configuration for the package"
},
"version": {
Expand Down Expand Up @@ -337,6 +341,33 @@
],
"description": "A positional input is a value inserted verbatim into the command line."
},
"RemoteTransport": {
"allOf": [
{
"anyOf": [
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
]
},
{
"properties": {
"variables": {
"additionalProperties": {
"$ref": "#/definitions/Input"
},
"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.",
"type": "object"
}
},
"type": "object"
}
],
"description": "Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables"
},
"Repository": {
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
"properties": {
Expand Down Expand Up @@ -427,14 +458,7 @@
},
"remotes": {
"items": {
"anyOf": [
{
"$ref": "#/definitions/StreamableHttpTransport"
},
{
"$ref": "#/definitions/SseTransport"
}
]
"$ref": "#/definitions/RemoteTransport"
},
"type": "array"
},
Expand Down Expand Up @@ -487,9 +511,9 @@
"type": "string"
},
"url": {
"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",
"format": "uri",
"pattern": "^https?://[^\\s]+$",
"type": "string"
}
},
Expand Down Expand Up @@ -533,8 +557,9 @@
"type": "string"
},
"url": {
"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",
"pattern": "^https?://[^\\s]+$",
"type": "string"
}
},
Expand Down
14 changes: 6 additions & 8 deletions internal/validators/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,11 @@ func IsValidRemoteURL(rawURL string) bool {
return false
}

// Replace template variables with placeholders before parsing for localhost check
testURL := replaceTemplateVariables(rawURL)

// Parse the URL to check for localhost restriction
u, err := url.Parse(rawURL)
u, err := url.Parse(testURL)
if err != nil {
return false
}
Expand All @@ -149,8 +152,8 @@ func IsValidRemoteURL(rawURL string) bool {

// IsValidTemplatedURL validates a URL with template variables against available variables
// For packages: validates that template variables reference package arguments or environment variables
// For remotes: disallows template variables entirely
func IsValidTemplatedURL(rawURL string, availableVariables []string, allowTemplates bool) bool {
// For remotes: validates that template variables reference the transport's variables map
func IsValidTemplatedURL(rawURL string, availableVariables []string) bool {
// First check basic URL structure
if !IsValidURL(rawURL) {
return false
Expand All @@ -164,11 +167,6 @@ func IsValidTemplatedURL(rawURL string, availableVariables []string, allowTempla
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 {
Expand Down
40 changes: 36 additions & 4 deletions internal/validators/validators.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,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 All @@ -390,7 +404,7 @@ func validatePackageTransport(transport *model.Transport, availableVariables []s
return fmt.Errorf("url is required for %s transport type", transport.Type)
}
// Validate URL format with template variable support
if !IsValidTemplatedURL(transport.URL, availableVariables, true) {
if !IsValidTemplatedURL(transport.URL, availableVariables) {
// Check if it's a template variable issue or basic URL issue
templateVars := extractTemplateVariables(transport.URL)
if len(templateVars) > 0 {
Expand All @@ -405,7 +419,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 @@ -414,7 +428,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) {
// 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 @@ -540,8 +569,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
Loading
Loading