FlowplaneBook a pilot
Blog
GuideUnified MCP endpoint

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:

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.

  1. 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 resource parameter. There are no shared service account tokens on laptops.
  2. 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.
  3. 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/list result to vary by the authorization on the request.
  4. 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.
  5. 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.
  6. 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

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