MCP Server Security

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

Reihaneh Rahmanipour

Reihaneh Rahmanipour

Software Engineer

.16 min read

.23 October, 2026

Share

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.

Calling an internal Billing API through an MCP server
  1. 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.

  2. Token A is issued. The authorisation server issues Token A. Its audience is the MCP server, and it carries narrow scopes only.

  3. Tool call. The MCP client sends the tool call to the MCP server with Token A.

  4. Token checks. The MCP server checks the token's signature, issuer, expiry, audience and scope, and checks the request's Origin header.

  5. Policy check. The MCP server decides whether this user, tenant and object may use this tool.

  6. 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.

  7. API call. The MCP server calls the Billing API with Token B.

  8. API checks. The Billing API runs its own object-level authorisation, tenant isolation and business rules.

  9. 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 send
export 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).

Token Authentication Strategies

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.

MCP Server Credential Pattern Selection

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.

Book a free consultation

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:

  1. 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.

  2. Scope tokens by task. Issue narrow tokens, such as a single repository or customer account, rather than tenant-wide ones.

  3. 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.

  4. 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

  1. 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.)

  2. Validate issuer, signature, expiry and audience on every request. (Spec requirement.)

  3. Define scopes by capability (billing:read, billing:credit:write), not all or *. (Spec guidance on scope minimisation.)

  4. Design five to fifteen business tools, not one per endpoint. Split read, draft and commit. (Our recommendation.)

  5. Use inputSchema with additionalProperties: false, add an outputSchema, and validate on the server. (Server-side validation is a spec requirement.)

  6. 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.)

  7. Exchange tokens for downstream calls and never forward the inbound token. (No passthrough is a spec requirement; token exchange is our recommendation.)

  8. Rate limit per user and per tool, and cap result sizes. (Rate limiting is a spec requirement; the size cap is our recommendation.)

  9. Log every call with subject, actor, scopes and decision. (Our recommendation.)

  10. Keep the API's own authorisation switched on. Treat the MCP server as another client. (Our recommendation.)

  11. 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.

Book a free consultation

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:

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.

Frequently Asked Questions

No. The specification makes authorisation optional and applies it to HTTP transports. For any server that exposes enterprise data over HTTP, treat it as required.

Technically yes, and it can suit non-personal data. For user data, it moves authorisation decisions into the MCP layer and removes individual identity from downstream logs. Prefer token exchange.

No. The spec says MCP can't enforce its trust and safety principles at the protocol level. Tool design, scopes and confirmation limit the impact of prompt injection. The protocol doesn't.

The MCP server owns client authentication, audience binding, tool exposure, per-call policy, output shaping and audit. The API owns object-level authorisation, tenant isolation, business rules and its own rate limits.

Whitefox.cloud logo

Copyright © 2026

All rights reserved.