MCP Registry vs MCP Gateway: How Uber Runs Both at Scale

• Tapovan

In its "Designing MCP Gateway" engineering post (2026-10-01), Uber says it hosts over 800 MCP servers and over 5000 tools. It manages them with two components: an MCP Registry that it calls the control plane, and a Proxy Gateway that it calls the data plane. That split answers most of the MCP registry vs MCP gateway question. A registry tells agents what exists. A gateway decides what actually happens when an agent calls a tool. Below is the short answer, then the request flow, Uber's setup, and a small private registry and gateway in Python that I ran end to end. If MCP servers are new to you, start with this intro to MCP servers.

MCP registry vs MCP gateway in one table

MCP registryMCP gateway
JobCatalog: which servers exist, which versions, where to get them, how to run themRuntime control: who may call which tool, with which arguments, and what comes back
Sits in the request path?No. Clients and gateways read from it.Yes. Every tools/list and tools/call passes through it.
Main dataserver.json entries (name, description, version, packages, remotes)Policies: auth, tool allowlists, parameter overrides, redaction rules, rate limits
Sees actual tool calls?NoYes, so it can log and audit them
Where to get oneregistry.modelcontextprotocol.io (in preview, public servers only)Several open-source projects (see the table further down). The 2026-07-28 spec adds headers so gateways can route without parsing the body.
Uber's name for itMCP Registry (control plane)Proxy Gateway (data plane)
Enough on its own whenA small team needs an approved listRarely. It needs a source of truth for its config.

Most enterprise setups run both, and many gateway products ship a registry built in. Seen from the gateway side (MCP gateway vs MCP registry), the registry is where the gateway gets its config.

How one tool call flows through a registry and a gateway

This is the MCP gateway architecture the rest of this post builds. The diagram is mine. It matches both Uber's design and the Python build below.

agent / IDE
   |  tools/list, tools/call  (Mcp-Method, Mcp-Name headers)
   v
MCP gateway  --(reads policy, cached)-->  private registry  (server.json + policy _meta)
   |  1. authenticate caller, check authorization
   |  2. hide tools not on the allowlist
   |  3. overwrite pinned arguments
   v
MCP server (internal service or third party)
   |
   v
MCP gateway  4. redact PII in the response
             5. append an audit record  -->  audit log / traces
   |
   v
agent

The registry is consulted when config changes or on a refresh cycle. Uber's data plane "refreshes its in-memory state at a fixed cadence". It is not consulted on every call.

What is an MCP registry, and what does it actually store?

An MCP registry is a catalog of server metadata, stored as server.json documents. It describes servers that expose tools, resources and prompts. It does not host packages or proxy traffic.

server.json, namespaces and the official registry

The official MCP Registry is still in preview: "Breaking changes or data resets may occur before general availability." The live service reports version 1.8.1. Points worth knowing before you rely on it:

  • A server.json needs name, description and version. The name is a reverse-DNS namespace such as io.github.<username>/server, verified through GitHub, DNS or an HTTP challenge. The description is capped at 100 characters.
  • Only servers with a public install method (npm, PyPI, a public Docker image) or a publicly reachable remote can be listed. Packages must come from allowlisted public registries.
  • Only _meta["io.modelcontextprotocol.registry/publisher-provided"] is kept, with a 4KB cap. Any other keys under _meta are silently dropped.
  • Security scanning is not its job. It handles namespace authentication and metadata hosting, and leaves scanning to package registries and downstream aggregators.

The read API is plain HTTP. These calls are tested with curl on 2026-10-06:

curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=github&version=latest&limit=3"
curl "https://registry.modelcontextprotocol.io/v0.1/servers/io.github.github%2Fgithub-mcp-server/versions/latest"
curl "https://registry.modelcontextprotocol.io/v0.1/version"

Two things surprised me. search is a substring match on the name, so the top hits for "github" were Smithery-hosted servers, not GitHub's own. And GitHub's entry returned version 1.13.0, while GitHub's latest release was already v1.14.0 (2026-10-02). Registry metadata stays behind until the publisher publishes the new version.

Publishing goes through mcp-publisher. Untested: this needs a real public package and a GitHub login. The syntax is copied from the registry quickstart.

