The Mistake I Keep Seeing With MCP
Model Context Protocol is often introduced as a cleaner way to connect AI agents to tools.
That is true, but incomplete.
MCP makes tool access easier to standardize. It does not automatically solve trust, identity, replay protection, authorization, auditability, or credential ownership.
The dangerous shortcut is to treat an MCP server as a thin API wrapper:
The model wants to do something, so the MCP server calls the backend.
That sounds harmless until the tool can create projects, push commits, read private data, trigger payments, update customer records, or act on behalf of a real user.
At that point the question is no longer:
Can the agent call the tool?
The real question is:
Which actor is making this request, what authority does it have, and can the backend prove that before doing anything irreversible?
That is the boundary most early MCP integrations under-design.
In 30 seconds
- MCP is a protocol boundary, not automatically a security boundary.
- An agent calling a backend is not the same actor as the human using the UI.
- Production MCP routes need their own authentication, authorization, replay protection, and audit model.
Key takeaways
- Do not reuse a browser session token as the default credential for agent-originated work.
- Separate human auth from machine-to-machine agent auth.
- Let trusted backend code resolve ownership, credentials, and permissions. Do not outsource that authority to the model.
MCP Changes the Shape of Tool Access
Before MCP, most agent tools were built as local functions, framework-specific plugins, or one-off HTTP adapters.
MCP gives us a more consistent model:
- clients discover tools;
- servers expose capabilities;
- messages follow a protocol;
- integrations become easier to share across agents and environments.
That standardization is useful. It reduces integration friction.
But lower friction cuts both ways. Once tool access becomes easy to add, dangerous tool access becomes easy to add too.
If an MCP server can call your production backend, it has moved from "developer convenience" into "distributed system component." It now needs the same engineering discipline as any other service that can mutate production state.
The protocol gives you a way to connect. Your system still has to decide what to trust.
The Actor Confusion Problem
Most applications already have human authentication.
The user signs in through a browser. The backend issues a session or JWT. The UI sends that token with requests. The backend maps the token to a human account.
That works for UI flows because the browser request represents a human session.
MCP-originated work is different.
An agent runtime or MCP server may be acting:
- for a human;
- for a workspace;
- for a project;
- for a specific agent identity;
- for a scheduled automation;
- for a delegated task that outlives the browser session.
Those are not all the same actor.
If the backend simply accepts the human JWT from an MCP route, the system blurs two separate questions:
- Is this human logged in?
- Is this agent allowed to perform this action now?
The first question belongs to browser auth.
The second question belongs to agent auth and authorization.
Mixing them makes audits confusing, revocation harder, and blast radius larger than it needs to be.
A Better Boundary
For production systems, I prefer to model MCP calls as machine-to-machine requests.
The backend should be able to verify:
- which agent is calling;
- that the request was signed by that agent or its runtime;
- that the request is fresh;
- that the request has not been replayed;
- that the agent is authorized for the target resource;
- that required downstream credentials exist and belong to the right owner.
That is a different trust model from "user is logged in."
The human still matters. The human may create the agent, connect GitHub, approve a workspace, or configure available tools. But once an MCP server calls the backend, the backend should treat that request as agent-originated and verify it through an agent-specific path.
A useful split is:
- human auth proves a browser user is present;
- agent auth proves a machine actor made a specific request;
- authorization decides whether that actor can touch the requested object;
- credential policy decides which external tokens may be used;
- audit records who did what, through which actor, and why.
That separation prevents a common failure mode: a valid login token accidentally becoming a universal tool credential.
What the Request Should Prove
For HTTP-based MCP integrations, the minimum useful request contract usually needs more than a bearer token.
One practical pattern is a signed request:
X-Agent-Address: 0x...
X-Agent-Signature: 0x...
X-Agent-Timestamp: 1779926400
X-Agent-Nonce: 2f1d7c1a-9328-4f22-b9d4-21fd21f97a1fThe backend rebuilds a canonical message and verifies the signature.
For example:
{METHOD}
{PATH}
{BODY_SHA256_HEX}
{TIMESTAMP_UNIX_SECONDS}
{NONCE_UUID_V4}
{AGENT_ID_OR_ADDRESS}The exact format matters more than the specific fields. Client and server must agree on one canonical representation and never improvise.
The signed payload should bind together:
- the HTTP method;
- the route path;
- the exact request body hash;
- a timestamp;
- a one-time nonce;
- the agent identity.
That prevents the request from being copied to a different route, replayed later, or reassigned to a different agent by swapping a header.
This is the same kind of boring security detail that feels excessive until the first automated agent starts retrying failed writes or a leaked request body gets replayed against a production endpoint.
Replay Protection Is Not Optional
A valid signature only proves that someone with the signing key created the message.
It does not prove the message is new.
Without replay protection, an attacker or faulty intermediary can resend a previously valid request. If the route is mutating, that may create duplicate projects, duplicate commits, duplicate bookings, duplicate orders, or duplicate external side effects.
A safer baseline:
- require a fresh timestamp;
- require a single-use nonce;
- verify the signature first;
- insert the nonce with a uniqueness constraint;
- reject duplicate nonce inserts;
- expire old nonce records after a short window.
The important implementation detail is the insert-first uniqueness check.
Do not do:
SELECT nonce
if missing:
INSERT nonceThat creates a race.
Do:
INSERT nonce
if unique constraint fails:
reject replayThis turns replay protection into a database guarantee instead of a timing assumption.
Authentication Is Still Not Authorization
Once the backend proves which agent sent the request, the next mistake is to stop there.
Authentication answers:
Is this really agent A?
Authorization answers:
Is agent A allowed to perform this action on this object?
For an MCP route that operates on projects, the backend should still check:
- does the agent exist?
- is the agent active?
- does the project belong to that agent or its owner?
- is the requested operation allowed for this agent?
- are required external credentials connected?
- are those credentials scoped to the correct human, tenant, organization, or workspace?
This check belongs in deterministic backend code, not in the model prompt.
A prompt can say:
Only modify projects you own.
That is a useful instruction.
It is not a boundary.
The boundary is the backend refusing a request when the resource does not belong to the authenticated agent.
Credential Ownership Needs a Decision
MCP integrations often run into a subtle credentials question:
Whose GitHub token, Slack token, cloud token, or database credential is the agent using?
There are several valid models:
- the human owns the external OAuth token and delegates limited actions to the agent;
- the workspace owns a service account;
- the agent has its own credential reference;
- the backend performs external actions directly and never returns raw tokens to the MCP server.
The worst model is the implicit one.
If nobody decides, systems drift toward whatever is easiest: pass the human token around, return it to the MCP process, store it in an agent record, or let the model decide which credential to request.
That is how convenience becomes credential sprawl.
For early systems, the cleanest path is often:
- keep OAuth connection human-owned;
- let the backend retrieve the token only after agent authorization succeeds;
- have the backend call the external API directly;
- avoid returning raw credentials to the MCP caller.
If you later need agent-owned credentials, treat that as a deliberate architecture migration. Do not smuggle it in through a nullable token_ref field and hope the security model catches up.
Tool Contracts Should Reduce Authority, Not Expand It
This connects to a broader rule I use for AI tools:
Let the model express intent. Let trusted code supply authority.
The model can say:
{
"repository": "checkout-service",
"operation": "open_pull_request",
"title": "Add idempotency to order creation"
}It should not get to invent:
{
"github_token": "...",
"owner_id": "8f6f...",
"project_id": "b91c...",
"admin": true
}The first payload describes intent.
The second payload tries to carry authority.
MCP does not change that rule. It makes the rule more important because the tool surface is easier to expose and reuse.
For production tools, prefer contracts where the backend derives authority-bearing fields from trusted context:
- agent identity from verified auth;
- project ownership from the database;
- user ownership from the agent record;
- external token access from server-side credential policy;
- idempotency from server-side keys;
- audit metadata from request context.
The model should not be the source of truth for any of those.
Error Design Matters
Agent-facing errors should be explicit enough for the system to recover, but not so detailed that they leak sensitive internals.
Good MCP auth errors are boring and typed:
MCP_MISSING_HEADERSMCP_MALFORMED_HEADERSMCP_UNKNOWN_AGENTMCP_INVALID_SIGNATUREMCP_STALE_TIMESTAMPMCP_INVALID_NONCEMCP_NONCE_REPLAYEDMCP_PROJECT_NOT_OWNEDMCP_CREDENTIAL_NOT_AUTHORIZED
This helps in three ways.
First, the MCP client can distinguish retryable failures from permanent failures.
Second, operators can alert on patterns like repeated invalid signatures or nonce replay attempts.
Third, the model does not receive a vague "500" and start inventing recovery steps.
The backend should still avoid returning secrets, stack traces, decrypted token state, or raw policy internals. Clear error codes are enough.
The Implementation Checklist I Would Use
Before I allow an MCP route to mutate production state, I want answers to these questions:
- Is the MCP route separate from human UI routes?
- Does the route have an agent-specific auth path?
- Is the signed message format documented exactly once?
- Does the signature bind method, path, body hash, timestamp, nonce, and agent identity?
- Does the backend validate timestamp freshness?
- Does the backend enforce nonce uniqueness with a database constraint?
- Are mutating routes idempotent, or are automatic retries explicitly forbidden?
- Does authorization check resource ownership after authentication?
- Are external credentials resolved server-side?
- Are raw credentials kept out of MCP responses whenever possible?
- Are audit logs keyed by agent, route, resource, and outcome?
- Are missing auth, replay, stale timestamp, wrong owner, and missing credential cases tested?
That checklist is not glamorous. It is the difference between "the demo works" and "the system has a defensible trust model."
Where the Official MCP Authorization Spec Fits
The MCP specification includes an authorization model for HTTP-based transports, and the latest stable spec at the time of writing is the 2025-11-25 version.
That matters because MCP is moving toward interoperable authorization patterns rather than ad hoc local trust.
But the spec does not remove the need for application-level decisions:
- which actor owns the operation;
- which resources the actor can touch;
- how credentials are delegated;
- how mutations are made idempotent;
- what gets audited;
- what the model is allowed to supply.
Protocol authorization and product authorization are related, but they are not the same thing.
The protocol can help the client and server speak securely.
Your backend still has to decide what "allowed" means.
Takeaway
MCP is not just an API wrapper.
It is a new service boundary between probabilistic agents and deterministic systems.
If that boundary is vague, the agent inherits more authority than anyone intended. A browser token becomes a machine credential. A retry becomes a duplicate write. A model-filled field becomes an authorization decision. A tool call becomes an unaudited production action.
The fix is not to avoid MCP.
The fix is to treat MCP integrations like real distributed systems:
- separate human auth from agent auth;
- verify every agent-originated request;
- reject replays;
- authorize against server-side ownership;
- keep credentials under backend control;
- log enough to reconstruct what happened.
MCP makes tool access easier.
Trust boundaries are what make that access safe enough to use.
References
- Model Context Protocol. Authorization - 2025-11-25 specification. Read the spec.
- Model Context Protocol. Authorization security tutorial. Read the tutorial.
- OAuth Working Group. OAuth 2.1. Read the draft.