In this article
July 14, 2026
July 14, 2026

MCP authorization patterns: Per-tool scopes, consent, and least privilege

Practical authorization patterns for MCP servers: per-tool scopes, dynamic consent, and least privilege enforcement using WorkOS AuthKit primitives.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

The Model Context Protocol (MCP) has become the standard way for AI models to interact with business logic. Anthropic introduced it in late 2024, and it's since been adopted by OpenAI, Google DeepMind, Microsoft, and thousands of development teams, with the Python and TypeScript SDKs alone seeing tens of millions of monthly downloads. In December 2025, Anthropic donated the protocol to the Agentic AI Foundation under the Linux Foundation, making it a vendor neutral, community governed standard with OpenAI and Block as co-founding members.

That growth means auth can no longer be an afterthought. The June 2025 update to the MCP specification made this explicit: MCP servers are OAuth Resource Servers, not Authorization Servers. That single distinction is the difference between "add a login screen" and "actually model who can do what." Here's practical guidance on how to do the latter, not generic OAuth advice, but the specific problem of an LLM invoking tools on behalf of a user. Per-tool scopes, consent flows when an agent asks for something new, and least privilege enforcement so a "read user profile" tool can't quietly turn into "delete user profile." All of it grounded in WorkOS primitives, with code you can copy.

Why MCP breaks the usual auth model

Traditional OAuth assumes a human clicking "allow" on a consent screen for a fixed list of scopes decided at sign in time. MCP inverts that. An agent discovers tools dynamically, decides which ones to call, and may need capabilities the user never explicitly considered when they first authenticated. A single AI client might connect to thousands of MCP servers it has never seen before, so authorization servers now need to handle registration and consent for clients that show up unannounced, not just the handful your team pre configured.

That has three consequences for anyone building a server.

  1. Identity has to reach every tool. Every tool invocation needs to know who the user is, what org they belong to, and what they're allowed to do, not just that a request arrived with a valid session cookie somewhere upstream.
  2. Scopes have to be per tool, not per server. "Access to the MCP server" is not a useful permission. "Can call generateImage" is.
  3. Consent has to be dynamic. When an agent tries to invoke a new tool, you need a flow that either allows it based on existing grants or prompts the user, in the moment, not just at initial signup.

Get identity into every tool

The foundation is identity per tool call. The @xmcp-dev/workos plugin adds WorkOS AuthKit to an MCP server, and AuthKit provides user management, SSO, SCIM directory sync, and audit logs behind that identity. The plugin exposes session management with access to the authenticated user's ID, organization, role, and permissions via getSession(), plus user details through getUser() including email, name, and profile picture.

Install it:

  
npm install @xmcp-dev/workos
# or
pnpm add @xmcp-dev/workos
  

Then wire up the middleware. workosProvider() accepts apiKey, clientId, baseURL, authkitDomain, and an optional docsURL:

  
// middleware.ts
import { workosProvider } from '@xmcp-dev/workos';

export default workosProvider({
  apiKey: process.env.WORKOS_API_KEY!,
  clientId: process.env.WORKOS_CLIENT_ID!,
  baseURL: process.env.BASE_URL!,
  authkitDomain: process.env.WORKOS_AUTHKIT_DOMAIN!,
});
  

WORKOS_API_KEY and WORKOS_CLIENT_ID live on the Overview page of the WorkOS Dashboard, and your AuthKit domain will look like https://yourcompany.authkit.app. Under Connect → Configuration, enable Client ID Metadata Document (CIMD) and Dynamic Client Registration (DCR). DCR lets clients register themselves without manual configuration, which is what makes agent driven flows work at the scale described above, since you can't hand provision credentials for every agent that might connect.

Per-tool scopes with RBAC

Once identity is in place, scopes stop being a server wide concept and become a property of each tool. The session carries the user's permissions, which is where per-tool gating happens. Cloudflare's official demo MCP server, built with WorkOS AuthKit, shows the pattern in its simplest form: it includes a generateImage tool gated behind the image_generation permission, assigned through AuthKit, so tools visibly light up or disappear from an agent's list based on role.

  
// tools/generate-image.ts
import { type ToolMetadata } from 'xmcp';
import { getSession } from '@xmcp-dev/workos';

export const metadata: ToolMetadata = {
  name: 'generateImage',
  description: 'Generate an image from a text prompt',
};

export default async function generateImage(prompt: string) {
  const session = getSession();

  if (!session.permissions?.includes('image_generation')) {
    throw new Error('Missing permission: image_generation');
  }

  // ... call the model
}
  