brew install mcp-publisher        # or download the release binary
mcp-publisher init                # writes a server.json template
mcp-publisher login github        # name must start with io.github.<user>/
mcp-publisher publish

Why enterprises run a private MCP registry

The official docs are direct about this: "The MCP Registry does not support private servers", and its codebase "is not designed for self-hosting". If you have internal servers, you need your own private MCP registry. The docs recommend implementing the registry's OpenAPI spec so existing host apps can read from it. The read paths are GET /v0.1/servers (with search, cursor, limit, updated_since, version) and GET /v0.1/servers/{serverName}/versions/{version}, where the name is URL-encoded and the version can be latest. ToolHive Registry Server (Apache-2.0, v1.5.2) is an open-source implementation that adds per-registry visibility based on JWT claims and SIEM audit logging.

Beyond metadata: quality scores, SLA tiers and versions

In the talk's roadmap section (12:00-12:28), Uber says it wants evaluation metrics and service SLAs in the registry, so agents can prefer higher-tier MCP servers. The schema has no field for that, but publisher-provided_meta can carry it without breaking portability. The MCP registry example in the build section does exactly that.

What is an MCP gateway, and what does it enforce at runtime?

An MCP gateway is a proxy between MCP clients and servers that enforces policy on live traffic. The 2026-07-28 transport spec has Mcp-Method and Mcp-Name headers so that "intermediaries (load balancers, gateways, observability tooling) can route and inspect requests without parsing the body".

Central authorization and per-agent tool scoping

Uber authorizes through its internal Access Control System. Policies are set per caller actor ("humans, services, and agents"), at the server level with optional overrides per tool. Scoping also helps reliability. In the talk (3:08), Uber says bad tools do more than fail: they "degrade the agent performance". An agent that sees fewer tools picks the wrong one less often.

Parameter overrides to stop hallucinated arguments

Uber's Agent Builder (talk, 9:20-10:33) lets the owner pin parameter values, so the model no longer chooses them; Uber says the LLM "doesn't have to make a decision" there. The Agent SDK's config.yaml does the same in code (10:38). In a gateway, this becomes "overwrite these keys before forwarding".

PII redaction, mutable-endpoint blocking and audit logs

According to Uber's post, the gateway does "out-of-the-box redaction for any PII or sensitive data from the tool responses". For third-party servers such as Jira and Google, it relays the caller's user token while still applying authorization, rate limiting and redaction. The talk (5:24) adds guardrails that block "mutable endpoints" able to take down critical services, plus logging, metrics and tracing. Uber's post does not describe how the blocking works.

Case study: Uber's enterprise MCP gateway for 60,000 weekly agent runs

Uber does not treat MCP registry vs MCP gateway as an either-or choice. It runs both, and the two sources give numbers that measure different things, so I keep them apart below. The talk is from MCP Dev Summit North America (April 2026). Uber's blog post came later.

FigureSourceNote
5,000+ engineers; over 90% use AI "every single month" for agentic workflowsTalk, 0:34-0:45The video description shortens this to "90% using AI monthly" and drops "for agentic workflows"
10,000+ servicesTalk, 1:00-1:06Matches the description
"more than 1,500 agents just internally monthly active", 60,000+ executions per weekTalk, 1:18-1:27At 7:47 the same talk says "thousands" of Agent Builder agents alone are active monthly. Those two statements don't agree unless "thousands" is loose, so I quote 1,500+.
About 1,800 code changes a week from the Minions background agentTalk, 8:58-9:01Built on the Claude harness
"used by 95% of our engineers"Talk, 9:03-9:08Refers to the coding agents as a group (Claude Code, Cursor, Minions), not Minions alone
Over 800 MCP servers, over 5000 tools, "tens of thousands of employees"Uber blog, 2026-10-01These are different metrics from the talk's figures. Don't combine them.

Auto-generating MCP tools from 10,000 service IDLs

This is how Uber scales. Its post describes an AutoCrawler on Cadence workflows that reads Protobuf and Thrift IDLs and "uses an LLM to generate enriched, agent-friendly MCP tool descriptions". The talk (5:40) adds that the descriptions are based on message names and comments. Generation does not mean exposure: "Every MCP server and tool starts in a disabled state and must be explicitly reviewed and enabled by the owning team." At runtime, the Proxy Gateway "translates MCP protocol calls into HTTP, gRPC, or TChannel requests" through Uber's Muttley sidecar.

