feat: Add LocalTransport/RemoteTransport with URL template variables (#570)
Co-authored with Claude Code, but I've closely reviewed/modified/vetted
all changes; ready for final review.
## Summary
This PR adds LocalTransport and RemoteTransport separation with URL
template variable support for remote servers, enabling e.g. multi-tenant
deployments with configurable endpoints.
**Key additions:**
- LocalTransport (for Packages) and RemoteTransport (for Remotes)
- URL template variables for remote transports (e.g., `{tenant_id}` in
URLs)
- Pattern validation ensuring URLs are URL-like while allowing templates
- Full test coverage via Go validator tests and documentation examples
## ⚠️ server.json Update
This PR updates the `server.json` schema. A new schema version will be
cut after this lands.
## Problem
The current schema describes URL templating with `{curly_braces}` for
transports but provides no mechanism for Remote contexts to define what
those variables should resolve to, making the feature unusable for
remote servers.
Referenced issue: #394
- Users need multi-tenant remote servers with different endpoints per
deployment
- Each tenant/region has its own URL (e.g., `us-cell1.example.com`,
`emea-cell1.example.com`)
- Current schema doesn't support parameterized remote URLs
## Solution
### Schema Architecture
**LocalTransport** (for Package context):
```typescript
LocalTransport = StdioTransport | StreamableHttpTransport | SseTransport
```
- Used in Package context (non-breaking rename of existing behavior)
- Variables in `{curly_braces}` reference parent Package
arguments/environment variables
- Supports all three transport types including stdio
**RemoteTransport** (for Remote context):
```typescript
RemoteTransport = (StreamableHttpTransport | SseTransport) + { variables: object }
```
- Extends StreamableHttp/Sse via `allOf` composition - no duplication!
- Adds `variables` object for URL templating
- Only supports streamable-http and sse (stdio not valid for remotes)
### Key Changes
**1. Schema Updates** (`openapi.yaml`, `server.schema.json`):
- Added `LocalTransport` union type for Package context
- Added `RemoteTransport` with `allOf` composition extending base
transports
- Updated Package to reference `LocalTransport`
- Updated remotes to reference `RemoteTransport`
- Added URL pattern validation: `^https?://[^\\s]+$` to both
StreamableHttp and SSE
- Removed `format: uri` from SSE (was blocking template variables)
**2. Go Types** (`pkg/model/types.go`):
- Base `Transport` struct unchanged for Package context
- `RemoteTransport` struct for remotes with `Variables` field
- Both work with existing validation infrastructure
**3. Validators** (`internal/validators/`):
- `validateRemoteTransport()` validates Remote-specific constraints
- `collectRemoteTransportVariables()` extracts available variables
- `IsValidTemplatedURL()` validates template variables reference defined
variables
- `IsValidRemoteURL()` handles template variable substitution before
localhost check
- 6 new test cases for remote transport variable validation
**4. Documentation Examples** (`generic-server-json.md`):
- Remote Server with URL Templating (StreamableHttp with `{tenant_id}`)
- SSE Remote with URL Templating (SSE with `{tenant_id}`)
- Local Server with URL Templating (Package with `{port}`)
- All examples validate via existing `validate-examples` tool
## Example: Remote Transport with Variables
```json
{
"remotes": [
{
"type": "streamable-http",
"url": "https://api.example.com/mcp/{tenant_id}/{region}",
"variables": {
"tenant_id": {
"description": "Tenant identifier",
"isRequired": true,
"choices": ["us-cell1", "emea-cell1", "apac-cell1"]
},
"region": {
"description": "Deployment region",
"isRequired": true
}
}
}
]
}
```
Clients configure the variables, and `{tenant_id}` and `{region}` get
replaced to connect to tenant-specific endpoints like
`https://api.example.com/mcp/us-cell1/east`.
## Architecture Details
**Context-based variable resolution:**
- **Package + LocalTransport**: `{port}` references `--port` argument or
`PORT` env var from parent Package
- **Remote + RemoteTransport**: `{tenant_id}` references
`variables.tenant_id` defined in the transport itself
- **Code reuse**: StreamableHttp/Sse definitions shared via `allOf`, not
duplicated
- **Validation**: Template variables validated against available
variables for each context
---
## Follow-on Work
Follow-on work will be done to adopt the feedback in
https://github.com/modelcontextprotocol/registry/pull/570#issuecomment-3367417636
regarding variable naming/prioritzation/scoping conventions, but should
not have any impact on the shape being introduced here (just
validations).
---------
Co-authored-by: yuna0x0 <yuna@yuna0x0.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Connor Peet <connor@peet.io>
This commit is contained in:
parent
bbbad1af1f
commit
b58836c48e
9 changed files with 350 additions and 52 deletions
|
|
@ -440,10 +440,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
|
||||
|
|
@ -599,8 +596,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
|
||||
|
|
@ -620,15 +618,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.
|
||||
|
|
@ -717,9 +736,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"
|
||||
|
|
|
|||
|
|
@ -688,3 +688,90 @@ 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-10-17/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-10-17/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-10-17/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"
|
||||
},
|
||||
"packageArguments": [
|
||||
{
|
||||
"type": "named",
|
||||
"name": "--port",
|
||||
"description": "Port for the server to listen on",
|
||||
"default": "3000"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
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 referenced as `{port}`. When the package runs with `--port 8080`, the URL becomes `http://localhost:8080/mcp`.
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
{
|
||||
|
|
@ -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": {
|
||||
|
|
@ -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": {
|
||||
|
|
@ -427,14 +458,7 @@
|
|||
},
|
||||
"remotes": {
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/StreamableHttpTransport"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/SseTransport"
|
||||
}
|
||||
]
|
||||
"$ref": "#/definitions/RemoteTransport"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
@ -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"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ var (
|
|||
ErrReservedVersionString = errors.New("version string 'latest' is reserved and cannot be used")
|
||||
ErrVersionLooksLikeRange = errors.New("version must be a specific version, not a range")
|
||||
|
||||
// Remote validation errors
|
||||
ErrInvalidRemoteURL = errors.New("invalid remote URL")
|
||||
// Transport validation errors
|
||||
ErrInvalidPackageTransportURL = errors.New("invalid package transport URL")
|
||||
ErrInvalidRemoteURL = errors.New("invalid remote URL")
|
||||
|
||||
// Registry validation errors
|
||||
ErrUnsupportedRegistryBaseURL = errors.New("unsupported registry base URL")
|
||||
|
|
|
|||
|
|
@ -61,7 +61,13 @@ func replaceTemplateVariables(rawURL string) string {
|
|||
result = strings.ReplaceAll(result, placeholder, replacement)
|
||||
}
|
||||
|
||||
// Handle any remaining {variable} patterns with generic placeholder
|
||||
// Handle any remaining {variable} patterns with context-appropriate placeholders
|
||||
// If the variable is in a port position (after a colon in the host), use a numeric placeholder
|
||||
// Pattern: :/{variable} or :{variable}/ or :{variable} at end
|
||||
portRe := regexp.MustCompile(`:(\{[^}]+\})(/|$)`)
|
||||
result = portRe.ReplaceAllString(result, ":8080$2")
|
||||
|
||||
// Replace any other remaining {variable} patterns with generic placeholder
|
||||
re := regexp.MustCompile(`\{[^}]+\}`)
|
||||
result = re.ReplaceAllString(result, "placeholder")
|
||||
|
||||
|
|
@ -132,8 +138,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
|
||||
}
|
||||
|
|
@ -153,8 +162,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
|
||||
|
|
@ -168,11 +177,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 {
|
||||
|
|
|
|||
|
|
@ -363,6 +363,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
|
||||
|
|
@ -379,14 +393,14 @@ 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 {
|
||||
return fmt.Errorf("%w: template variables in URL %s reference undefined variables. Available variables: %v",
|
||||
ErrInvalidRemoteURL, transport.URL, availableVariables)
|
||||
ErrInvalidPackageTransportURL, transport.URL, availableVariables)
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrInvalidRemoteURL, transport.URL)
|
||||
return fmt.Errorf("%w: %s", ErrInvalidPackageTransportURL, transport.URL)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
|
|
@ -394,7 +408,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 {
|
||||
|
|
@ -403,7 +417,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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1598,6 +1598,138 @@ func TestValidate_TransportValidation(t *testing.T) {
|
|||
},
|
||||
expectedError: "invalid remote URL",
|
||||
},
|
||||
// Remote transport variable tests
|
||||
{
|
||||
name: "remote transport with URL variables - valid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "streamable-http",
|
||||
URL: "https://example.com/mcp/{tenant_id}",
|
||||
Variables: map[string]model.Input{
|
||||
"tenant_id": {
|
||||
Description: "Tenant identifier",
|
||||
IsRequired: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "remote transport with multiple URL variables - valid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "streamable-http",
|
||||
URL: "https://{region}.example.com/mcp/{tenant_id}",
|
||||
Variables: map[string]model.Input{
|
||||
"region": {
|
||||
Description: "Server region",
|
||||
Choices: []string{"us-east-1", "eu-west-1"},
|
||||
},
|
||||
"tenant_id": {
|
||||
Description: "Tenant identifier",
|
||||
IsRequired: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "remote transport with undefined URL variable - invalid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "streamable-http",
|
||||
URL: "https://example.com/mcp/{tenant_id}",
|
||||
// Missing variables definition
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "template variables in URL",
|
||||
},
|
||||
{
|
||||
name: "remote transport with missing variable in URL - invalid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "streamable-http",
|
||||
URL: "https://example.com/mcp/{tenant_id}/{region}",
|
||||
Variables: map[string]model.Input{
|
||||
"tenant_id": {
|
||||
Description: "Tenant identifier",
|
||||
},
|
||||
// Missing "region" variable
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "template variables in URL",
|
||||
},
|
||||
{
|
||||
name: "remote transport SSE with URL variables - valid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "sse",
|
||||
URL: "https://events.example.com/mcp/{api_key}",
|
||||
Variables: map[string]model.Input{
|
||||
"api_key": {
|
||||
Description: "API key for authentication",
|
||||
IsRequired: true,
|
||||
IsSecret: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "",
|
||||
},
|
||||
{
|
||||
name: "remote transport with variables but no template in URL - valid",
|
||||
serverDetail: apiv0.ServerJSON{
|
||||
Schema: model.CurrentSchemaURL,
|
||||
Name: "com.example/test-server",
|
||||
Description: "A test server",
|
||||
Version: "1.0.0",
|
||||
Remotes: []model.Transport{
|
||||
{
|
||||
Type: "streamable-http",
|
||||
URL: "https://example.com/mcp",
|
||||
Variables: map[string]model.Input{
|
||||
"unused_var": {
|
||||
Description: "This variable is defined but not used",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "", // Valid - variables can be defined but not used
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ const (
|
|||
StatusDeleted Status = "deleted"
|
||||
)
|
||||
|
||||
// Transport represents transport configuration for both Package and Remote contexts.
|
||||
// For Remote context, the Variables field can be used for URL templating.
|
||||
type Transport struct {
|
||||
Type string `json:"type" doc:"Transport type (stdio, streamable-http, or sse)" example:"stdio"`
|
||||
URL string `json:"url,omitempty" doc:"URL for streamable-http or sse transports" example:"https://api.example.com/mcp"`
|
||||
Headers []KeyValueInput `json:"headers,omitempty" doc:"HTTP headers for streamable-http or sse transports"`
|
||||
Type string `json:"type" doc:"Transport type (stdio, streamable-http, or sse)" example:"stdio"`
|
||||
URL string `json:"url,omitempty" doc:"URL for streamable-http or sse transports" example:"https://api.example.com/mcp"`
|
||||
Headers []KeyValueInput `json:"headers,omitempty" doc:"HTTP headers for streamable-http or sse transports"`
|
||||
Variables map[string]Input `json:"variables,omitempty" doc:"Variables for URL templating in remote transports"`
|
||||
}
|
||||
|
||||
// Package represents a package configuration.
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ func main() {
|
|||
|
||||
func runValidation() error {
|
||||
// Define what we validate and how
|
||||
expectedServerJSONCount := 12
|
||||
expectedServerJSONCount := 15
|
||||
targets := []validationTarget{
|
||||
{
|
||||
path: filepath.Join("docs", "reference", "server-json", "generic-server-json.md"),
|
||||
|
|
|
|||
Loading…
Reference in a new issue