
MCP Server Security: Where Protocol Controls End and Your API's Responsibilities Begin

Reihaneh Rahmanipour
.16 min read
.23 October, 2026
Software Engineer
.16 min read
.23 October, 2026
Checked against MCP specification revision 2026-07-28, the current revision on 13 October 2026. The protocol is still changing, so check the revision your clients and servers use.
MCP server security starts with one question: how do you let an AI agent use an internal system without giving it more access than the person behind it should have? This article covers how to authenticate users, authorise individual tool calls, protect the downstream API and audit what agents do. It focuses on the MCP server itself. For the wider question of agent identity, least privilege and oversight, see our guide to AI agent security.
Short answer: When you expose enterprise capabilities through the Model Context Protocol (MCP), design the server as a policy layer. It should accept only tokens issued for itself and check every tool call against the calling user's identity. It should offer a small set of business-shaped tools instead of a mirror of your API, call the backend with a separate credential, and log every call. The underlying API still has to enforce its own authorisation, because the MCP server is one of its clients.
How to read this article
MCP security advice often mixes three different things, so we label them:
Spec requirement: the specification says MUST or MUST NOT. If you implement that feature, it isn't optional.
Spec recommendation: the specification says SHOULD or MAY. These are sensible defaults you can depart from with a reason.
Our recommendation: engineering advice that isn't in the spec. These are design choices we think hold up in enterprise deployments.
Terms used in this article
MCP server: the service that exposes tools and data to an AI application. Here it sits in front of an internal API.
MCP client: the part of the AI application that talks to the server on the user's behalf.
Authorisation server: the system that authenticates the user and issues access tokens.
Audience (aud): the token claim that names the service the token is for.
Token passthrough: an MCP server forwarding the token it received to another API. The spec forbids it.
Token exchange (RFC 8693): swapping one token for another issued for a different service, often while keeping the user's identity.
Scope: a named permission carried in a token, such as billing:read.
Tool: an operation the model can ask the server to run.
If you want background on APIs before the security detail, start with our guide to what an API is in software development.
What goes wrong when you wrap an API
The quickest way to give an AI agent access to an internal system is to generate an MCP server from the API's OpenAPI document, give it a service account and publish every endpoint as a tool. It works well in a demo.
It also hands the agent everything the service account can do. The model chooses from 140 operations, and the audit log shows one service account doing all of it.
Take an illustrative scenario (not a client case). A B2B company has an internal Billing API with endpoints for listing invoices, issuing credit notes, changing payment terms and exporting customer ledgers. A finance analyst wants an assistant that can answer: "Which of my accounts are more than 30 days overdue?"
Wrapped API | Designed MCP server | |
Tools | One per endpoint (about 140) | Five: list_overdue_invoices, get_invoice, get_customer_balance, draft_credit_note, submit_credit_note |
Credential used against the API | Shared service account | Per-user, short-lived, exchanged for each request |
Identity the API sees | svc-mcp | The analyst, with the MCP server recorded as the actor |
Write operations | Callable at will | Drafting is free; submitting needs a separate scope and user confirmation |
Output | Full JSON records | Only the fields the task needs |
Audit trail | "svc-mcp called POST /credit-notes" | "Analyst X, via client Y, called submit_credit_note with scope billing:credit:write; allowed" |
Most of the right-hand column is design work the MCP specification leaves to you. The spec defines a transport, an authorisation framework and a set of requirements. It doesn't say what your tools are or which identity reaches your backend.
The reference architecture
Here is the whole flow for the Billing API example, from sign-in to audit log, with the check that happens at each boundary.

