Once you have more than one MCP server, the question stops being “how do I expose a tool?” and becomes “how do I expose many tools without turning every AI client into a spaghetti of credentials, transports, and half-documented endpoints?” That is the moment people start talking about an MCP gateway.
I have already written about what MCP servers are and how I choose between stdio, Streamable HTTP, and SSE. This post sits one layer above those pieces. It is the architecture I reach for when local demos are fine, but a real team needs a single place to attach Claude Code, Cursor, or an internal agent host — with auth, allowlists, and logs that an enterprise security review will not immediately reject.
I am not selling a product here. This is the pattern I would sketch on a whiteboard for a .NET shop that already runs APIs on Azure and does not want every agent process spawning its own copy of every tool server.
What an MCP Gateway Actually Is
In the wild, “MCP gateway” gets used for three slightly different things:
- A reverse proxy for JSON-RPC — one public endpoint, many backend MCP servers.
- A transport adapter — wrap stdio servers so remote clients can reach them over Streamable HTTP.
- A policy plane — authentication, per-tool allowlists, rate limits, audit trails, and catalog aggregation.
A useful enterprise gateway is all three. If you only reverse-proxy without policy, you have moved the mess behind a nicer hostname. If you only do policy without transport bridging, half your existing stdio servers stay stuck on laptops.
The mental model I use is deliberately boring:
- Northbound: one MCP endpoint that clients understand (preferably Streamable HTTP).
- Southbound: many servers — some remote Streamable HTTP, some legacy SSE, some local stdio wrapped by the gateway process or a sidecar.
- Control plane: registry of servers/tools, authn/authz, observability.
That is closer to an API gateway plus a service registry than it is to “another chat plugin.”
Why Teams Outgrow Point-to-Point MCP
Point-to-point works for a single developer. It falls over in familiar ways once more people join:
- Every client config file lists five servers, five URLs, and five secret storage stories.
- stdio servers cannot be shared cleanly across a team or a CI runner.
- Tool names collide (
search,query,run) with no namespace strategy. - Security asks who can call
execute_sqland you have no answer beyond “the laptop that has the config.” - You cannot rotate a backend without editing every client.
None of that means MCP is wrong. It means the integration boundary needs a facade, the same way we stopped giving every browser a direct connection to every microservice.
Reference Architecture I Would Use in .NET
Here is the shape I would implement first, then harden.
| Layer | Responsibility | .NET-friendly choice |
|---|---|---|
| Edge | TLS, WAF, private networking | Azure Front Door / App Gateway, or internal only via VNet |
| Gateway host | MCP session, routing, policy | ASP.NET Core + official MCP C# SDK transports |
| Identity | Who is calling | Entra ID (OAuth 2.1 / OIDC), workload identity for services |
| Registry | Server catalog, tool metadata, versions | Config + SQL/Cosmos; eventually a small admin API |
| Backends | Actual tools | Existing MCP servers, internal APIs wrapped as tools |
| Telemetry | Traces, tool audit, failures | OpenTelemetry → App Insights |
Two design choices I treat as non-negotiable for anything beyond a lab:
- Northbound transport is Streamable HTTP (stateful only if you truly need sessions; stateless when you can get away with it). Clients that only speak stdio can use a thin local bridge, but the shared service is HTTP.
- The gateway never becomes the place business logic lives. It routes, authorizes, and observes. Domain rules stay in the backend servers or existing APIs.
Core Responsibilities, in Practice
1. Catalog aggregation
Clients should see a merged tool list. Behind the scenes you will:
- Connect to each registered backend
- Pull
tools/list(and prompts/resources if you expose them) - Prefix or namespace tool names to avoid collisions — for example
billing.refundvs barerefund - Refresh on a schedule or on admin signal when a backend deploys
I prefer explicit namespaces over hoping two teams never pick the same verb.
2. Request routing
When a client calls a tool, the gateway:
- Authenticates the caller
- Checks allowlist / role policy for that tool
- Maps the public tool name to a backend + original tool name
- Forwards the JSON-RPC call with a correlation id
- Returns the result (or a sanitized error)
Keep the public schema stable even if a backend renames an internal tool. That stability is half the point of the facade.
3. Transport bridging
stdio is still the fastest way to build a tool server on a workstation. In production I wrap those processes deliberately — supervisor restarts them, the gateway owns their lifetime, and they are not reachable except through the gateway. Remote backends should already speak Streamable HTTP. Legacy SSE gets a compatibility adapter with a sunset date, not a forever home. I covered the trade-offs in the transport comparison.
4. Auth and tool policy
MCP’s authorization story has matured around HTTP transports and OAuth-style flows. In an enterprise .NET environment I still design policy the way I would for any sensitive API:
- Service-to-service: managed identity / client credentials where appropriate
- User-delegated agents: Entra ID tokens with clear audience and scopes
- Tool-level authorization: not just “is logged in,” but “may call
payments.capture” - Optional approval workflows for high-risk tools (human in the loop) outside the protocol if needed
If everything is allowed once you pass the front door, you built a reverse proxy with better marketing.
A Minimal .NET Sketch
The following is intentionally incomplete. It shows the seams I care about — registry, policy, and forward — rather than a full production host. Use the official MCP C# SDK types for the real wire protocol; treat this as architecture-shaped pseudocode that compiles in spirit.
public sealed record McpBackend(
string Id,
string PublicNamespace,
Uri? HttpEndpoint,
BackendKind Kind);
public enum BackendKind { StreamableHttp, StdioSupervised }
public interface IToolCatalog
{
Task RefreshAsync(CancellationToken ct);
bool TryResolve(string publicToolName, out ResolvedTool tool);
}
public sealed record ResolvedTool(
string BackendId,
string UpstreamToolName,
IReadOnlyList<string> RequiredRoles);
public sealed class GatewayToolDispatcher
{
private readonly IToolCatalog _catalog;
private readonly IAuthorizationService _authz;
private readonly IBackendClientFactory _clients;
private readonly ILogger<GatewayToolDispatcher> _log;
public GatewayToolDispatcher(
IToolCatalog catalog,
IAuthorizationService authz,
IBackendClientFactory clients,
ILogger<GatewayToolDispatcher> log)
{
_catalog = catalog;
_authz = authz;
_clients = clients;
_log = log;
}
public async Task<ToolResult> InvokeAsync(
ClaimsPrincipal caller,
string publicToolName,
BinaryData arguments,
string correlationId,
CancellationToken ct)
{
if (!_catalog.TryResolve(publicToolName, out var tool))
return ToolResult.Error("tool_not_found", $"Unknown tool '{publicToolName}'.");
var decision = await _authz.AuthorizeAsync(caller, tool, ct);
if (!decision.Succeeded)
{
_log.LogWarning(
"Denied {Tool} for {Subject} corr={CorrelationId}",
publicToolName, caller.Identity?.Name, correlationId);
return ToolResult.Error("forbidden", "Not allowed to invoke this tool.");
}
var client = _clients.Get(tool.BackendId);
using var scope = _log.BeginScope(new Dictionary<string, object>
{
["tool"] = publicToolName,
["backend"] = tool.BackendId,
["correlationId"] = correlationId
});
// Forward as upstream tools/call with mapped name.
return await client.CallToolAsync(tool.UpstreamToolName, arguments, correlationId, ct);
}
}
A few implementation notes from real systems work:
- Correlation ids belong on every hop. When an agent misbehaves at 2am, you will want one id that ties client request → gateway → backend logs.
- Argument schemas should be validated at the edge. Do not assume backends are hostile-input-proof.
- Timeouts and cancellation matter more than people expect; tool calls are often long and LLM clients retry enthusiastically.
- Idempotency is not free. If a client retries
create_invoice, decide whether the gateway or the backend owns deduplication keys.
Configuration Beats Cleverness
Early on I keep the registry in configuration plus a small database table, not a distributed discovery fantasy.
{
"McpGateway": {
"Backends": [
{
"Id": "billing",
"PublicNamespace": "billing",
"Kind": "StreamableHttp",
"HttpEndpoint": "https://billing-mcp.internal/"
},
{
"Id": "ops-scripts",
"PublicNamespace": "ops",
"Kind": "StdioSupervised",
"Command": "dotnet",
"Args": ["OpsMcp.dll"]
}
],
"DefaultDeny": true
}
}
DefaultDeny is deliberate. New tools stay dark until someone maps roles. That is mildly annoying in week one and deeply comforting in month six.
Security Review Talking Points
If you present this design inside a bank, telco, or large government supplier, expect these questions. Having answers ready is part of the architecture.
| Question | Answer shape I use |
|---|---|
| Where do secrets live? | Backend credentials in Key Vault / managed identity; never in client MCP configs. |
| Can the model see admin tools? | Only if the caller’s token maps to a role that includes them; catalog is filtered per principal. |
| What is logged? | Tool name, subject, latency, success/failure, correlation id — not raw secrets or full PII payloads. |
| Blast radius if the gateway is compromised? | Network isolation to backends, short-lived credentials, per-backend least privilege. |
| How do we revoke access? | IdP group removal + gateway policy cache TTL measured in minutes, not days. |
I also keep a hard list of tools that must never be exposed to a general-purpose coding agent — production data deletion, unrestricted shell, wire-transfer equivalents. Those either stay off the gateway or require a separate break-glass path.
Where Gateways Go Wrong
Patterns I have seen (and occasionally caused):
- God gateway. Business rules, prompt templates, and five product teams’ special cases all land in one repo. Split backends early.
- Transparent proxy with no schema discipline. Tool names thrash every sprint; clients break; people blame “AI.”
- stdio farm without supervision. Orphan processes, leaked file handles, mysterious port conflicts.
- Logging full tool arguments “just for debug.” That is how payroll numbers end up in a log analytics workspace with the wrong retention policy.
- Ignoring multi-tenancy. If two business units share a gateway, tenant isolation is a first-class design input, not a later flag.
How This Fits the Rest of an AI Platform
An MCP gateway is not a model router and not an agent framework. Keep those boundaries clean:
- Model gateway / LLM proxy — tokens, model selection, spend caps.
- Agent runtime — planning, memory, multi-step loops.
- MCP gateway — tools as capabilities with enterprise controls.
When those three collapse into one binary, upgrades get political. When they stay separate, you can swap an agent host without re-litigating tool security.
If your world also includes operational systems — and mine often does, including EV charging platforms speaking OCPP to a CSMS — the same facade thinking applies. Agents should not get a raw socket to production infrastructure. They get mediated tools with audit trails. The technology changes; the integration discipline does not.
Frequently Asked Questions
What is an MCP gateway?
An MCP gateway is a mediating service between AI clients and one or more MCP servers. It aggregates tool catalogs, enforces authentication and authorization, bridges transports such as stdio and Streamable HTTP, and provides a single stable endpoint for clients.
Do I need an MCP gateway for local development?
Usually no. stdio servers wired directly into Claude Desktop or an IDE are fine for personal workflows. Introduce a gateway when multiple users, multiple servers, or shared credentials enter the picture.
Should the gateway use stdio or Streamable HTTP northbound?
For anything shared, prefer Streamable HTTP northbound. Keep stdio for local clients or as a southbound implementation detail behind the gateway.
Is an MCP gateway the same as an API gateway?
Related idea, different protocol concerns. You still want TLS termination, identity, and routing, but you must handle MCP’s JSON-RPC methods, tool catalogs, and sometimes long-lived sessions — not only REST paths.
How do I prevent tool name collisions?
Namespace by backend or domain (billing.refund, ops.restart_worker) and treat public names as a stable contract owned by the gateway team.
Can I put every internal API behind MCP?
You can, but you should not. Expose small, intention-revealing tools with clear side effects. A 200-endpoint OpenAPI surface dumped into tool form overwhelms models and frightens security reviewers.
Closing
The useful question is not “do we need an MCP gateway?” It is “where do we want the control point for agent tools to live?” If the answer is “in twelve desktop config files,” you already know the next outage’s shape.
Start with one HTTP endpoint, one backend, real identity, and default-deny policy. Add namespaces, telemetry, and stdio supervision when the first design proves itself. The protocol will keep evolving; a clear facade still ages better than a pile of special cases.
If you want the foundations under this post, read What Are MCP Servers? and MCP Transport Types Explained next — or again, with the gateway lens in mind.
Leave a Reply