Intro0:00
Hey folks, thank you for joining us for this session on why your insecure MCP server won't survive production. My name is Tun Shwe, and I lead AI at Lenses, and day to day I'm an AI Engineer and you can connect with me here on LinkedIn.
And I'm Jeremy Frenay, I work on AI engineering at Lenses.
First, a quick note on where we work: Lenses is a data operating fabric that sits between your agents and any combination of Apache Kafka. Lenses is the de facto streaming data layer for providing trusted real-time context to agentic AI.
Companies work with us because we have governance, security, and large scale at the top of mind.
Here are a selection of our customers, which gives us exposure to lots of different industry use cases at large scale.
And we are here today, of course, because we have an open source MCP server that we're applying our learnings to from the field, so please give us a star to follow the project. And here's what we'll cover in this session.
The takeaways we want you to have are the ways of thinking about designing MCP servers, and in fact, any interface to make it robust for agentic AI systems. And since this is a talk about MCP, we'll ensure you have tips on how to approach securing your MCP servers for production.
I'll cover the first few sections, and in his sections, Jeremy will go over the OAuth flows.
So let's go straight into why most MCP servers aren't great.
Security Shadows1:34
I like the way Jeremy Lowen, who is the creator of Fast MCP, put it. He said that agents deserve their own interface that is optimized for their use cases. To approach designing for agents through a product engineering lens, I want to take that approach one step further.
A badly designed MCP server is also a badly secured one. Poor design and poor security compound each other. Jeremy put forward three dimensions in how humans and agents differ from one another, and to consider these three dimensions when you're designing for MCP or any agentic interface.
The extra layer I wanted to emphasize is security, that each one casts a security shadow. First, there's discovery. When you use the new API, you pull up the docs, you scan through them once, you find the three endpoints that you need, and you never look at those docs again.
An agent can't do that. Every time it connects to an MCP server, it enumerates every single tool and reads every single description, and that's expensive in tokens. But here's the security shadow. Every one of those tool descriptions is a surface for tool poisoning.
Attackers can embed hidden instructions inside descriptions that are invisible in the UI, but the model will follow them without question. More tools means more surface area for injection. Second, is iteration. If your script fails, you run it again.
It takes a second. When an agent retries, it sends the full conversation history over the wire. And here's its security shadow. An agent iterating over a poorly scoped MCP server is broadcasting your data with every retry. The full conversation history goes over the wire, including any sensitive data returned by previous tool calls.
Each round trip is a chance for data leakage.
Third, context. You and I have decades of memories and experiences and intuition. An agent has roughly 200,000 tokens, and that's it. The security shadow is detailed in OWASP's MCP Top 10 list, which I recommend you all to go and read, and there it's listed as number 10: context injection and oversharing.
If your server dumps unfiltered data into that limited window, you're handing off PII, credentials, internal system details to a model that can be tricked into exfiltrating them. An agent has to load all the context in before it can make a decision.
It makes it suitable for finding specific things, but it comes at the cost of latency and context bloat. So you think of it as finding a needle in a haystack. If some of that hay is poisoned, the agent just won't notice.
So you should think about curation. Curate the MCP tools available to the agent, and aim to expose the smallest amount of information. The less you expose, the less can be attacked. And here, less is more. Next, I'll go over what I consider five key rules for secure agentic design, to think with your product engineering hat on, and to apply it to MCP servers.
The thing I want you to take away from this section is that good MCP design and good MCP security are the same discipline. If you get the design wrong, no amount of OAuth will save you. I've got five principles here, and they all give you protection against the OWASP MCP Top 10 before you write even a single line of auth code.
Five Principles4:55
So number one, shrink the attack surface by design. Think in terms of outcomes. The idea here is to squash all the fine-grained operations or underlying API calls into a single coarse-grained operation that produces a desired outcome. Every tool you expose is a door.
Don't give the agent access to delete users when all it needs is to check an order. Consolidate related operations behind a single tool call with a well-defined outcome. So you have one permission check, one audit log entry, one place to enforce authorization.
So think fewer doors with fewer locks to manage. Number two, constrain your inputs at the schema level. You've got to accept the top-level primitives like the enums; that will be the best approach. Dictionaries are also fine, as long as they're not nested.
And to introduce more strictness, you could use a typing library like Pydantic. The aim is to reject free-form nested payloads to avoid command injection flaws, where the root cause is almost always unconstrained string arguments that get passed downstream to a shell, a query engine, or an API.
Constrained inputs are easier to validate and harder to exploit. Number three, treat your documentation as a defensive layer. Tool poisoning is number three on the OWASP MCP guide, and it works by embedding malicious instructions in tool descriptions that are invisible in the UI but executed by the model.
If you don't write clear, complete instructions, an attacker-controlled tool description in a neighboring MCP server can shadow yours. If your documentation is complete and unambiguous for every tool, it crowds out the space that a poisoned neighboring server would try to fill.
Number four, return only what the agent needs. Oversharing data in tool responses is number ten in OWASP's MCP guide, and it turns the agent's context window into a liability. PII, internal identifiers, credentials, system details, all sitting in the context, they're all just one prompt injection away from exfiltration.
So strip your payloads to the minimum. If the agent doesn't need a piece of data for its immediate task, then don't return it.
And number five, minimize the blast radius. Scope permissions at the tool and resource level, not the session level. Use the MCP read-only annotation for non-destructive tools so that clients can enforce boundaries, or if an MCP tool is intended to have read-only access, then consider turning it into an MCP resource.
Also remember that every tool you remove is an attack vector that you eliminate.
And you're building an interface, not a tool, so this is the mindset to go in with. An agent will use anything you provide it with confidence, so you have to provide that trust layer.
So now you've designed your server well, you've followed the five principles, now you need to actually deploy it, and this is where most teams hit what I call the security cliff. If you're running MCP in standard IO mode, life is pretty comfortable.
Security Cliff8:12
It's a local process, a single user, no network exposure, no authentication needed. Your MCP host talks directly to the server process on your machine. It's a walled garden, and it works beautifully for single-player developer productivity. But production requires something completely different.
You need the streamable HTTP transport. This enables remote deployment, multiple clients connecting to the same server. You can horizontally scale, and you can centralize your governance. And this is really where MCP becomes genuinely valuable to an organization, where you go from one developer on one laptop to a shared capability that an entire team or entire fleet of agents can use.
MCP becomes the single interface that all clients can use without having to worry about whether they're the latest version of an API or considering the resources needed to scale. The problem is there's no gradual on-ramp. You go from zero security surface to a huge list of concerns all at once.
You're suddenly needing OAuth, token management, cause configuration, TLS, rate limiting, and more. And you need it all at once. So there's no halfway house because you can't do a little bit of production. You're either behind the wall or you're standing out in the open.
And you can't just stay local and hope for the best. Stacklog ran load tests on standard IO transport, and the results were brutal. 20 out of 22 requests failed with just 20 simultaneous connections. Standard IO falls over the moment you add concurrency.
So if you want to scale out, you have to cross the chasm. And how do you start crossing that chasm? I'm going to hand it over to Jeremy to continue.
OAuth Challenges10:10
Yes, so implementing an OAuth authorization server for MCP isn't that simple. Let's look at the list of RFCs to implement.
With the core OAuth flow, the OAuth client discovery and metadata, and the management of the token lifecycle, we already have more than 10 specifications to implement. Now, let's say we read all these RFCs and I'm ready to implement an authorization server for MCP.
What does the enterprise-grade authorization look like?
So let's start by reviewing the local versus remote MCP server setups and their respective OAuth flows. Tun talked about the walled garden, the local MCP server running over standard IO with an API key. Let's look at the flow diagram.
The MCP server runs on my machine. The client connects via standard IO. The user must set the key as a parameter in the MCP client config, and the parameter will be stored as an environment variable passed by the MCP server with its request to the external service.
That might be good for local setups, but I need to provision, store, and maintain the key. This key is long-lived, it's rarely rotated, and it isn't scoped to the specific actions that my client performs. Even worse, these keys are often shared across systems.
So the key is stored in a config file, an environment variable, and it isn't verified by the MCP server. Now, let's look at a remote MCP server.
In this case, the MCP server runs on a remote server. The client connects via HTTP. The user must set the key in the HTTP authorization header. Again, we can see the MCP client config here on screen. So phase one is the generation of the token and the configuration of a client.
On step two, runtime, we can see the client performing a request, attaching this API key in the authorization header. This API key is validated or not by the MCP server itself and will be passed through to the upstream API, where it will be this time verified.
Whether the API key is validated or not, we'll get a 200 response or a 401 response, in which case the user will need to rotate the token manually. That's how the majority of remote MCP servers are configured today.
The key is long-lived. It isn't scoped to the specific action of my agent either. The key is stored in a config file, and it isn't always verified by the MCP server. Either the key is simply passed through to the API, creating a confused DPT vulnerability where malicious clients obtain authorization without the proper user consent, or sometimes the key might be mapped to another key and token for the API access itself.
Now we have single-shell credentials serving many users. That credential is even more powerful, harder to revoke per user, and if leaked, it compromises everyone. This approach works for long-lived, unscoped credential setups, and it still represents more than 50% of the MCP servers out there.
What we see the ecosystem moving towards is short-lived, scoped tokens via OAuth 2.1. We even see token exchange for least privilege access. Traditional OAuth assumes you know your clients upfront. You register them in a developer portal, you get a client ID, and you move on.
OAuth 2.113:41
This works when you have 5 to 10 apps connecting to your service. But with MCP, this flow breaks completely. Think about what MCP's architecture actually looks like. Any client—Claude Desktop, Cursor, VS Code, a CLI tool, a random agent—can discover and connect to any MCP server at runtime.
Pre-registration requires too much effort in a highly variable setting. It's an unbounded number of clients connected to an unbounded number of servers. You can't ask every developer to manually register their app with every MCP server they might ever want to talk to.
DCR14:43
So that's where the dynamic client registration comes in. In this case, we still have an MCP server running on a remote server, but now it's protected by an OAuth authorization server. The client can self-register itself against the authorization server and will get a new client ID on every registration.
So on phase one, the discovery, our MCP client, in this case Cursor, will perform a request on /mcp against the MCP server. We can see the MCP server returning a 401 response because we do not have a token to pass yet, but it also passes a www authenticate header containing the resource metadata that can be used by our client in order to discover the MCP server and its metadata.
The document itself looks a bit like this. It describes the resource we're trying to access and the authorization server protecting it. This lets our client point at the authorization server itself and discover, this time, the metadata exposed by the authorization server itself.
Now that our client knows how to authorize itself for an MCP server access, it needs to register itself against the authorization server. That is done via a request, a POST request on /register.
As we mentioned earlier, the authorization server will generate and persist on disk a new client ID and return it to the client. Now we know who we're talking to. Next, it's time to authorize our client against the authorization server.
And for that, the MCP spec is mandating to use the Pixie, the proof key for code exchange protocol. So our MCP client is first generating a code verifier and a code challenge, but it does pass through a request to /authorize in order to obtain an authorization code.
Our authorization server will validate this request and the code challenge, and since we don't have a running session yet for this user, it will redirect the user to its identity provider. So that's your single sign-on form in order for the user to log in.
Upon successful login, the user will be redirected to a consent page where they can grant different scopes to their client. Now that we issued a valid authorization code for our client, it's time to use it in order to get a token, an access token.
It does that by sending a request on /token and passing the authorization code and the code verifier that we generated for the Pixie protocol earlier. Our authorization server will validate the Pixie challenge and the authorization code, and it will then emit a brand new token.
In this case, we are using JSON Web Tokens in order to return an access token for our client to now use the MCP server. So the final step is actually to use the MCP server. This is when your MCP client is going to perform a tool call, for example.
We can see it will pass the access token we just issued in the authorization as the bearer value. Our MCP server will validate this token, check the valid scopes, and it will now perform the token exchange flow in order to change this delegation token for a session token.
This means our MCP server now is actually an OAuth client for a new resource server or API, but it's using the exact same authorization server in order to get a token. So that is the token exchange flow that is defined in RFC 8693.
And as we complete the flow, our MCP server can use this new session token in order to perform an API call by passing the token in the authorization header. So DCR solves the self-registration, the dynamic registration of the client, so that our user doesn't have to go and pre-register, pre-generate static credentials, and set it on their client.
But it does have its own problems. First, every time a user connects a client to an MCP server, a new registration is created. Registrations are not portable, so using Claude on Windows and then on macOS creates two distinct client registrations.
DCR is vulnerable to phishing attacks because it doesn't provide a reliable way to verify client identities. Anyone can post to that endpoint, the /register endpoint, including attackers. Finally, the server is just trusting whatever metadata the client self-asserts. It means a malicious client can claim to be Claude, and the server has no way to know otherwise.
CIMD19:54
So the MCP community had to come up with a better way to let clients self-register. And that is CIMD, the Client ID Metadata Document. Here in this case, we still have an OAuth authorization server in front of our MCP server, but the client owner exposes the client ID on a public URL.
This will let our MCP server fetch the client ID during the authorization. Let's have a look at the diagram. So phase one is still the discovery. Our client is the MCP server without a token, gets a 401 response and the resource metadata URL.
It can follow this URL, discover the MCP server, and it will get to discover the authorization server. But this time, the authorization server isn't mentioning it needs a /register request. It means the client, the MCP client, can go straight to the authorization phase.
We generate again the Pixie code verifier, and we perform a /authorize request. But this time, our client passes its unique ID, and we can see it here. It's actually a valid URL where the metadata for the client is being exposed.
This lets our authorization server fetch this metadata and register a new client with a unique ID. That is the URL that is exposed by the client owner,
and we can move to the authentication phase.
Again, the authorization server will redirect to the identity provider, wait for a valid login on our user's side, present a consent screen for the user to grant some scopes, and we are ready to issue the delegation token and the session token for a token used by the MCP server.
So here, CIMD has no growing database of client registration to maintain. Proving that you control HTTPS Claude.ai is meaningful, unlike proving that you can post on a registration endpoint. The redirect URLs that are explicitly bound to the client in its metadata document are making it harder for attackers to sneak in malicious callbacks.
And the authorization server can selectively allow or deny clients.
So in summary, DCR is a good start, but it does create problems. CIMD is a leap forward, and it is the preferred approach since November 2025.
Enterprise Layers22:40
But becoming enterprise-grade requires adding other layers of security and confidence. For permissions, OAuth scopes get you part of the way there, but it's scoped to the session. True enterprise-grade role-based access control means scoping permissions at the individual tool and resource level, not just the session.
Data masking is how you deal with the PII fields such as email, phone, and national insurance numbers. They may need to be masked before the agent sees them because agents should never be exposed to data that they have no business handling.
You will need to log what's happening in each interaction: which agent called which tool, with what parameters, and what data was returned. For compliance with regulations such as the EU AI Act, regulators will expect this level of transparency and detail for autonomous AI systems.
Finally, you'll need to be able to observe the full request. This means the client request, validation, tool execution, data retrieval, and the generated response. If you cannot trace what an agent did end-to-end, you cannot govern it. Tracing for agentic AI follows the same principles as distributed system observability, but applied to autonomous decision-making.
Thanks very much, Jeremy. And thank you all for tuning in to this session. We'd love to know how your journey with productionizing MCP service is going, so please leave us a comment or send us a message. You know where to find us.
Outro24:12
And please do check out our MCP server and give us a star. So hopefully, we'll see you again soon. Thanks, and bye.
Thank you.





