feat: Add LocalTransport/RemoteTransport with URL template variables
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
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):
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):
RemoteTransport = (StreamableHttpTransport | SseTransport) + { variables: object }
- Extends StreamableHttp/Sse via
allOfcomposition - no duplication! - Adds
variablesobject 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
LocalTransportunion type for Package context - Added
RemoteTransportwithallOfcomposition 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: urifrom SSE (was blocking template variables)
2. Go Types (pkg/model/types.go):
- Base
Transportstruct unchanged for Package context RemoteTransportstruct for remotes withVariablesfield- Both work with existing validation infrastructure
3. Validators (internal/validators/):
validateRemoteTransport()validates Remote-specific constraintscollectRemoteTransportVariables()extracts available variablesIsValidTemplatedURL()validates template variables reference defined variablesIsValidRemoteURL()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-examplestool
Example: Remote Transport with Variables
{
"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--portargument orPORTenv var from parent Package - Remote + RemoteTransport:
{tenant_id}referencesvariables.tenant_iddefined 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).
I somehow missed this PR and created https://github.com/modelcontextprotocol/registry/pull/576 (which should get closed if this PR is adopted). FWIW, I've reviewed this PR and am a fan of the approach (including adding variables to the remote transports for URI substitution).
I'm happy with this approach 👍. Might just need a rebase and minor code cleanup
OK, I really like this PR and don't want to slow it down. That being said, in going through existing registry entries with an eye toward making them take full advantage of the configuration support offered, I ran across a case that might impact this.
The general use case for this feature is configurable ports for the package transport url (so we configure the port we want the server to run on, and then have the transport config pick up that same port from the runtime config). But what about the case where the port is not an argument or env var, but is another part of the config? For example (from a published server):
Current
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
"name": "io.github.SamYuan1990/i18n-agent-action",
"description": "An i18n github action for language translate",
"version": "mcp",
"packages": [
{
"registryType": "oci",
"registryBaseUrl": "https://ghcr.io",
"identifier": "SamYuan1990/i18n-agent-action",
"version": "mcp",
"runtimeHint": "docker",
"transport": {
"type": "sse",
"url": "https://example.com:8080/sse"
},
"runtimeArguments": [
{
"description": "Port mapping from host to container",
"value": "8080:8080",
"type": "named",
"name": "-p"
}
]
}
]
}
If we wanted to parameterize the port so we can change it and have it be reflected in the docker command and the transport url, we would do something like this:
Parameterized
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
"name": "io.github.SamYuan1990/i18n-agent-action",
"description": "An i18n github action for language translate",
"version": "mcp",
"packages": [
{
"registryType": "oci",
"registryBaseUrl": "https://ghcr.io",
"identifier": "SamYuan1990/i18n-agent-action",
"version": "mcp",
"runtimeHint": "docker",
"transport": {
"type": "sse",
"url": "https://example.com:{host_port}/sse"
},
"runtimeArguments": [
{
"description": "Port mapping from host to container",
"value": "{host_port}:8080",
"type": "named",
"name": "-p",
"variables": {
"host_port": {
"description": "The host (local) port",
"isRequired": true,
"format": "number",
"default": "8080"
}
}
}
]
}
]
}
For docker we have to parameterize (templatize) the port (-p) argument with a variable to make the host port configurable. With the current resolution logic, there would be no way to map the token in the url to that (because there is no corresponding arg or env var representing just the host port).
I would argue that the mapping logic is already a little "spicy" since you could theoretically have runtime args, package args, and env vars with conflicting names (so you already need to specify the resolution logic / priority for deterministic resolution). My recommendation is that the guidance for token substitution be amended to say that after checking the runtime args, package args, and env vars (in that order) that you then fall back to checking for a matching variable under each of those things (in the same order). That would support the exact case above and keep the syntax clean.
I also considered a syntax like {p.host_port} indicating look in the first matching config element (arg/env) named "p", then use it's variable called "host_port". It's more specific, but I'm not sure that's worth the complexity for what it solves (directing to a specific variable when there might be multiple config elements with the same variable name - and since the variable names are only internal and are under the publisher's control, it's their own fault if they're ambiguous IMO).
Good callout.
I also considered a syntax like {p.host_port} indicating look in the first matching config element (arg/env) named "p", then use it's variable called "host_port".
I think I'd prefer this to the blind sub-variable matching. Though it would actually be {-p.host_port} in your example as -p is the name of the argument.
I could certainly live with the directed (dot) references.
I can't explain exactly why, but {-p.host_port} and even {--port} hurt my eyes, even though they are technically correct. It's complicated by the fact that many publishers aren't using the -- in the argument name (even though the docs are very clear about it), making the tokens look prettier, but causing the generated args to be wrong. I have a lax mode of config generation that works around this (both in token matching and config generation), but I'm not happy about it. I have a validator proposal coming soon (with schema validation and a linter to catch logic errors / misconfigurations, including things like missing dashes in arg names).
But back to the point: I'd advocate that we specify that it's OK to omit the leading dashes for named arg matching in the url token just to make it not hurt my eyes, but maybe that's not a good enough reason ;)
IMO, both StreamableHttpTransport and SseTransport url should have:
"pattern": "^https?://[^\\s]+$"
So as to at least encourage url-like values (versus free form strings)
Currently StreamableHttpTransport has no pattern or format, and SseTransport has:
"format": "uri"
which will prevent tokens in SSE (the intent to allow tokens in SSE seems clear, so I assume this is just an oversight).
Sorry I've been slow to follow up here - planning to get into it tomorrow
@BobDickinson @connor4312 regarding the last few comments -- am I correct in reading that those changes don't affect the shape of "adding template variable support in remote transports", and are both additional changes we could do as follow on PRs?
If so I will probably skip on those and let's do them in follow ups, just so I can land this. Sorry I've been slow here.
am I correct in reading that those changes don't affect the shape of "adding template variable support in remote transports", and are both additional changes we could do as follow on PRs?
Correct
I'd like to see the fix to the SSE format (which is a current bug that prevents SSE transports using templates, and fails on currently published server entries that would otherwise pass).
The more flexible template resolution could be a separate PR.
I'd like to see the fix to the SSE format (which is a current bug that prevents SSE transports using templates, and fails on currently published server entries that would otherwise pass).
The more flexible template resolution could be a separate PR.
Fair - included this.
Ready for final review @BobDickinson @connor4312 @modelcontextprotocol/registry-wg
Follow up ticket for the case @BobDickinson raised a few comments ago: https://github.com/modelcontextprotocol/registry/issues/656
@modelcontextprotocol/registry-wg sorry I forgot about landing this - just fixed up merge conflicts and would appreciate a final review here.