MCP Servers in the Enterprise: Identity, Policy and Blast Radius

A model summarising an incident for us once pulled the contents of a Kubernetes Secret into a transcript.

Nothing was compromised. No attacker involved. The MCP server had been given a read-only role, somebody had written that role the way you write roles for humans — broad read across the API, because reading is harmless — and secrets was in the resource list. The model did what it was asked: gather context about a failing workload. A Secret is context.

That transcript then went where transcripts go, which is a datastore nobody had classified as credential-bearing.

Wiring up an MCP server is an afternoon. Making one defensible took us months. The distance between those two numbers is the whole enterprise story, and most of what is written about MCP is about the afternoon.

“Read-only” is not a security property

The first thing to discard is the intuition that read access is the safe class of access.

Read access to Secrets is write access to production, one indirection away. Read access to a configuration store tells an attacker your topology. Read access to incident history tells them what you are bad at. And in an agent system, every read is a write into a context window that is often persisted, frequently replicated, and almost never classified at the level of the most sensitive thing it has ever held.

The RBAC most teams hand an MCP server is RBAC designed for a human with eyes and attention. An agent has neither. It has recall, throughput, and no concept of “I probably should not have looked at that.”

What we ended up with is narrower than the role any of our engineers hold: an explicit resource allow-list rather than wildcards, no secrets at all, and a redaction pass on tool output that runs before anything reaches the model. The redaction pass catches what the role misses, and it has caught things.

I would also, from day one, have treated the transcript store as a credential-adjacent system with the retention and access controls that implies. We retrofitted that. Retrofitting a data classification is expensive and it is never fully complete.

There are four identities in an MCP call and the spec addresses about one and a half

Walk a single tool invocation and count the principals.

The human. Someone asked a question.

The agent. A model instance acting with some delegated authority, possibly several steps removed from the human’s original request.

The MCP server. A workload with a network identity and its own credentials.

The downstream system. Whatever the tool actually calls, which has its own authorisation model and its own idea of who the caller is.

MCP’s authorisation work has made real progress on the client-to-server leg. OAuth 2.1 patterns, protected resource metadata, resource indicators per RFC 8707 — this is the right direction and it is a genuine improvement over where the ecosystem started.

It authenticates the client to the server. That is one leg of four.

What is missing is a standard, verifiable way to carry the human subject and the delegation chain through to the downstream system, in a form the downstream can enforce on. The confused deputy problem is not a theoretical concern here; it is the default architecture. The server holds credentials broader than any individual caller’s entitlements, and the only thing standing between a caller and the full reach of those credentials is logic the server author wrote by hand.

We solve this today with token exchange — RFC 8693, the server exchanging its own credential plus the caller’s assertion for a narrowly scoped downstream token. For workload identity of the server itself, SPIFFE and the WIMSE work give us short-lived, attested identity rather than a secret in a config. The IETF AIMS effort is pointed at the agent-identity half, which is the piece nobody has solved.

None of that is in the protocol. All of it is bespoke. Every organisation doing this seriously is building the same three components privately, which is a reliable signal that something belongs in a spec.

The static credential we had already removed, reintroduced

One of our tools needed to call an older internal API that only supports static API keys. No OIDC, no mTLS, no roadmap.

So the MCP server held a long-lived key with broad reach. We had spent two years removing exactly that shape of credential — Vault-issued short-lived certificates, mutual TLS between workloads, no static secrets in deployment config — and an agent tool put one back, in a single place, reachable by a model.

The fix was a broker. The tool calls the broker, the broker holds the key, and the broker enforces per-subject scope and rate limits before it touches the legacy API. The tool never sees the credential.

The general rule I now apply: if a tool needs a credential the rest of your platform is not allowed to hold, the tool does not get the credential. Something else holds it and answers narrower questions.

The tool schema describes inputs and says nothing about effects

This is the omission that costs the most, and I want to be direct about it.

An MCP tool definition tells the model what arguments to pass. It does not tell anyone whether invoking the tool reads state, mutates state, costs money, pages a human, or does something that cannot be undone. There is no field for that.

