The Mistake I Keep Seeing With MCP
Model Context Protocol gives AI agents a standard way to connect to tools. 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.
Then the backend has to know which actor is making the request and what authority it has, and it has to be able to prove both before doing anything irreversible.
That is the boundary most early MCP integrations under-design.
In 30 seconds
- MCP defines how agents reach tools. The security boundary around those calls is still yours to build.
- 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.
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 it also makes dangerous tool access easier to add.
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 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 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 binding may look like overkill, but it matters as soon as an 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.
Use an insert-first uniqueness check.
Do not do:
SELECT nonce
if missing:
INSERT nonceThat creates a race.
Do:
INSERT nonce
if unique constraint fails:
reject replayThe unique constraint makes the check atomic, so two requests carrying the same nonce cannot both get through, even if they arrive at the same moment.
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?
A prompt can say:
Only modify projects you own.
That instruction is worth having. The check itself lives in deterministic backend code, which refuses the 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.
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.
What the Model Is Allowed to Put in a Tool Call
This connects to a broader rule I use for AI tools. The model describes what it wants done, and trusted code supplies anything that carries authority.
A tool input the model can reasonably send looks like this:
{
"repository": "checkout-service",
"operation": "open_pull_request",
"title": "Add idempotency to order creation"
}This one carries authority the model does not have:
{
"github_token": "...",
"owner_id": "8f6f...",
"project_id": "b91c...",
"admin": true
}The backend should be the one filling in every field of the second payload.
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
Typed codes let the MCP client tell retryable failures from permanent ones, and the model does not get a vague "500" and start inventing recovery steps.
Operators can also alert on patterns like repeated invalid signatures or nonce replay attempts.
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?
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 authorization patterns that work across different clients and servers.
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.
MCP Routes as Production Services
An MCP route into your backend creates a service boundary for agent-originated requests.
If that boundary is vague, the agent inherits more authority than anyone intended. A browser token gets used as a machine credential, and a retry turns into a duplicate write. A field the model filled in ends up deciding authorization, and a tool call changes production with no audit trail.
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.
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.