
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 registry | MCP gateway | |
|---|---|---|
| Job | Catalog: which servers exist, which versions, where to get them, how to run them | Runtime 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 data | server.json entries (name, description, version, packages, remotes) | Policies: auth, tool allowlists, parameter overrides, redaction rules, rate limits |
| Sees actual tool calls? | No | Yes, so it can log and audit them |
| Where to get one | registry.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 it | MCP Registry (control plane) | Proxy Gateway (data plane) |
| Enough on its own when | A small team needs an approved list | Rarely. 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
agentThe 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.jsonneedsname,descriptionandversion. The name is a reverse-DNS namespace such asio.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_metaare 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 publishWhy 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.
| Figure | Source | Note |
|---|---|---|
| 5,000+ engineers; over 90% use AI "every single month" for agentic workflows | Talk, 0:34-0:45 | The video description shortens this to "90% using AI monthly" and drops "for agentic workflows" |
| 10,000+ services | Talk, 1:00-1:06 | Matches the description |
| "more than 1,500 agents just internally monthly active", 60,000+ executions per week | Talk, 1:18-1:27 | At 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 agent | Talk, 8:58-9:01 | Built on the Claude harness |
| "used by 95% of our engineers" | Talk, 9:03-9:08 | Refers 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-01 | These 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.yamlwith 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 searchandaifx mcp callfor 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 gateway | MCP gateway | AI / LLM gateway | |
|---|---|---|---|
| Traffic | HTTP APIs | JSON-RPC MCP: tool lists, tool calls, resources | Model inference requests |
| Routes on | Path, host, headers | Mcp-Method, Mcp-Name, Mcp-Param-* headers or the body | Model name, provider |
| Typical policy | Auth, quotas, 4xx/5xx handling | Tool allowlists, argument pinning, response redaction, call audit | Provider fallback, token budgets, prompt logging |
| Examples | See the cloud API gateway comparison | ContextForge, agentgateway, Docker MCP Gateway | AI 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
| Project | Licence, version | What it is |
|---|---|---|
| IBM ContextForge | Apache-2.0, v1.0.11 | Registry and proxy. Federates MCP, A2A and REST/gRPC, with virtual servers and a plugin framework. Its 2026-07-28 support is off by default. |
| agentgateway | Apache-2.0, v1.6.0 | Proxy for LLM, MCP and A2A traffic. CEL RBAC can match mcp.methodName. |
| Docker MCP Gateway | MIT, v0.44.1 | docker mcp plugin. Runs each server in its own container and logs and traces calls. |
| Microsoft MCP Gateway | MIT, no releases | Kubernetes reverse proxy. Supports 2026-07-28 clients only. |
| Envoy AI Gateway | Apache-2.0, v1.1.0 | AI gateway that also fronts MCP servers |
| ToolHive Registry Server | Apache-2.0, v1.5.2 | Private registry that implements the official API |
| Obot / MCPJungle / Unla | MIT v0.26.2 / MPL-2.0 0.4.6 / MIT v0.10.0 | Gateway 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 streamingBuild 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) # stdio4. 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
initializehandshake andMcp-Session-Idare gone. Each request carries its protocol version and capabilities in_meta, and servers MUST implementserver/discover. Gateways no longer need sticky sessions. - Routing headers.
Mcp-Methodis required on every Streamable HTTP request.Mcp-Nameis required fortools/call,resources/readandprompts/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 unrecognisedMcp-Param-*headers. - Cacheable lists. List results carry
ttlMsandcacheScope(publicorprivate), andcacheScopedecides whether shared intermediaries may cache them. - Tracing. OpenTelemetry
traceparent,tracestateandbaggagetravel in_meta. - Auth. Dynamic Client Registration is deprecated in favour of Client ID Metadata Documents. RFC 9207
issvalidation 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.jsonin 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
| Symptom | Cause | Fix |
|---|---|---|
HTTP 400, -32020 "mcp-name header does not match..." | Mcp-Name differs from params.name, or Mcp-Method is missing | Set both headers from the body in your client or proxy |
HTTP 400, -32022 Unsupported protocol version | The client sent a version the server doesn't list | Read data.supported in the error and use one of those versions |
| PII still reaches the model after redaction | Only structuredContent was rewritten | Rebuild content too, as gateway.py does |
| Agent picks the wrong tool, or the context fills with tool schemas | Too many tools exposed | Allowlist each agent's tools at the gateway, or add a tool-search layer such as Omni MCP |
| Denied calls behind the gateway | Allowlist or authorization policy | Check 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 publishing | The official registry keeps only publisher-provided (4KB) | Nest your keys under it with a reverse-DNS subkey |
| Registry shows an old version | The publisher hasn't published the new release | Pin 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 README | Add it so it matches the server name |
| Auth breaks after moving the authorization server | Credentials are bound to the issuing server, and iss is validated | Register the client again with the new server |
| ContextForge ignores 2026-07-28 clients | v1.0.11 defaults to legacy modes | Set MCP_CLIENT_CONNECT_MODE and MCP_INBOUND_PROTOCOL_MODE to auto and GATEWAY_MODERN_LISTENERS_ENABLED=true |
| Registry 404 body doesn't match your parser | The 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.