MCP security: giving AI agents access to company systems without sharing tokens
What the MCP spec says about OAuth, audience binding and token passthrough, and a gateway pattern that gives AI agents per-person access with a full audit log.
Most teams connect their first MCP server in an afternoon. Someone creates a personal access token, pastes it into the config file of Claude Desktop, Cursor or VS Code, and the agent can suddenly read tickets, query a database or open pull requests. This article looks at what that setup gets wrong, what the Model Context Protocol specification says about authorization, and a pattern that lets agents reach company systems without anyone copying a token into a JSON file.
All references to the specification are to revision 2026-07-28, which modelcontextprotocol.io lists as the current version at the time of writing (29 September 2026).
What the usual setup looks like
A local MCP server is a process that the client starts on your laptop. The client passes it credentials through environment variables. The GitHub MCP server README, for example, shows a local configuration along these lines:
{
"mcp": {
"servers": {
"github": {
"command": "docker",
"args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}" }
}
}
}
}The server itself is fine. GitHub's README advises minimal scopes and careful storage, and its remote server supports OAuth instead. Trouble starts when the same shape is repeated across ten tools and twenty people:
- Long-lived tokens sit in plain config files and shell profiles on every laptop that uses them. Rotating one means finding every copy.
- Tokens are created with broad scopes because nobody knows in advance which tools the agent will call.
- Teams share a service account token for systems where individual accounts are a hassle, so the downstream system sees one identity for everyone.
- The local server runs with the same operating system rights as the user, often with network access to internal systems.
- No central record exists of which person, using which agent, called which tool with which arguments. The SaaS audit log shows the token owner, if it shows anything.
In May 2025 Invariant Labs described an attack where a malicious issue in a public GitHub repository instructed an agent to pull data from the user's private repositories and publish it. The researchers said the GitHub MCP server code was not at fault and called it an architectural issue at the level of the agent system: the agent could reach private repositories that a task in the public one did not need. Their mitigations were narrower permissions and limiting an agent to one repository per session. They also noted that many users click "Always Allow" on tool confirmations, which removes that safeguard.
Local tooling around MCP has had its own bugs. CVE-2025-6514 in the npm package mcp-remote allowed OS command injection when the tool connected to an untrusted MCP server, through a crafted authorization endpoint URL. The GitHub advisory rates it critical (CVSS 9.6) and lists versions 0.0.5 through 0.1.15 as affected, with a fix in 0.1.16. On a laptop that also holds a folder of API tokens, code execution means those tokens are exposed too.
What the MCP specification says about authorization
The authorization chapter is optional for implementations, and it applies to HTTP transports. For stdio, the spec says servers should take credentials from the environment, which is exactly the local pattern above. Over HTTP with authorization turned on, the rules are strict.
Roles and discovery
A protected MCP server acts as an OAuth 2.1 resource server. The MCP client is an OAuth 2.1 client acting on behalf of a resource owner, usually the person at the keyboard. A separate authorization server issues tokens, and it can be your existing identity provider. MCP servers must publish OAuth 2.0 Protected Resource Metadata (RFC 9728), and clients must use that document to find the authorization server. In practice, an unauthenticated request gets a 401 with a WWW-Authenticate header that points to /.well-known/oauth-protected-resource, and the client follows it from there. Clients must use PKCE with S256 where they can, according to the security considerations page.
Audience binding and the ban on token passthrough
Clients must send the resource parameter from RFC 8707 (Resource Indicators) in both the authorization and token requests, set to the canonical URI of the MCP server. Servers must check that each token was issued for them as audience and reject anything else. The spec also rules out a common shortcut: an MCP server that calls an upstream API needs its own, separate token for that API, and it must not forward the token it received from the client.
The security best practices document calls token passthrough "explicitly forbidden" and explains why. Passthrough skips whatever rate limiting or validation the MCP server was meant to apply. It also muddles the audit trail: the MCP server cannot tell clients apart, and the downstream API logs a different identity from the server that actually sent the request. And a token that is accepted in several places lets an attacker who compromises one service move on to the others.
The confused deputy problem
The same document describes a confused deputy attack on MCP proxy servers. The vulnerable setup is a proxy that uses one static OAuth client ID with a third-party API, lets MCP clients register themselves dynamically, and relies on the third party's consent cookie. An attacker registers a client with their own redirect URI, sends the user a link, the third party skips its consent screen because of the cookie, and the authorization code ends up with the attacker. The required fix is per-client consent at the proxy: keep a registry of approved client IDs per user, show a consent page that names the client, the scopes and the redirect URI, and validate redirect URIs and the state parameter exactly.
Client registration and scopes
Clients need a client ID before they start. The client registration page lists three mechanisms: pre-registration, Client ID Metadata Documents (the client uses an HTTPS URL pointing to its own metadata as its ID), and Dynamic Client Registration under RFC 7591. In 2026-07-28, Dynamic Client Registration is marked deprecated and kept for backward compatibility, and new implementations are pointed to Client ID Metadata Documents.
On scopes, the spec asks servers to announce the scopes a request needs in the WWW-Authenticate challenge and asks clients to request only what they need. When a call needs more, the server answers 403 with insufficient_scope and the client goes through a step-up authorization. The best practices page lists wildcard scopes and publishing every possible scope up front as common mistakes.
Humans in the loop
The tools chapter says there should always be a human able to deny tool invocations, and that clients should ask for confirmation on sensitive operations and log tool usage for audit. Tools can carry annotations such as readOnlyHint and destructiveHint, but the schema describes these as hints that clients should not rely on when they come from untrusted servers, so the decision to block or confirm a destructive tool has to be made somewhere you control.
For organisations with a central identity provider, the MCP project also published an Enterprise-Managed Authorization extension in June 2026. It lets the company IdP decide which MCP servers a user can reach, using an Identity Assertion JWT Authorization Grant that the client exchanges for an MCP server token.
A practical pattern: per-person access through a gateway
The specification gives you the building blocks. Put together for a company with a handful of internal systems and a few SaaS tools, they form a pattern that looks like this.
- Every agent authenticates as the person using it. The MCP client runs the OAuth flow against your identity provider, and the resulting token is bound to the gateway's URI through the
resourceparameter. There are no shared service account tokens on laptops. - Upstream credentials stay on the server side. The gateway holds the API keys or OAuth refresh tokens for GitHub, Jira, the ERP or the database, or obtains a per-user token through a flow such as OAuth 2.0 Token Exchange (RFC 8693). The client's token is never forwarded.
- Scopes are defined per tool. Reading tickets and deleting a project are different permissions, and the gateway only lists the tools a caller's scopes allow. The spec explicitly allows the
tools/listresult to vary by the authorization on the request. - Tools that write or delete are off by default. A new connection gets read-only tools, and write access is granted per group or through step-up authorization when someone needs it.
- High-risk calls need a human decision. Payments, deletions, permission changes and bulk exports pause until the user, or a named approver, confirms. This check runs at the gateway, so it does not depend on the client's settings or on a tool's own annotations.
- One log records every tool call: the person, the agent or client ID, the tool, the arguments (redacted where needed), the policy decision and the result. When something goes wrong, this is the record that shows which person and which agent made the call, which the SaaS audit log usually cannot.
This pattern lines up with outside guidance. The OWASP Top 10 for Agentic Applications for 2026, published in December 2025, includes Tool Misuse (ASI02) and Identity and Privilege Abuse (ASI03) among its categories. The MCP spec's own scope minimization guidance describes the same progressive, least-privilege model: a small initial scope set, with elevation when a privileged operation is first attempted.
Local stdio servers still make sense for tools that only touch the developer's own machine. For those, the spec asks clients to show the exact command before starting a new server and to sandbox it. Anything that reaches a shared company system belongs behind HTTP authorization.
Checklist
- Inventory every MCP server in use, local and remote, and the credential each one holds.
- Remove personal access tokens and shared service account keys from client config files. Where a vendor offers a remote MCP server with OAuth, use it.
- For your own HTTP MCP servers, publish Protected Resource Metadata, validate the token audience, and reject tokens issued for other resources.
- Never forward the client's token upstream. Use a separate credential or a token exchange per upstream API.
- If you run an OAuth proxy in front of a third-party API, implement per-client consent and exact redirect URI checks.
- Define scopes per tool, avoid wildcard scopes, and start new users on read-only tools.
- Require explicit approval for destructive or high-risk calls, enforced on the server side.
- Log every tool call with person, client, tool, decision and outcome, and keep those logs where your security team already looks.
- Keep MCP client tooling up to date, and treat a connection to an unknown MCP server like running unknown code.
What we are building
Flowplane is building an MCP endpoint that follows this pattern: agents will sign in as the person using them, upstream secrets will stay at the gateway, and every tool call will be logged against a person and a policy decision. It is not available yet, and we will write about it here when there is something to try.
Sources
- MCP specification: versioning (current revision 2026-07-28)
- MCP specification 2026-07-28: Authorization
- MCP specification 2026-07-28: Authorization security considerations
- MCP specification 2026-07-28: Client registration
- MCP specification 2026-07-28: Tools
- MCP Security Best Practices
- MCP blog: Enterprise-Managed Authorization (18 June 2026)
- RFC 8707: Resource Indicators for OAuth 2.0
- RFC 9728: OAuth 2.0 Protected Resource Metadata
- RFC 8693: OAuth 2.0 Token Exchange
- Invariant Labs: GitHub MCP exploited (26 May 2025)
- GitHub advisory GHSA-6xpm-ggf7-wc3p (CVE-2025-6514, mcp-remote)
- OWASP Top 10 for Agentic Applications for 2026
- GitHub MCP server README