Sign-in and token request. The user signs in. The MCP client asks the authorisation server for a token and names the MCP server as the resource.
Token A is issued. The authorisation server issues Token A. Its audience is the MCP server, and it carries narrow scopes only.
Tool call. The MCP client sends the tool call to the MCP server with Token A.
Token checks. The MCP server checks the token's signature, issuer, expiry, audience and scope, and checks the request's Origin header.
Policy check. The MCP server decides whether this user, tenant and object may use this tool.
Token exchange. The MCP server exchanges Token A at the authorisation server for Token B. Token B's audience is the Billing API, its subject is the user and its actor is the MCP server.
API call. The MCP server calls the Billing API with Token B.
API checks. The Billing API runs its own object-level authorisation, tenant isolation and business rules.
Response and audit. The MCP server returns only the fields the task needs and writes an audit event: who called which tool, with which scopes, and whether it was allowed.
Two trust boundaries sit in this flow. The first surrounds the MCP server (steps 4, 5, 6 and 9). The second surrounds the Billing API (step 8).
Two details matter. The token in step 3 and the token in step 7 are different tokens for different audiences. And the Billing API in step 8 doesn't assume the MCP server got its checks right.
What the MCP specification requires
MCP is a JSON-RPC 2.0 protocol that connects hosts, clients and servers. The current revision, 2026-07-28, is stateless. Every request carries its own protocol version and client capabilities, and the initialize handshake and protocol-level sessions are gone (changelog). That leaves no session to hang trust on, so each request has to stand on its own credentials.
The specification calls authorisation optional and applies it to HTTP-based transports. Servers using stdio should take credentials from the environment instead (Authorization). For a remote enterprise server, we'd treat it as mandatory.
These are the requirements worth designing around. Unless a row says otherwise, they come from revision 2026-07-28:
Area | What the spec says | Level |
Role | The MCP server acts as an OAuth 2.1 resource server | Spec requirement (when authorisation is used) |
Discovery | Servers implement OAuth Protected Resource Metadata (RFC 9728) | Spec requirement (MUST) |
Audience | Validate that access tokens were issued specifically for this server, and only accept tokens valid for its own resources | Spec requirement (MUST) |
Passthrough | Don't pass the received token on to upstream APIs. The upstream token is a separate one | Spec requirement (MUST NOT) |
Token transport | Authorization: Bearer on every request. No tokens in the query string | Spec requirement (MUST) |
Errors | 401 for missing or invalid tokens. 403 with insufficient_scope and a scope parameter for step-up | 401: MUST. 403 scope challenge: SHOULD |
Origin | Validate the Origin header and return 403 if it's invalid | Spec requirement (MUST) |
Tool safety | Validate all tool inputs, implement access controls, rate limit invocations and sanitise outputs | Spec requirement (MUST) |
State handles | Possessing a state handle isn't authentication | Spec requirement (MUST NOT) |
Human in the loop | A human should be able to deny tool invocations | Spec recommendation (SHOULD), mainly a client job |
Tool list by scope | tools/list may vary with the authorisation on the request | Spec allows it (MAY) |
The last row is useful, and we come back to it below.
Three jobs that belong to the MCP server
1. Authenticate to the server, not through it
An MCP server in front of an internal API receives a token from the client. The first check is the audience: was this token issued for this MCP server?
The spec ties this to the resource parameter in RFC 8707, which clients must send in authorisation and token requests, and to aud validation (Security Best Practices). RFC 9700, the OAuth security best current practice, recommends the same: restrict access tokens to a specific resource server (RFC 9700).
A minimal validation step in TypeScript, using the jose library:
import { createRemoteJWKSet, jwtVerify } from "jose";const JWKS = createRemoteJWKSet(new URL(process.env.AS_JWKS_URI!));
const CANONICAL_URI = "https://mcp.example.com/mcp"; // must equal the `resource` clients sendexport async function authenticate(req: Request) {
const header = req.headers.get("authorization") ?? "";
const token = header.startsWith("Bearer ") ? header.slice(7) : null;
if (!token) throw challenge(401); try {
const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.AS_ISSUER!,
audience: CANONICAL_URI, // reject tokens minted for any other resource
});
return {
userId: String(payload.sub),
tenantId: String(payload.tenant_id), // claim name depends on your IdP
scopes: String(payload.scope ?? "").split(" "),
};
} catch {
throw challenge(401);
}
}This is an illustration, not production code. It assumes JWT access tokens. If your authorisation server issues opaque tokens, use token introspection (RFC 7662) and check the audience in the response. The tenant_id claim depends on your identity provider and isn't an MCP convention. You'll also need to cache the JWKS, handle key rotation and keep tokens out of your logs.
2. Keep inbound and outbound credentials separate
The spec says the token the server receives must not be forwarded. The Security Best Practices page lists what passthrough breaks: rate limiting and monitoring that rely on the token's audience, accountability (downstream logs can't tell clients apart) and trust boundaries (Token Passthrough).