Owner edits as reviewed, security-scanned diffs

When an owner edits a tool description, that creates a diff. Engineering security's scanning APIs check it, the scan report is attached, and then it is committed and deployed (talk, 6:43-7:11). Owners must approve the diff, and they can roll back to an earlier version (blog). Code scanning also runs periodically, not only at commit time (talk, 4:49).

Three consumer surfaces

  • Agent Builder (no-code): @mention the MCP server in the system instructions, pick specific tools and set parameter overrides.
  • Agent SDK (code-first): a config.yaml with the MCP name or ID, the selected tools and the overrides. Uber uses it for its grocery, care coordination and customer support agents.
  • Coding agents: the AIFX CLI has an add command that makes remote or local servers available to Claude Code and Cursor (talk, 11:16). The blog documents aifx mcp list, aifx mcp search and aifx mcp call for Code Mode.

To cut context bloat, Omni MCP exposes four tools, discover_server, discover_tools, get_tool_schema and invoke_tool, so agents discover tools step by step. Response Projection lets the caller name the fields it wants, and the gateway trims the response to those fields.

Third-party vs internal MCP servers

External servers get more gating, scanning and checks than internal ones. Uber also shut down one-off playgrounds, and everything is "centrally committed and managed in code" (talk, 4:05-4:33).

Watch: How Uber Runs 60,000 AI Agent Tasks Per Week With MCP by Agentic AI Foundation

Meghana Somasundara and Rush Tehrani explain why Uber put the registry and gateway in front of everything. The talk is strongest on the problems (1:50-3:27), the control-plane design (3:29-5:37), the IDL-crawling architecture (5:40-7:11) and the per-surface tool scoping (9:20-11:40). The roadmap at 12:00 covers SLA tiers and Omni MCP.

Published by the Agentic AI Foundation channel. For implementation detail, read it alongside Uber's Designing MCP Gateway post.

MCP gateway vs API gateway vs AI (LLM) gateway

This is my framing of how the three differ, based on the spec and the projects' READMEs. Many products now cover more than one column.

API gatewayMCP gatewayAI / LLM gateway
TrafficHTTP APIsJSON-RPC MCP: tool lists, tool calls, resourcesModel inference requests
Routes onPath, host, headersMcp-Method, Mcp-Name, Mcp-Param-* headers or the bodyModel name, provider
Typical policyAuth, quotas, 4xx/5xx handlingTool allowlists, argument pinning, response redaction, call auditProvider fallback, token budgets, prompt logging
ExamplesSee the cloud API gateway comparisonContextForge, agentgateway, Docker MCP GatewayAI gateways like Cloudflare and Vercel, Envoy AI Gateway

One point specific to MCP: since 2026-07-28 a plain API gateway can route MCP traffic on headers. However, the spec says a policy-enforcing intermediary SHOULD reject requests whose MCP-Protocol-Version is missing or older than the revision that requires header-body validation. Otherwise it is trusting header values that nobody has checked against the body.

MCP gateway open source options

ProjectLicence, versionWhat it is
IBM ContextForgeApache-2.0, v1.0.11Registry and proxy. Federates MCP, A2A and REST/gRPC, with virtual servers and a plugin framework. Its 2026-07-28 support is off by default.
agentgatewayApache-2.0, v1.6.0Proxy for LLM, MCP and A2A traffic. CEL RBAC can match mcp.methodName.
Docker MCP GatewayMIT, v0.44.1docker mcp plugin. Runs each server in its own container and logs and traces calls.
Microsoft MCP GatewayMIT, no releasesKubernetes reverse proxy. Supports 2026-07-28 clients only.
Envoy AI GatewayApache-2.0, v1.1.0AI gateway that also fronts MCP servers
ToolHive Registry ServerApache-2.0, v1.5.2Private registry that implements the official API
Obot / MCPJungle / UnlaMIT v0.26.2 / MPL-2.0 0.4.6 / MIT v0.10.0Gateway and registry platforms (versions checked; features not verified)

Untested quick starts copied from the READMEs. ContextForge also needs .env secrets and a JWT, and Docker needs Docker Desktop:

pip install mcp-contextforge-gateway && mcpgateway --host 0.0.0.0 --port 4444      # IBM ContextForge
docker mcp gateway run --profile dev-tools                                       # Docker MCP Gateway
docker mcp gateway run --port 8080 --transport streaming

Build a minimal private registry and MCP gateway in Python

The quickest way to see the MCP registry vs MCP gateway split is to build both small. The build has five files: a catalog, a registry API that serves it, a backend MCP server, the gateway, and a client. It is tested on Windows 11, Python 3.12.10, fastmcp 4.0.11 and mcp 2.3.0 (pip install fastmcp mcp).

1. The catalog: registry.json

Each entry is a standard server.json. The gateway policy goes under publisher-provided with a reverse-DNS subkey, because that is the only _meta key the official registry keeps. owner, tier and eval_score are illustrative fields, modelled on Uber's roadmap.

{
  "servers": [{
    "server": {
      "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
      "name": "com.example.internal/trips-service",
      "title": "Trips service",
      "description": "Read trips and rider profiles from the internal trips service.",
      "version": "1.4.0",
      "packages": [{"registryType": "pypi", "identifier": "trips-service-mcp", "version": "1.4.0",
                    "transport": {"type": "stdio"}}],
      "_meta": {
        "io.modelcontextprotocol.registry/publisher-provided": {
          "com.example.gateway/policy": {
            "owner": "team-trips", "tier": "tier-1", "eval_score": 0.92,
            "allowed_tools": ["get_rider", "search_trips"],
            "parameter_overrides": {"search_trips": {"region": "in-south", "limit": 3}},
            "redact_fields": ["email", "phone"]
          }
        }
      }
    },
    "_meta": {"io.modelcontextprotocol.registry/official": {"status": "active", "isLatest": true}}
  }],
  "metadata": {"count": 1}
}

2. Registry API: registry_api.py

A standard-library subset of the official v0.1 read paths. Its 404 body uses the same shape as the live registry's.

import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import parse_qs, unquote, urlparse

DATA = json.load(open("registry.json", encoding="utf-8"))
OFFICIAL = "io.modelcontextprotocol.registry/official"

class Handler(BaseHTTPRequestHandler):
    def _send(self, code, body):
        raw = json.dumps(body).encode()
        self.send_response(code); self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(raw))); self.end_headers(); self.wfile.write(raw)

    def do_GET(self):
        u = urlparse(self.path); q = parse_qs(u.query); parts = u.path.strip("/").split("/")
        if u.path == "/v0.1/servers":                       # GET /v0.1/servers?search=
            s = q.get("search", [""])[0]
            hits = [e for e in DATA["servers"] if s in e["server"]["name"]]
            return self._send(200, {"servers": hits, "metadata": {"count": len(hits)}})
        # GET /v0.1/servers/{serverName}/versions/{version}   (version may be "latest")
        if len(parts) == 5 and parts[:2] == ["v0.1", "servers"] and parts[3] == "versions":
            name, ver = unquote(parts[2]), unquote(parts[4])
            for e in DATA["servers"]:
                srv = e["server"]
                latest = ver == "latest" and e["_meta"][OFFICIAL]["isLatest"]
                if srv["name"] == name and (ver == srv["version"] or latest):
                    return self._send(200, e)
        self._send(404, {"title": "Not Found", "status": 404, "detail": "Server not found"})

    def log_message(self, *args): pass

if __name__ == "__main__":
    HTTPServer(("127.0.0.1", int(sys.argv[1]) if len(sys.argv) > 1 else 8765), Handler).serve_forever()

3. Backend server: trips_server.py

This stands in for an auto-generated internal service. get_rider returns PII, and cancel_trip modifies data.

from fastmcp import FastMCP
mcp = FastMCP("trips-service")

@mcp.tool(annotations={"readOnlyHint": True})
def get_rider(rider_id: str) -> dict:
    """Look up a rider profile by ID."""
    return {"rider_id": rider_id, "name": "Asha Rao", "email": "asha.rao@example.com",
            "phone": "+91-98450-12345", "city": "Bengaluru", "rating": 4.9}

@mcp.tool(annotations={"readOnlyHint": True})
def search_trips(city: str, limit: int = 5, region: str = "global") -> dict:
    """Search recent trips in a city."""
    return {"city": city, "region": region, "limit": limit,
            "trips": [{"trip_id": f"t-{i}", "fare": 210 + i} for i in range(min(limit, 3))]}

