When we first built Obot’s MCP gateway, our goal was straightforward: give users a single, secure entry point to their MCP servers. Over time, though, the gateway began to absorb more and more responsibility. Authentication, authorization, server lifecycle management, webhook execution, audit logging. Every new feature seemed to add another layer of bespoke logic.
This post is a design story about how and why we refactored that gateway. The end result is a simpler core application, a more composable architecture, and a system that leans on existing standards instead of reinventing them. All without breaking existing MCP server deployments.
Try Obot Today
⬇️ Download the Obot open-source gateway on GitHub and begin integrating your systems with a secure, extensible MCP foundation.
The Original Design: An Intercepting MCP Server
Originally, Obot ran its own MCP server. Every incoming MCP request flowed through that server, where Obot would:
Authenticate and authorize the user
For non-remote servers: ensure the target MCP server was deployed (via Kubernetes or Docker)
Construct a client for the “real” MCP server
Invoke any configured webhook filters (on both request and response)
Record audit logs using request/response metadata
Forward the MCP request to the server
This design worked well early on. Centralizing logic inside the gateway made it easy to reason about behavior and ensured consistent enforcement of security and logging.
When the Gateway Became the Bottleneck
As Obot evolved, the gateway increasingly became the place where everything had to be implemented.
A good example was a feature we added called Composite MCP Servers. A Composite MCP Server exposes tools backed by multiple underlying MCP servers. Supporting this feature significantly increased the complexity of the gateway: routing logic, lifecycle coordination, request fan-out, and error handling all ended up layered into the proxy.
Each new feature followed the same pattern. The gateway grew more custom, harder to extend, and harder to reason about. Adding functionality meant touching critical proxy code, increasing both risk and cognitive load.
At the same time, we were solving problems that already had good answers elsewhere, especially around authentication and authorization. That’s when we stepped back and asked a simpler question:
What if the gateway didn’t need to be smart at all?
The Core Shift: Obot as a Reverse-Proxy Passthrough
The refactor started with a clear boundary:
Obot should not be an MCP server
Obot should not intercept or reinterpret MCP requests
Obot should ensure servers exist, then get out of the way
In the new architecture, Obot acts as a reverse-proxy passthrough. It ensures that the “real” MCP server is deployed, then proxies requests directly to it without modifying the MCP protocol itself.
All of the behavior that previously lived in the gateway moved elsewhere.
That “elsewhere” is the shim.
The Shim Container and Nanobot
Every MCP server now runs alongside a shim container. This includes MCP servers deployed by Obot (via Kubernetes or Docker) and remote MCP servers that live outside Obot entirely.
The shim is protocol-aware. It understands MCP and is responsible for:
Authenticating requests
Performing authorization checks
Calling webhook filters
Recording audit logs
Handling token exchange for OAuth-backed servers
Under the hood, the shim is built on another tool of ours called Nanobot, which has become a key architectural component going forward.
Crucially, secrets live in the shim. Client credentials, token exchange secrets, and audit-log tokens are never exposed to the MCP server itself. Similarly, the secrets and configuration needed to run the MCP are never exposed to the shim.
This shift had an immediate payoff. Features like composite MCP servers, which previously added complexity to the gateway, are now handled naturally by the shim and Nanobot without inflating the core proxy logic.
There is a tradeoff here: deployment complexity. Every MCP server now involves more containers. But that complexity is isolated and repeatable, while the gateway itself becomes dramatically simpler. And we now have a path to reduce a lot of this overhead in future releases.
Webhooks as MCP Servers
In the original design, webhook filters were traditional HTTP webhooks: a URL and an optional signing secret. The gateway would call them directly during request and response processing. We kept the interface the same, but changed the execution model.
Existing webhooks are converted to an MCP server using a converter that we built. This MCP server takes the URL and optional signing secret as configuration and runs next to the shim in a separate container (to avoid exposing secrets). When the shim needs to invoke a webhook, it does so as an MCP request to the converter server. The converter then signs the payload and makes the outbound HTTP request.
This is an extra hop, but it gives us something much more valuable: a uniform model. Users will be able to take advantage of this new design in a future release by building their webhook filters as MCP servers, something they are already familiar with developing. Obot will be able to manage these MCP servers just as it does any other, and users won’t need external webhook infrastructure.
This was a deliberate, future-facing decision. Many users have told us that a major value of Obot is not having to think about MCP server deployment at all. We wanted the same experience for webhook filters.
OAuth and Token Exchange
Supporting OAuth-protected MCP servers exposed another weakness in the old design.
Previously, Obot would authenticate the user, obtain a third-party access token if required, and then replace the bearer token when forwarding the request to the MCP server. This worked, but it was bespoke and tightly coupled to specific expectations.
In the new architecture, Obot still handles user authentication and OAuth setup. But when proxying a request, it forwards the original bearer token unchanged.
The shim then performs a token exchange using the OAuth 2.0 Token Exchange standard (RFC 8693).
At a high level, the flow looks like this:
This has two major benefits:
Standards compliance Token exchange is a well-defined OAuth extension. Using it builds trust and familiarity, especially for MCP authors already operating in OAuth ecosystems.
Flexibility Token exchange isn’t limited to OAuth access tokens. It gives us a standardized way to provide other credentials to MCP servers when needed without changing the gateway.
In Kubernetes, all containers (MCP server, shim, converter servers) run in a single pod and communicate over localhost.
In Docker-based setups, containers communicate via host.docker.internal or a local IP.
Remote MCP servers now also get a shim container, even though the MCP server itself is external.
Importantly, this refactor is not a breaking change. Existing MCP server deployments migrate automatically, and existing webhook configurations are transparently converted to use the new MCP-based model.
From a user’s perspective, things continue to work but on a much sturdier foundation.
A Simpler Gateway, A Stronger Foundation
By pushing behavior out of the gateway and closer to MCP servers themselves, we’ve ended up with a system that is:
Easier to extend
More standards-aligned
More secure by default
Simpler at its core
The gateway is now a thin, reliable conduit. The real power lives in composable infrastructure that we can evolve without turning the proxy into a monolith.
This refactor gives us confidence that Obot can grow alongside the MCP ecosystem, without repeating the same complexity traps we started with.
Thanks for being part of the Obot community! We look forward to your feedback and can’t wait to see what you build with these new features.