You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
#570 (comment)
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>
Copy file name to clipboardExpand all lines: docs/reference/api/openapi.yaml
+27-10Lines changed: 27 additions & 10 deletions
Original file line number
Diff line number
Diff line change
@@ -440,10 +440,7 @@ components:
440
440
description: A hint to help clients determine the appropriate runtime for the package. This field should be provided when `runtimeArguments` are present.
description: Transport protocol configuration for the package
448
445
runtimeArguments:
449
446
type: array
@@ -599,8 +596,9 @@ components:
599
596
example: "streamable-http"
600
597
url:
601
598
type: string
602
-
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.
599
+
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."
603
600
example: "https://api.example.com/mcp"
601
+
pattern: "^https?://[^\\s]+$"
604
602
headers:
605
603
type: array
606
604
description: HTTP headers to include
@@ -620,15 +618,36 @@ components:
620
618
example: "sse"
621
619
url:
622
620
type: string
623
-
format: uri
624
-
description: Server-Sent Events endpoint URL
621
+
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: "Configuration variables that can be referenced in URL template {curly_braces}. The key is the variable name, and the value defines the variable properties."
647
+
additionalProperties:
648
+
$ref: '#/components/schemas/Input'
649
+
description: Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables
650
+
632
651
Icon:
633
652
type: object
634
653
description: An optionally-sized icon that can be displayed in a user interface.
Copy file name to clipboardExpand all lines: docs/reference/server-json/generic-server-json.md
+87Lines changed: 87 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -688,3 +688,90 @@ For MCP servers that follow a custom installation path or are embedded in applic
688
688
}
689
689
```
690
690
691
+
692
+
### Remote Server with URL Templating
693
+
694
+
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:
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`).
"description": "Local MCP server with configurable port",
752
+
"title": "Configurable Server",
753
+
"version": "1.0.0",
754
+
"packages": [
755
+
{
756
+
"registryType": "npm",
757
+
"registryBaseUrl": "https://registry.npmjs.org",
758
+
"identifier": "@example/mcp-server",
759
+
"version": "1.0.0",
760
+
"transport": {
761
+
"type": "streamable-http",
762
+
"url": "http://localhost:{--port}/mcp"
763
+
},
764
+
"packageArguments": [
765
+
{
766
+
"type": "named",
767
+
"name": "--port",
768
+
"description": "Port for the server to listen on",
769
+
"default": "3000"
770
+
}
771
+
]
772
+
}
773
+
]
774
+
}
775
+
```
776
+
777
+
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`.
Copy file name to clipboardExpand all lines: docs/reference/server-json/server.schema.json
+47-22Lines changed: 47 additions & 22 deletions
Original file line number
Diff line number
Diff line change
@@ -156,6 +156,20 @@
156
156
}
157
157
]
158
158
},
159
+
"LocalTransport": {
160
+
"anyOf": [
161
+
{
162
+
"$ref": "#/definitions/StdioTransport"
163
+
},
164
+
{
165
+
"$ref": "#/definitions/StreamableHttpTransport"
166
+
},
167
+
{
168
+
"$ref": "#/definitions/SseTransport"
169
+
}
170
+
],
171
+
"description": "Transport protocol configuration for local/package context"
172
+
},
159
173
"NamedArgument": {
160
174
"allOf": [
161
175
{
@@ -262,17 +276,7 @@
262
276
"type": "string"
263
277
},
264
278
"transport": {
265
-
"anyOf": [
266
-
{
267
-
"$ref": "#/definitions/StdioTransport"
268
-
},
269
-
{
270
-
"$ref": "#/definitions/StreamableHttpTransport"
271
-
},
272
-
{
273
-
"$ref": "#/definitions/SseTransport"
274
-
}
275
-
],
279
+
"$ref": "#/definitions/LocalTransport",
276
280
"description": "Transport protocol configuration for the package"
277
281
},
278
282
"version": {
@@ -337,6 +341,33 @@
337
341
],
338
342
"description": "A positional input is a value inserted verbatim into the command line."
339
343
},
344
+
"RemoteTransport": {
345
+
"allOf": [
346
+
{
347
+
"anyOf": [
348
+
{
349
+
"$ref": "#/definitions/StreamableHttpTransport"
350
+
},
351
+
{
352
+
"$ref": "#/definitions/SseTransport"
353
+
}
354
+
]
355
+
},
356
+
{
357
+
"properties": {
358
+
"variables": {
359
+
"additionalProperties": {
360
+
"$ref": "#/definitions/Input"
361
+
},
362
+
"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.",
363
+
"type": "object"
364
+
}
365
+
},
366
+
"type": "object"
367
+
}
368
+
],
369
+
"description": "Transport protocol configuration for remote context - extends StreamableHttpTransport or SseTransport with variables"
370
+
},
340
371
"Repository": {
341
372
"description": "Repository metadata for the MCP server source code. Enables users and security experts to inspect the code, improving transparency.",
342
373
"properties": {
@@ -427,14 +458,7 @@
427
458
},
428
459
"remotes": {
429
460
"items": {
430
-
"anyOf": [
431
-
{
432
-
"$ref": "#/definitions/StreamableHttpTransport"
433
-
},
434
-
{
435
-
"$ref": "#/definitions/SseTransport"
436
-
}
437
-
]
461
+
"$ref": "#/definitions/RemoteTransport"
438
462
},
439
463
"type": "array"
440
464
},
@@ -487,9 +511,9 @@
487
511
"type": "string"
488
512
},
489
513
"url": {
490
-
"description": "Server-Sent Events endpoint URL",
514
+
"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.",
491
515
"example": "https://mcp-fs.example.com/sse",
492
-
"format": "uri",
516
+
"pattern": "^https?://[^\\s]+$",
493
517
"type": "string"
494
518
}
495
519
},
@@ -533,8 +557,9 @@
533
557
"type": "string"
534
558
},
535
559
"url": {
536
-
"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.",
560
+
"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.",
0 commit comments