@mcp.tool(annotations={"readOnlyHint": False, "destructiveHint": True})
def cancel_trip(trip_id: str) -> str:
    """Cancel a trip (mutating)."""
    return f"trip {trip_id} cancelled"

if __name__ == "__main__":
    mcp.run(show_banner=False)  # stdio

4. The gateway: gateway.py

The gateway loads the policy from the registry, then wraps a FastMCP proxy in middleware. on_list_tools hides tools that are not on the allowlist. on_call_tool denies those tools, merges in the pinned arguments, redacts the response and writes an audit line.

import json, sys, time, urllib.request
from pathlib import Path
from fastmcp.exceptions import ToolError
from fastmcp.server import create_proxy
from fastmcp.server.middleware import Middleware, MiddlewareContext
from fastmcp.tools import ToolResult
from mcp.types import TextContent

REGISTRY = "http://127.0.0.1:8765"
SERVER = "com.example.internal%2Ftrips-service"   # URL-encoded, as the OpenAPI spec requires
POLICY_KEY = "io.modelcontextprotocol.registry/publisher-provided"
AUDIT = Path("audit.jsonl")

def load_policy():
    with urllib.request.urlopen(f"{REGISTRY}/v0.1/servers/{SERVER}/versions/latest") as r:
        entry = json.load(r)["server"]
    return f'{entry["name"]}@{entry["version"]}', entry["_meta"][POLICY_KEY]["com.example.gateway/policy"]

def redact(obj, fields):
    if isinstance(obj, dict):
        return {k: ("[REDACTED]" if k in fields else redact(v, fields)) for k, v in obj.items()}
    if isinstance(obj, list):
        return [redact(v, fields) for v in obj]
    return obj

class Governance(Middleware):
    def __init__(self, server_id, policy):
        self.server_id, self.p = server_id, policy

    def audit(self, **rec):
        with AUDIT.open("a", encoding="utf-8") as f:
            f.write(json.dumps({"ts": round(time.time(), 3), "server": self.server_id, **rec}) + "\n")

    async def on_list_tools(self, context: MiddlewareContext, call_next):
        tools = await call_next(context)
        return [t for t in tools if t.name in self.p["allowed_tools"]]   # agents only see allowlisted tools

    async def on_call_tool(self, context: MiddlewareContext, call_next):
        name, args = context.message.name, dict(context.message.arguments or {})
        if name not in self.p["allowed_tools"]:
            self.audit(tool=name, args=args, decision="deny")
            raise ToolError(f"Tool '{name}' is not allowlisted for {self.server_id}")
        forced = self.p["parameter_overrides"].get(name, {})
        context.message.arguments = {**args, **forced}                   # static values beat model-chosen ones
        result = await call_next(context)
        data = redact(result.structured_content, set(self.p["redact_fields"]))
        self.audit(tool=name, args=args, overridden=forced, decision="allow")
        # rebuild BOTH content and structured_content, or PII leaks via the text block
        return ToolResult(content=[TextContent(type="text", text=json.dumps(data))], structured_content=data)

server_id, policy = load_policy()
backend = {"mcpServers": {"trips": {"command": sys.executable, "args": ["trips_server.py"]}}}
gateway = create_proxy(backend, name="mcp-gateway")
gateway.add_middleware(Governance(server_id, policy))

if __name__ == "__main__":
    if len(sys.argv) > 1 and sys.argv[1] == "http":
        gateway.run(transport="http", host="127.0.0.1", port=8800, show_banner=False)
    else:
        gateway.run(show_banner=False)

The redaction gotcha. FastMCP returns a tool's dict twice: once as structuredContent and once as a JSON string in content[0].text. I checked this by calling get_rider directly on trips_server.mcp with a fastmcp.Client. Both fields contained asha.rao@example.com. If a redactor rewrites only structured_content, the PII still reaches the model through the text block. That is why the last line of on_call_tool rebuilds both fields.

5. Run it: agent_client.py

import asyncio, sys
from fastmcp import Client

