registry icon indicating copy to clipboard operation
registry copied to clipboard

feat: Add LocalTransport/RemoteTransport with URL template variables

Open tadasant opened this issue 3 months ago • 12 comments

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 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

{
  "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).

tadasant avatar Sep 29 '25 15:09 tadasant

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).

BobDickinson avatar Oct 01 '25 21:10 BobDickinson

I'm happy with this approach 👍. Might just need a rebase and minor code cleanup

domdomegg avatar Oct 02 '25 17:10 domdomegg

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).

BobDickinson avatar Oct 03 '25 22:10 BobDickinson

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.

connor4312 avatar Oct 03 '25 22:10 connor4312

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 ;)

BobDickinson avatar Oct 04 '25 00:10 BobDickinson

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).

BobDickinson avatar Oct 07 '25 18:10 BobDickinson

Sorry I've been slow to follow up here - planning to get into it tomorrow

tadasant avatar Oct 09 '25 01:10 tadasant

@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.

tadasant avatar Oct 13 '25 15:10 tadasant

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

connor4312 avatar Oct 13 '25 15:10 connor4312

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.

BobDickinson avatar Oct 13 '25 16:10 BobDickinson

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

tadasant avatar Oct 13 '25 17:10 tadasant

Follow up ticket for the case @BobDickinson raised a few comments ago: https://github.com/modelcontextprotocol/registry/issues/656

tadasant avatar Oct 13 '25 17:10 tadasant

@modelcontextprotocol/registry-wg sorry I forgot about landing this - just fixed up merge conflicts and would appreciate a final review here.

tadasant avatar Dec 05 '25 21:12 tadasant