Which means the model cannot reason about consequence, the client cannot ask for confirmation proportionate to risk, and the policy engine has nothing to key on except the tool’s name. A tool called restart_workload and a tool called get_workload_status are, to the protocol, the same kind of thing.

Every enterprise deploying MCP is inventing this field. We did:

{
"name": "scale_deployment",
"effect_class": "mutating",
"reversible": true,
"scope_required": ["namespace"],
"max_blast_radius": { "replicas_delta": 4 },
"approval": "none",
"audit": "required"
}

effect_class is one of read, mutating or irreversible. Nothing registers without it. irreversible requires a second human approval regardless of who is asking, and there is no configuration option to turn that off, because the first thing anyone asks for in an incident is to turn it off.

That declaration is not optional metadata in our setup. It is the registration gate. A server that does not supply it does not get a route.

Put the policy decision outside the server

The pattern I see most often, and would argue against hardest, is the allowlist inside the server.

It does not survive. It lives in the code of whichever team wrote that server, it diverges from every other server’s version, it cannot be audited centrally, and when you need to answer “which tools could this user invoke last Tuesday” you have to read source control history.

Treat the MCP server as a policy enforcement point and nothing more. It asks an external decision point, in band, on every call:

package mcp.authz
default allow := false
allow if {
input.effect_class == "read"
input.subject.groups[_] == "platform-engineering"
not sensitive_resource(input.arguments)
}
allow if {
input.effect_class == "mutating"
input.subject.groups[_] == "platform-engineering"
input.arguments.namespace in data.subject_scopes[input.subject.id].namespaces
input.agent.attested == true
}
# irreversible effects never pass on policy alone

Two things in there matter more than the syntax. input.agent.attested means the agent’s identity was verified, not asserted — the difference between a workload presenting a short-lived attested credential and a header saying who it claims to be. And the decision is keyed on effect_class, which only works if the declaration above is mandatory.

The output of this call is a decision with an identifier. That identifier goes in the audit event. Which brings me to the part everyone defers.

“We have logs” is not an audit trail

We could show tool invocations in server logs. We could show prompts in application logs. We had no shared identifier between them.

So when we wanted to reconstruct a session — which tools ran, on whose behalf, in what order, against which policy decisions — we joined on timestamps. Across systems with different clocks and different buffering behaviour.

That is not evidence. Under Regulation SCI, on a platform settling on the order of $4.5 trillion a day, “we correlated the logs by hand afterwards and we are fairly confident” is not an answer you want to give. The standard is a record that was captured at the time, not assembled later from fragments.

The event shape we settled on:

{
"session_id": "s-01JA9...",
"request_id": "r-01JA9...",
"subject": { "id": "user:redacted", "auth_method": "oidc", "mfa": true },
"agent": { "id": "spiffe://internal/agents/ops-assistant", "attested": true },
"delegation": { "granted_at": "2026-09-11T14:02:08Z", "scopes": ["read:workloads"] },
"server": { "id": "spiffe://internal/mcp/cluster-inspector" },
"tool": { "name": "get_workload_status", "effect_class": "read" },
"arguments_hash": "sha256:7c1e...",
"policy": { "decision": "allow", "decision_id": "d-01JA9...", "bundle": "mcp-authz@2026.09.3" },
"result": { "status": "ok", "redactions_applied": 2 }
}

arguments_hash rather than arguments, because arguments contain the things you do not want in a log. bundle so you can prove which version of policy made the decision. delegation so the chain from human to agent is in the record rather than inferred.

MCP has no canonical audit event. Everyone is inventing one. This is a worse outcome than a mediocre standard event, because it means no tooling, no shared detections and no portable evidence.

We shipped read-and-recommend agents fast and mutating agents slowly, deliberately

Our generative-AI operations assistant runs on Amazon Bedrock with retrieval-augmented generation and evaluation sets behind it, and it cut routine operational queries by about 50%. Agents doing anomaly detection, monitoring and incident response cut mean time to repair by roughly 60%.

Both of those numbers came from systems that read and recommend. A human still acts.

We have moved much more slowly on agents that mutate state, and that was a choice rather than a limitation. The risk profile is not adjacent, it is different in kind. A read agent that is wrong wastes an engineer’s time. A mutating agent that is wrong at 03:00, on a platform where an outage is a market event, has a blast radius measured in things you cannot take back.