async def main():
    async with Client({"mcpServers": {"gw": {"command": sys.executable, "args": ["gateway.py"]}}}) as c:
        print("tools via gateway:", [t.name for t in await c.list_tools()])
        print("get_rider:", (await c.call_tool("get_rider", {"rider_id": "r-42"})).structured_content)
        r = await c.call_tool("search_trips", {"city": "Bengaluru", "limit": 50, "region": "us-west"})
        print("search_trips (model sent limit=50, region=us-west):", r.structured_content)
        r = await c.call_tool("cancel_trip", {"trip_id": "t-1"}, raise_on_error=False)
        print("cancel_trip:", "is_error=", r.is_error, "|", r.content[0].text)
    print(open("audit.jsonl", encoding="utf-8").read())

asyncio.run(main())

Start python registry_api.py 8765 &, then run python agent_client.py. This is the actual output:

tools via gateway: ['get_rider', 'search_trips']
get_rider: {'rider_id': 'r-42', 'name': 'Asha Rao', 'email': '[REDACTED]', 'phone': '[REDACTED]', 'city': 'Bengaluru', 'rating': 4.9}
search_trips (model sent limit=50, region=us-west): {'city': 'Bengaluru', 'region': 'in-south', 'limit': 3, 'trips': [{'trip_id': 't-0', 'fare': 210}, {'trip_id': 't-1', 'fare': 211}, {'trip_id': 't-2', 'fare': 212}]}
cancel_trip: is_error= True | Tool 'cancel_trip' is not allowlisted for com.example.internal/trips-service@1.4.0
--- audit.jsonl ---
{"ts": 1791227167.491, "server": "com.example.internal/trips-service@1.4.0", "tool": "get_rider", "args": {"rider_id": "r-42"}, "overridden": {}, "decision": "allow"}
{"ts": 1791227167.874, "server": "com.example.internal/trips-service@1.4.0", "tool": "search_trips", "args": {"city": "Bengaluru", "limit": 50, "region": "us-west"}, "overridden": {"region": "in-south", "limit": 3}, "decision": "allow"}
{"ts": 1791227167.879, "server": "com.example.internal/trips-service@1.4.0", "tool": "cancel_trip", "args": {"trip_id": "t-1"}, "decision": "deny"}

The audit log keeps what the model asked for next to what the gateway forced. That record is how you find agents that keep sending bad arguments. The proxy starts the backend lazily, on first use.

What the MCP 2026-07-28 spec changes for gateways

The 2026-07-28 revision removes most of the reasons gateways had to be stateful:

  • Stateless requests. The initialize handshake and Mcp-Session-Id are gone. Each request carries its protocol version and capabilities in _meta, and servers MUST implement server/discover. Gateways no longer need sticky sessions.
  • Routing headers.Mcp-Method is required on every Streamable HTTP request. Mcp-Name is required for tools/call, resources/read and prompts/get. These headers do not apply to stdio. If a header does not match the body, the server must return HTTP 400 with -32020 (HeaderMismatch, previously -32001). Gateways MUST forward unrecognised Mcp-Param-* headers.
  • Cacheable lists. List results carry ttlMs and cacheScope (public or private), and cacheScope decides whether shared intermediaries may cache them.
  • Tracing. OpenTelemetry traceparent, tracestate and baggage travel in _meta.
  • Auth. Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents. RFC 9207 iss validation is added, and client credentials are bound to the authorization server that issued them.

Identity used to be the weak point. The Enterprise-Managed Authorization extension (stable as of 2026-06-18) changes that. The client exchanges the user's SSO token at the corporate IdP for an ID-JAG. The IdP evaluates policy before issuing it, and the client then exchanges the ID-JAG for an MCP access token. Okta supports this through Cross App Access. You still need an IdP, but it now takes part in the protocol itself rather than being bolted onto the gateway.

I ran the gateway over HTTP (python gateway.py http) and probed it with raw requests. This is tested on fastmcp 4.0.11:

META='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}'
curl -s http://127.0.0.1:8800/mcp -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: 2026-07-28" \
  -H "Mcp-Method: tools/call" -H "Mcp-Name: get_rider" \
  -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"get_rider\",\"arguments\":{\"rider_id\":\"r-1\"},$META}}"

With correct headers, the call returns 200 and both content fields are redacted. tools/list returned "cacheScope":"private","ttlMs":0, so by default FastMCP lets no shared cache store the list. A legacy initialize at version 2025-11-25 still received an mcp-session-id. FastMCP 4 serves both protocol versions on one endpoint, even though server/discover lists only 2026-07-28. The errors are in the table below.

