-
Notifications
You must be signed in to change notification settings - Fork 423
feat: Add template variable support for remote transport URLs #570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Enable URL templating for StreamableHttpTransport and SseTransport by adding 'arguments' and 'environmentVariables' arrays. This allows remote servers to support multi-tenant deployments with configurable endpoints. Key changes: - Add optional 'arguments' array to both transport types for valueHint-based templating - Add optional 'environmentVariables' array for environment-based templating - Update URL descriptions to clarify template variable resolution behavior - Add comprehensive multi-tenant example demonstrating the new functionality - Maintain backwards compatibility - existing configs continue to work This addresses the common use case where remote MCP servers are deployed across multiple tenants/regions, each requiring different endpoint URLs. Variables in {curly_braces} now properly resolve from the defined arrays. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Focus the example on just demonstrating the core templating functionality rather than complex multi-transport/multi-variable scenarios. The minimal example clearly shows how {tenant_host} resolves from environmentVariables.
…emplating Arguments make more sense for remote URL configuration since clients are configuring connection parameters, not running processes with environment variables. The valueHint 'tenant_host' maps to {tenant_host} in the URL.
Replace the complex arguments/environmentVariables approach with a clean 'variables' array that directly maps variable names to {curly_braces} in URLs. Key changes: - Replace 'arguments' and 'environmentVariables' with single 'variables' array - Update Transport struct to include Variables field - Fix Go validators to support template variables in remote URLs - Update example to use path-based templating for better namespace validation - All validation and schema checks now pass This provides a much cleaner API: variables array contains KeyValueInput objects where the 'name' field maps directly to {variable_name} in the URL. Example: { "url": "https://api.example.github.io/mcp/{tenant_id}", "variables": [{"name": "tenant_id", "description": "Tenant ID", "isRequired": true}] } 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Replace array-based variables with an object where keys are variable names and values are Input definitions. This provides a much cleaner API and enables rich client UIs. Key improvements: - Variables as object: {"tenant_id": {"description": "...", "isRequired": true}} - No duplicate 'name' field - variable name is the object key - Full Input capabilities: choices, isSecret, default, format, etc. - Consistent with existing InputWithVariables pattern - Client-friendly for building rich UIs (dropdowns, validation, etc.) Example: { "url": "https://api.example.github.io/mcp/{tenant_id}", "variables": { "tenant_id": { "description": "Tenant identifier", "isRequired": true, "choices": ["us-cell1", "emea-cell1"] } } } All validation and tests pass. Schema, Go types, validators, and example have been updated to support the new structure. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
"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} reference variable names from the 'variables' object. If variables are not provided, {curly_braces} should be treated as literal text. After variable substitution, this should produce a valid URI.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea with the references initially here is that a server that is launched with stdio and has, for example, a --port
argument could automatically reference the value of that argument in the transport. I hypothesize the vast majority of locally-launched servers are going to have an address detereministic from their command line inputs and (from a client PoV) asking the user to fill in the port in the url
separately from a port in the package_arguments
is just confusing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also,
All existing server.json files remain valid
this is not the case -- this change is breaking for any packages that were already published with the original reference behavior (not sure if there are any, this was only in a recent addition)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right. Thanks for the reminder that was the original reason we didn't include a separate field here.
The learning we've had since is that this doesn't work for the usage of StreamableHttpTransport
and SseTransport
within Remote
.
Will think more on how we can properly combine (or separate out) the use cases.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
definitely -- I suggest perhaps combining the variables
with an allOf
in just the Remote
case and adding some description in the property.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also re -
this is not the case -- this change is breaking for any packages that were already published with the original reference behavior (not sure if there are any, this was only in a recent addition)
That's my bad - I had forgotten this original use case inside Package
and thus thought the reference to arguments/envvars was just a typo. This is all clearer now that you've reminded me :) Thanks again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feeling pretty good about the direction now: https://github.com/modelcontextprotocol/registry/pull/570/files
Not ready to land this, but intent for me here was to explore whether we can add this functionality without breaking changes. I think the answer is yes, so might punt this for a few days.
Ensure that template variable validation doesn't bypass the existing IsValidRemoteURL check that prevents localhost URLs in remote transports. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
73c2f75
to
622a257
Compare
Only keep the core functional changes for URL templating feature: - Schema updates for variables object - Go type definitions - Validation logic - Documentation example - Test count update 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Address breaking change feedback by creating separate transport types: - Package transports: reference parent arguments/environment variables - Remote transports: define their own variables object Key changes: - Revert base Transport schema - keep original Package behavior unchanged - Add RemoteStreamableHttpTransport and RemoteSseTransport via allOf pattern - Create separate RemoteTransport Go type with Variables field - Update API types to use RemoteTransport for remotes array - Fix validation logic for both contexts - Update test files to use correct types This maintains backwards compatibility for Package context while enabling the needed templating functionality for Remote context. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Replace the weird RemoteStreamableHttpTransport and RemoteSseTransport with a single clean RemoteTransport type that supports both streamable-http and sse with a discriminated union approach. Benefits: - Much more DRY - no repetition between transport types - Clear separation: Package transports vs RemoteTransport - Cleaner schema - single RemoteTransport definition - Simplified Go types - RemoteTransport handles both cases This addresses feedback about the previous design being repetitive and confusing with dual-purpose transport definitions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Create clean separation with proper inheritance/composition: - **LocalTransport**: Union of StdioTransport | StreamableHttpTransport | SseTransport - Used in Package context for local/package transports - Non-breaking rename of existing concepts - **RemoteTransport**: Extends StreamableHttpTransport | SseTransport + variables - Reuses base transport definitions via allOf composition - Adds variables object for URL templating - Only supports streamable-http and sse (no stdio for remotes) Benefits: - ✅ Proper code reuse - shares StreamableHttp/Sse definitions - ✅ Clear naming - LocalTransport vs RemoteTransport - ✅ Non-breaking - just renames Package transport concept - ✅ DRY design - no duplication between transport types - ✅ Type safety - RemoteTransport can't use stdio This addresses feedback about wanting proper inheritance and clearer separation between local vs remote transport contexts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Add example showing LocalTransport templating with Package arguments to clarify the difference between Local vs Remote templating contexts: - Local: {port} references --port argument or valueHint from Package - Remote: {tenant_id} references variables object in RemoteTransport This helps users understand when to use Package arguments vs Remote variables for their URL templating needs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
The base StreamableHttpTransport and SseTransport descriptions were Package-specific but got inherited by RemoteTransport via allOf, causing incorrect documentation for the Remote context. Changes: - Update base URL descriptions to explain both Package and Remote contexts - Remove redundant URL overrides in RemoteTransport since base is now generic - Ensures correct documentation regardless of inheritance context Now the descriptions correctly explain: - Package context: Variables reference parent arguments/environment variables - Remote context: Variables reference transport's 'variables' object 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
- Add Variables field back to base Transport struct for compatibility - Revert API types to use []model.Transport for remotes - Keep schema separation with LocalTransport and RemoteTransport - This maintains schema benefits while fixing validation issue
The Remote Server with URL Templating example was failing namespace validation because the URL domain didn't match the reverse-DNS namespace. Fixed by ensuring the namespace `io.modelcontextprotocol.anonymous/multi-tenant-server` matches the URL `https://anonymous.modelcontextprotocol.io/mcp/{tenant_id}`. This ensures the example passes integration tests where remote URLs must match the publisher domain extracted from the namespace. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
This reverts commit 0e219a0.
I somehow missed this PR and created #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 |
Good callout.
I think I'd prefer this to the blind sub-variable matching. Though it would actually be |
I could certainly live with the directed (dot) references. I can't explain exactly why, but 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 "pattern": "^https?://[^\\s]+$" So as to at least encourage url-like values (versus free form strings) Currently StreamableHttpTransport has no "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 |
[Draft built largely with iterating with Claude Code; intended as a proof of concept, including this PR description, will clean up and review line by line later]
Summary
This PR adds template variable support for remote transport URLs, enabling multi-tenant deployments with configurable endpoints while maintaining full backwards compatibility.
Proper design - Uses clean
LocalTransport
andRemoteTransport
separation with proper inheritance/composition.Problem
The current schema promises URL templating with
{curly_braces}
for StreamableHttpTransport but provides no mechanism for Remote contexts to define what those variables should resolve to. This makes the feature unusable for remote servers.Referenced issue: #394
us-cell1.example.com
,emea-cell1.example.com
)Solution
Clean separation with proper inheritance:
LocalTransport:
StdioTransport | StreamableHttpTransport | SseTransport
{curly_braces}
reference parent arguments/environment variablesRemoteTransport:
(StreamableHttpTransport | SseTransport) + variables
allOf
composition - no duplication!variables
object for URL templatingKey Changes
Schema Updates
allOf
compositionLocalTransport
reference (cleaner, non-breaking)RemoteTransport
with proper inheritanceGo Types
Transport
struct unchanged for Package contextRemoteTransport
struct handles both cases withVariables
fieldVariable Definition
The Remote context extends base transports cleanly:
Benefits
Test Plan
Architecture
Perfect separation with inheritance:
{port}
references--port
argument orPORT
env var from parent Package{tenant_id}
referencesvariables.tenant_id
object, inherits all base transport propertiesallOf
, not duplicated🤖 Generated with Claude Code