So how does the Billing API learn who the user is? There are three common patterns:
Pattern | Backend sees | Main trade-off |
Shared service account | svc-mcp for everyone | Simple, but the server becomes a universal-access proxy |
Token exchange (RFC 8693) | The user, with the MCP server as actor | Needs authorisation-server support and an exchange policy |
Delegated third-party grant | The user, via the third party's own OAuth | Needs per-client consent protections for proxy servers |
RFC 8693 defines delegation as one party acting on behalf of another, with the actor keeping its own identity. The act claim records that relationship (RFC 8693). For auditing, this lets the Billing API log "analyst X, via mcp-gateway".
If your MCP server proxies a third-party API using a static client ID and allows dynamic client registration, the specification describes a confused deputy attack that works through consent cookies. It requires per-client consent before the request is forwarded to the third-party authorisation server (Confused Deputy Problem). If your server only fronts your own API with your own authorisation server, that attack doesn't apply, but it explains why the rule exists.
3. Decide what the model can see and do
The shape of your tools is an access control. An endpoint-shaped tool like PATCH /customers/{id} makes you trust the model to choose safe arguments. A capability-shaped tool like draft_credit_note can only do what its implementation does.
Some practical rules:
Split read, draft and commit. Let draft_credit_note create a pending record, and require a separate scope for submit_credit_note. Clients then have a natural point to ask the user for confirmation. (Our recommendation.)
Return only the fields you need, and filter on the server. The outputSchema field lets you declare the shape of results, and the spec says servers MUST produce results that conform to it.
Constrain inputs in the schema with enum, maxLength, pattern, maximum and additionalProperties: false, then validate again in code. The schema is a contract for the client, and the spec still requires server-side validation. Our application security basics guide covers input validation, error handling and secrets management in more depth.
Filter the tool list by scope. A caller without billing:credit:write never sees submit_credit_note. That reduces what the model can attempt and shrinks the surface for injection, but it isn't a security boundary, so check again on tools/call. (The spec allows the filtering; the re-check is our recommendation.)
Don't rely on annotations. They describe a tool's behaviour, but the spec says clients must treat them as untrusted unless the server is trusted. A readOnlyHint is a signal to well-behaved clients, not enforcement.
What a per-call authorisation check looks like
The scope in a token tells you what the client was granted. The authorisation check tells you whether this user may do this thing to this object:
async function submitCreditNote(ctx: AuthContext, args: { noteId: string }) {
requireScope(ctx, "billing:credit:write"); // else 403 insufficient_scope + scope="..."
const downstream = await exchangeToken(ctx); // RFC 8693, audience = billing-api
const note = await billing.getNote(downstream, args.noteId);
if (note.tenantId !== ctx.tenantId) {
audit.deny(ctx, "submit_credit_note", args.noteId, "tenant_mismatch");
return toolError("Credit note not found."); // do not reveal existence
}
if (note.status !== "draft") return toolError("Only draft notes can be submitted.");
const result = await billing.submit(downstream, note.id);
audit.allow(ctx, "submit_credit_note", note.id);
return structured({ id: result.id, status: result.status });
}The helpers (requireScope, exchangeToken, billing, audit, toolError, structured) are placeholders. The tenant comparison is deliberate redundancy. The Billing API should refuse a cross-tenant read on its own, and the MCP server checks as well so that a downstream bug doesn't become a data leak. The same holds in reverse.
Which architecture should you choose?
Teams most often get the credential pattern wrong, so here are three realistic situations.