The 60% figure is the one people quote at me, and I think it is the less interesting number. The interesting one is that we got it without granting write access to anything.

If you are being pushed to give agents mutating authority to show progress, that is the counter-argument. Most of the value in operational AI so far is in retrieval and correlation, which are read problems.

Tool descriptions are prompts, and the server writes them

A tool description is text supplied by the server that goes directly into the model’s context and influences its behaviour. That is a prompt. Supplied by a third party. Mutable at any time, with no attestation and no version pinning in the protocol.

A compromised or hostile server can rewrite a description to redirect behaviour — and the model has no way to distinguish an instruction that came from its operator from one that came from a tool catalogue.

Tool results are the same problem in the other direction. Output from a tool is untrusted content, and treating it as trusted input to the next reasoning step is how you get an agent following instructions that were planted in a log line.

We already solved a version of this problem for container images: sign the artifact, constrain the signer identity, verify at admission, alert on drift. The reasoning transfers directly. Sign the tool manifest, verify it at registration, pin the version, and treat a changed description as a change requiring review rather than a silent update.

That is not in the spec either, and of everything on my list it is the one I would want first.

The spec needs three fields more than it needs more features

If I could put three things into the protocol and nothing else, it would be these.

An effect declaration on every tool. Read, mutating, irreversible, with reversibility and scope requirements. Without it, no client can ask for proportionate confirmation and no policy engine can decide anything meaningful.

A delegation claim. A verifiable statement of which human authorised which agent for which scope, carried through the call and enforceable by the downstream system. Not a header. A signed assertion.

A canonical audit event. One shape, with subject, agent, server, tool, effect class, policy decision and correlation identifiers. Mediocre and universal beats excellent and private.

None of those are features. They are the fields that make governance possible at all, and until they exist every serious deployment will keep rebuilding the same three components privately and incompatibly.

The thing I would tell anyone starting this work: the hard part was never wiring the tool up. It was governed access, policy that lives outside the server, and an audit trail that holds up when someone asks you to reconstruct a decision six months later. Build the registration gate before the first server reaches production. We built it third, and paid for the ordering.

Published by Ramasankar

As a Principal Cloud Architect with over 18 years of experience, I am dedicated to revolutionizing IT landscapes through cutting-edge cloud solutions. My expertise spans Cloud Architecture, Security Architecture, Solution Design, Cloud Migration, Database Transformation, Development, and Big Data Analytics.Currently, I spearhead cloud initiatives with a focus on Infrastructure, Containerization, Security, Big Data, Machine Learning, and Artificial Intelligence. I collaborate closely with development teams to architect, build, and manage robust cloud ecosystems that drive business growth and technological advancement.Core Competencies: • Cloud Platforms: AWS, Google Cloud Platform, Microsoft Azure • Technologies: Kubernetes, Serverless Computing, Microservices • Databases: MS SQL Server, PostgreSQL, Oracle, MongoDB, Amazon Redshift, DynamoDB, Aurora • Industries: Finance, Retail, Manufacturing. Throughout my career, I’ve had the privilege of working with industry leaders such as OCC, Gate Gourmet, Walgreens, and Johnson Controls, gaining invaluable insights across diverse sectors.As a lifelong learner and knowledge sharer, I take pride in being the first in my organization to complete all major AWS certifications. I am passionate about mentoring and guiding fellow professionals in their cloud journey, fostering a culture of continuous learning and innovation.Let’s connect and explore how we can leverage cloud technologies to transform your business: • LinkedIn: https://www.linkedin.com/in/ramasankar-molleti-23b13218/ • Book a mentorship session: [1:1] Together, let’s architect the future of cloud computing and drive technological excellence. Disclaimer The views expressed on this website/blog are mine alone and do not reflect the views of my company. All postings on this blog are provided “AS IS” with no warranties, and confers no rights. The owner of https://ramasankarmolleti.com will not be liable for any errors or omissions in this information nor for the availability of this information. The owner will not be liable for any losses, injuries, or damages from the display or use of this information.

Leave a Reply