Do you need a registry, a gateway, or both?

In practice, the MCP registry vs MCP gateway decision depends on team size and on what your agents can touch:

  • Solo developer or a handful of public servers: neither. Pin server versions in your client config. Use the official registry to discover servers, but remember it is in preview.
  • One team, internal servers, low risk: a private MCP registry (or simply a reviewed registry.json in git) as the approved inventory. That stops shadow servers but does not control calls.
  • Agents touching customer data or mutating systems: add a gateway. Once you need per-caller tool scoping, pinned arguments, redaction or an audit of real calls, something has to sit in the request path.
  • Many teams and hundreds of servers (the Uber case): both, with the registry as the control plane that feeds the gateway. Add generated tool definitions with owner review, and tool search to keep context small.

Common problems and fixes

SymptomCauseFix
HTTP 400, -32020 "mcp-name header does not match..."Mcp-Name differs from params.name, or Mcp-Method is missingSet both headers from the body in your client or proxy
HTTP 400, -32022 Unsupported protocol versionThe client sent a version the server doesn't listRead data.supported in the error and use one of those versions
PII still reaches the model after redactionOnly structuredContent was rewrittenRebuild content too, as gateway.py does
Agent picks the wrong tool, or the context fills with tool schemasToo many tools exposedAllowlist each agent's tools at the gateway, or add a tool-search layer such as Omni MCP
Denied calls behind the gatewayAllowlist or authorization policyCheck the audit log's decision field first. The approach is the same as for 403 Forbidden errors from API gateways.
Custom _meta keys disappear after publishingThe official registry keeps only publisher-provided (4KB)Nest your keys under it with a reverse-DNS subkey
Registry shows an old versionThe publisher hasn't published the new releasePin versions you have checked. Aggregators should poll about once an hour.
mcp-publisher: "Registry validation failed for package"Missing mcpName in package.json, or the mcp-name: line in a PyPI READMEAdd it so it matches the server name
Auth breaks after moving the authorization serverCredentials are bound to the issuing server, and iss is validatedRegister the client again with the new server
ContextForge ignores 2026-07-28 clientsv1.0.11 defaults to legacy modesSet MCP_CLIENT_CONNECT_MODE and MCP_INBOUND_PROTOCOL_MODE to auto and GATEWAY_MODERN_LISTENERS_ENABLED=true
Registry 404 body doesn't match your parserThe live registry returns {"title","status","detail"}, but the OpenAPI example shows {"error"}Handle both shapes

FAQ

What is an MCP registry?

A catalog of MCP server metadata (name, version, where to get the server, how to run it), stored as server.json. The official registry is in preview and lists only publicly reachable servers. Its OpenAPI spec is the one private registries should implement.

Can I put internal servers in the official MCP registry?

No. The official registry does not support private servers, and its code is not designed for self-hosting. Run a private registry that implements the same API, such as ToolHive Registry Server or the small one above.

Do I need a gateway if I already have a registry?

Only if you need control over actual calls: per-caller tool scoping, pinned arguments, redaction or an audit trail. A registry never sees tool calls. Uber runs both, with the registry as the control plane and the gateway as the data plane.

Is there an open-source MCP gateway?

Yes. IBM ContextForge, agentgateway, Docker MCP Gateway, Microsoft MCP Gateway and Envoy AI Gateway are all open source. Licences and versions are in the table above.

What is the difference between an MCP gateway and an API gateway?

An API gateway routes HTTP by path and headers. An MCP gateway understands MCP methods, tool names, tool lists and arguments. With the 2026-07-28 headers a generic proxy can route MCP traffic, but it should reject requests with older protocol versions before it enforces policy on those headers.

For a worked example of an agent running on this kind of platform, see how to debug production issues with AI agents, based on Uber's Debug Assist.

Last updated: 2026-10-06; tested on Python 3.12.10, fastmcp 4.0.11, mcp 2.3.0, Windows 11, against the official registry service 1.8.1. The mcp-publisher, ContextForge and Docker MCP Gateway commands are untested.

Last updated: October 06, 2026
an "open and free" initiative. Powered by Blogger.