A read-only assistant over public product documentation. The data isn't personal and isn't tenant-specific, so a shared service account is reasonable. You still need audience validation, rate limits and logging. Per-user exchange adds little.
An internal assistant that reads and changes customer data, backed by an API you control. Use token exchange. The API gets the user as subject and the MCP server as actor, so its own authorisation rules and logs work as they do for any other client. This is the Billing API case.
An assistant that acts in an external SaaS product on a user's behalf. The SaaS has its own authorisation server, so use a delegated grant. If you proxy with a static client ID and dynamic registration, add the per-client consent flow the spec requires.
A quick test: if a wrong answer would expose one person's or one tenant's data, don't use a shared service account. Token exchange also assumes the API trusts your authorisation server and can enforce rules per user. If it can't, fix that first, or limit the tools to non-personal data.
Not sure which pattern fits your systems?
Choosing between a shared service account, token exchange and a delegated grant depends on your API, your identity provider and your data. If you'd like a second opinion before you build, our team is happy to talk it through.
Where the MCP server's responsibility ends
An MCP server can't make a weak API safe, and a strong API still needs a policy layer in front of it. These duties stay with the API. Our API development services page describes how we approach OAuth 2.0, JWT, role-based access control, rate limiting and audit logs when we build them:
Concern | Owned by | Why not the MCP server |
Object-level and tenant authorisation | API | Other clients, such as the web app and scripts, reach the API without MCP |
Business rules (credit limits, approvals) | API | Rules have to hold whichever client is calling |
Rate limiting per principal | Both | MCP limits per tool and per user. The API limits per credential |
Data classification and masking | API, plus MCP output shaping | Defence in depth: MCP trims what the model sees |
Secrets for databases and downstream systems | API or platform secrets manager | MCP servers should hold as few long-lived secrets as possible |
The Asana incident shows why the API-side check matters. Asana launched its MCP server on 1 May 2025 and found a logic flaw on 4 June. It took the feature offline until 17 June. Roughly 1,000 customers were affected. The exposed data was limited to the objects a given MCP user could reach, and reporting says there was no evidence of malicious exploitation (BleepingComputer).
For tenant isolation, test it explicitly in the MCP layer with cross-tenant tests in CI. Don't assume the shared API enforces it on every access path.
Prompt injection: what an MCP server can and can't do
Indirect prompt injection happens when untrusted content processed by a model changes its behaviour. OWASP's LLM01 guidance recommends least-privilege access, human approval for high-risk actions and keeping untrusted content separate (OWASP LLM01).
The Invariant Labs disclosure of 26 May 2025 shows the shape of the problem. A malicious issue in a public repository led an agent, using a GitHub MCP integration with broad repository access, to read private repository data and leak it through a public pull request. Invariant described this as an architectural issue at the agent-system level, not a defect in the server code (Invariant Labs).
An MCP server can't tell that a model has been manipulated. It can limit the damage:
Avoid risky combinations. Don't expose private-data access, untrusted-content ingestion and an outbound write channel under one credential. If a tool returns external content such as tickets, emails or web pages, keep write tools off the same token, or require confirmation for them.
Scope tokens by task. Issue narrow tokens, such as a single repository or customer account, rather than tenant-wide ones.
Treat tool output as data. Strip markup and instructions from fields you don't control, and label external text as external. This reduces the risk without removing it.
Put irreversible actions behind confirmation or out-of-band approval. Elicitation can ask the user for input, but the client mediates it, so it shouldn't be the only gate for destructive actions. How much autonomy to allow at each stage is a separate decision, which we cover in our AI agent loops framework for CTOs.
Don't rely on injection-detection filters as your main control. Structural limits, meaning what a tool can do and what a token allows, can be tested. Classifier accuracy can't carry a compliance argument.
A compact threat model
The table maps the main threats to where they're addressed. The first five come from the spec's security guidance. The rest come from the sources cited above.
Threat | Where it's stopped | Control |
Token meant for another service is accepted (token passthrough) | MCP server | Validate audience; accept only tokens issued for this server |
Stolen or over-broad token used for unrelated tools | MCP server, authorisation server | Narrow scopes, short-lived tokens, step-up challenges |
Confused deputy through a proxy with a static client ID | MCP server (proxy) | Per-client consent before forwarding |
DNS rebinding against a local or internal server | MCP server | Validate Origin, bind to localhost in development |
Guessed or stolen state handle | MCP server | Bind handles to the verified user; never treat a handle as authentication |
Cross-tenant data access | MCP server and API | Tenant check in both layers, cross-tenant tests in CI |
Prompt injection driving legitimate tools | Tool design, scopes, client confirmation | Narrow tokens, no risky combinations, approval for writes |
Unreviewed MCP servers in use (shadow servers) | Governance | Registry of approved servers |
No evidence after an incident | MCP server | Structured audit log per tool call |
Identity at enterprise scale
Per-user OAuth consent works for individuals. In a company, security teams want central policy and central revocation.
The Enterprise-Managed Authorization extension (io.modelcontextprotocol/enterprise-managed-authorization) targets this. The client gets an Identity Assertion JWT Authorization Grant (ID-JAG) from the corporate identity provider, then exchanges it at the MCP server's authorisation server for an access token. Access policy and revocation sit in the identity provider (extension docs). The ext-auth repository lists it as stable. Client support varies, so check the client matrix before you commit to it.
Other changes in the current revision to plan for:
Client registration. Client ID Metadata Documents are now the preferred mechanism. Dynamic Client Registration is deprecated but still available for compatibility. For an internal deployment, you may prefer pre-registered clients and an allowlist policy.
Issuer validation. Clients must validate the iss parameter against the issuer they recorded (RFC 9207), and authorisation servers should send it. This closes mix-up attacks that PKCE alone doesn't prevent.
State without sessions. With sessions gone, any state your tools keep, such as a draft basket or a workflow ID, lives behind an explicit handle. The spec says possessing a handle isn't authentication. Bind the handle to the verified user, for example by keying storage as <user_id>:<handle> with the user taken from the token.
Observability and audit
The spec says clients should log tool usage. It doesn't require server-side audit, so you have to add it. Log one structured event for each tools/call:
Timestamp, correlation ID (the revision documents traceparent propagation in _meta) and server version
Subject, tenant, client ID and delegated actor
Tool name, scopes presented, and the decision (allow, deny or step-up required)
An argument digest or redacted arguments, never raw secrets or full personal data
Downstream call identifiers and outcome, plus latency and result size
Set alerts for repeated denied calls from one user, spikes in scope step-ups, unusually large results, and write-tool calls outside business hours.
OWASP's MCP Top 10 is still a beta, living document. It lists "Lack of Audit and Telemetry" and "Shadow MCP Servers" among its ten categories (OWASP MCP Top 10). A registry of approved servers belongs with your shadow AI governance work.
Transport and deployment hardening
Validate Origin on every request and return 403 on a mismatch. The spec makes this mandatory. Bind local development servers to 127.0.0.1, not 0.0.0.0.
Require HTTPS for all authorisation endpoints and use exact-match redirect URIs.
If you expose parameters through x-mcp-header, never mark secrets or personal data. They travel as HTTP headers that intermediaries can see. Servers must reject requests where the headers don't match the body, and any gateway that routes on those headers should check the protocol version first.
If your server fetches URLs derived from user or model input, or your gateway acts as an MCP client, apply SSRF protections: block private and link-local ranges, validate redirects and consider an egress proxy. The spec's guidance is written for clients and authorisation servers, but the principle applies to any component that fetches attacker-influenced URLs.
Pin and review dependencies. An MCP server is code that holds credentials, so put it through the same supply chain process as any other privileged service.
Common implementation mistakes
Generating tools from the OpenAPI file and shipping them as they are. The result is endpoint-shaped tools, broad scopes and a larger target for injection.
Forwarding the client's token to the backend. The spec forbids it, and it removes the audience boundary.
Using one service account "for now". Temporary shared credentials tend to stay, and per-user identity is much harder to add after launch.
Publishing every scope up front. The spec's scope minimisation guidance warns against wildcard or omnibus scopes and against listing everything in scopes_supported.
Treating the tool list as a permission model. Hiding a tool doesn't deny a call.
Skipping negative tests. Test a token for the wrong audience, an expired token, a missing scope, another tenant's object ID and a schema-violating argument. Each should fail with the right status and appear in the log.
Logging tokens or full payloads. Audit logs shouldn't become a second place for secrets and personal data to leak from.
Implementation checklist
Publish Protected Resource Metadata (RFC 9728), and return WWW-Authenticate with resource_metadata and scope on a 401. (Metadata is a spec requirement; the scope parameter is SHOULD.)
Validate issuer, signature, expiry and audience on every request. (Spec requirement.)
Define scopes by capability (billing:read, billing:credit:write), not all or *. (Spec guidance on scope minimisation.)
Design five to fifteen business tools, not one per endpoint. Split read, draft and commit. (Our recommendation.)
Use inputSchema with additionalProperties: false, add an outputSchema, and validate on the server. (Server-side validation is a spec requirement.)
Authorise every call against user, tenant and object, and test cross-tenant access in CI. (Access controls are a spec requirement; the CI tests are our recommendation.)
Exchange tokens for downstream calls and never forward the inbound token. (No passthrough is a spec requirement; token exchange is our recommendation.)
Rate limit per user and per tool, and cap result sizes. (Rate limiting is a spec requirement; the size cap is our recommendation.)
Log every call with subject, actor, scopes and decision. (Our recommendation.)
Keep the API's own authorisation switched on. Treat the MCP server as another client. (Our recommendation.)
Review the server against the spec each time the protocol revision changes. (Our recommendation.)
Planning an MCP rollout?
If you are about to expose internal systems to AI agents, talk to us before the tool list is final. We can look at which credential pattern fits your setup, how wide your tool surface is, and where your API's own authorisation needs to hold. Book a free consultation and talk it through with the Whitefox team.
Conclusion
The protocol gives you an authorisation framework: audience-bound tokens, protected resource metadata and scope challenges. It doesn't decide what an agent may do. The tool list, the scope model, the downstream identity and the audit trail decide that, and all four are ordinary architecture work.
A server generated from an OpenAPI file becomes a second, less controlled copy of your API. A server built around a few business tools is narrower and easier to audit.
If you're unsure which credential pattern fits your system, or whether your tool surface is too wide, review both before rollout. Whitefox can help in a few ways:
API development services: secure API design and integration, which is the layer your MCP server depends on.
AI software development services: integrating AI into business workflows and automation.
Fractional Head of AI: hands-on design of AI systems, including LLM orchestration, data flows and monitoring.
Fractional CTO: engagements that typically begin with a focused technical assessment.
For related reading, see AI agent security on controlling what an agent is allowed to do, shadow AI governance on unapproved AI tools, and the AI agent loops framework on autonomy. You can browse all of our articles too.
Ready to Explore AI in Your Projects?
Let’s talk about how AI models can accelerate your engineering workflows
and unlock new possibilities.