That's the whole least privilege story at the tool level: each tool declares the permission it needs, checks it against the session, and refuses otherwise. A tool that reads user profiles simply never checks a users.write permission, so even if an agent tries to call it in a destructive way, it can't. In practice, most MCP clients handle a failed permission check by treating the tool as unavailable rather than surfacing a raw error to the end user, which is why it's worth thinking about permission checks as shaping which tools an agent can even see, not just what happens after it tries one.

Organization aware tools and audit trails

Least privilege isn't only about which tools an agent can call. It's about which data those tools can touch once called. getClient() exposes the full WorkOS SDK for features like directory sync, audit logs, and organization management. Scoping by organization looks like this:

  
import { getSession, getClient } from '@xmcp-dev/workos';

const session = getSession();
const workos = getClient();

if (session.organizationId) {
  const org = await workos.organizations.getOrganization(session.organizationId);
  // Scope data access to the user's organization
}
  

Every meaningful tool call should leave an audit trail. workos.auditLogs.createEvent() accepts organizationId, event, actor, and targets:

  
await workos.auditLogs.createEvent({
  organizationId: session.organizationId,
  event: {
    action: 'document.accessed',
    actor: { id: session.userId, type: 'user' },
    targets: [{ id: documentId, type: 'document' }],
  },
});
  

For enterprise directories, workos.directorySync.listUsers({ directory }) pulls users straight from the connected directory, which keeps permission grants in sync with the source of truth rather than a local copy that quietly drifts out of date.

Consent when an agent asks for something new

Dynamic Client Registration is the piece that makes consent work in an agent context. A new agent doesn't need a human to pre-provision credentials, but it also doesn't automatically inherit every scope. The user still authenticates through AuthKit, and permissions are still assigned through AuthKit. If the agent needs a capability the user hasn't granted, the tool call fails on the permission check, and you can prompt the user to grant it through your normal RBAC flow.

There's also a broader pattern worth building toward as the ecosystem matures: session scoped authorization. Instead of granting an agent persistent access through a long-lived OAuth token, access is time-limited to the duration of a specific task. When the session ends, access ends automatically, and the agent can't renew it on its own. A human has to explicitly approve a new session. This matters because static client secrets are still common in production MCP deployments industry-wide, and a token that only lives as long as the task it was issued for shrinks the blast radius of exactly the kind of leak that turns a minor bug into a real incident.

Cloudflare's integration with AuthKit makes the consent piece concrete in practice. It provides role based access control for agents, enterprise SSO and OAuth flows, and auditable delegation, deployed on Cloudflare Workers. The MCP endpoint is served at /sse, the callback URL is configured under Redirects → Sign in callback in the WorkOS Dashboard, and secrets are set with npx wrangler secret put for WORKOS_CLIENT_ID and WORKOS_CLIENT_SECRET. You can test the whole flow against the Cloudflare Workers AI Playground before deploying anything to production.

A checklist for MCP authorization

If you're building an MCP server today, the shape of a defensible authorization model looks like this.

  • Authenticate at the transport. Use @xmcp-dev/workos or the Cloudflare plus AuthKit pattern so every tool call carries a real session, not just a request that happened to reach your server.
  • Gate every tool on a specific permission. Not "authenticated," a named permission like image_generation. Assign permissions through AuthKit rather than checking role names in tool code.
  • Scope data by organization. Pull organizationId from getSession() and filter every query by it, so a tool built for one tenant can't accidentally return another tenant's data.
  • Audit every mutating call. Use workos.auditLogs.createEvent() on writes, not just reads of sensitive data, since writes are what your customers' security teams will ask about first.
  • Turn on DCR and CIMD. Under Connect → Configuration. This is what lets new agents register without you hand-provisioning clients one at a time.
  • Prefer time-boxed grants over standing access where you can. Session scoped authorization is still an emerging pattern, but it's the direction the ecosystem is heading, and it's worth designing your token lifetimes with that in mind now rather than retrofitting it later.

MCP is still expanding quickly. In January 2026 the protocol gained MCP Apps, its first official extension, letting tools return interactive interfaces instead of just text. The WorkOS team's own Enterprise MCP hackathon, with about 100 developers building over a weekend, produced a winning project called WebMCP that dynamically creates MCP tools from form elements in the DOM and uses AuthKit's RBAC to decide which tools a given user can see, alongside a separate project that scans MCP server repositories for vulnerabilities using the SAFE-MPC taxonomy. The servers that hold up as that ecosystem grows will be the ones where every tool call is tied to a real user, a real org, a real permission, and a real audit event, not just a server that happens to require a login.