> ## Documentation Index
> Fetch the complete documentation index at: https://fastmcp-ts.docs.prefect.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization

> Scopes, per-component checks, and combining auth strategies

[Verification](/servers/auth/token-verification) establishes who is calling and hands every handler an `AccessToken`. Authorization is the next decision: given that verified identity, what is this caller allowed to do? FastMCP expresses authorization as small checks that read the `AccessToken` and either pass silently or throw `AuthorizationError`. You attach those checks where the decision belongs — to an individual tool, resource, or prompt — and FastMCP runs them at the right moments without you wiring anything into the request path.

The recommended starting point is scopes. OAuth tokens already carry scopes describing what was granted, so gating a component on a required scope is the most direct expression of "this caller may use this." Reach for richer logic only when a scope check cannot say what you mean.

## Requiring scopes

`requireScopes` builds an authorization check from one or more scope names. It passes when the caller's token carries every named scope and throws `AuthorizationError` naming the first one it lacks. You set it as the `auth` field on a component's config, and it gates that component alone.

```typescript tool.ts theme={null}
import { z } from 'zod'
import { requireScopes } from '@prefecthq/fastmcp-ts/server'

server.tool(
  {
    name: 'deleteRecord',
    input: z.object({ id: z.string() }),
    auth: requireScopes('records:write'),
  },
  ({ id }) => {
    // only reached when the caller's token includes 'records:write'
    return `deleted ${id}`
  },
)
```

A component's check does double duty. When a caller lists tools, FastMCP runs each tool's check against their token and silently drops the ones they fail, so the list a caller sees contains only what they may actually use. When a caller calls a tool directly, the same check runs again before the handler — calling something you were never shown still fails. This is why authorization belongs on the component and not buried inside the handler: putting it on the config gets you correct list filtering and correct call gating from one declaration, and a request with no verified identity at all is rejected before any check even runs.

## Per-component auth

`requireScopes` is one ready-made check, but the `auth` field accepts any `AuthCheck` — a function that takes the `AccessToken` and throws to deny. That escape hatch is for decisions scopes cannot express: ownership ("this caller may only touch their own records"), tenancy ("this caller's organization must match"), or anything that depends on the token's `claims` rather than its scopes.

```typescript tool.ts theme={null}
import { z } from 'zod'
import { AuthorizationError } from '@prefecthq/fastmcp-ts/server'

server.tool(
  {
    name: 'readBilling',
    input: z.object({}),
    auth: (token) => {
      if (token.claims.plan !== 'enterprise') {
        throw new AuthorizationError('Enterprise plan required')
      }
    },
  },
  () => 'billing details',
)
```

The check runs in the same two places — list filtering and call gating — so a custom predicate hides the component from callers who fail it exactly as a scope check does. Throwing `AuthorizationError` is what marks a denial; the message you pass surfaces to the caller. Resources and prompts take the same `auth` field on their config and behave identically, which keeps one authorization model across all three primitives.

## Multiple schemes

A server sometimes has to accept tokens of different kinds at once — JWTs from your identity provider beside long-lived service tokens, or two providers during a migration. `multiAuth` composes several verifiers into a single one that tries each in turn and returns the first identity any of them produces. The point for authorization is that every verifier yields the same `AccessToken` shape, so your scope checks and per-component rules are written once and apply no matter which scheme verified the caller.

```typescript server.ts theme={null}
import {
  FastMCP,
  multiAuth,
  jwtVerifier,
  staticTokenVerifier,
} from '@prefecthq/fastmcp-ts/server'

const server = new FastMCP({
  name: 'secure-server',
  auth: multiAuth(
    jwtVerifier({
      jwksUri: 'https://auth.example.com/.well-known/jwks.json',
      issuer: 'https://auth.example.com',
      audience: 'my-mcp-server',
    }),
    staticTokenVerifier({
      'service-token': { clientId: 'internal-service', scopes: ['read'] },
    }),
  ),
})
```

For the verifier side of this — why you would mix schemes and how to order them — see [token verification](/servers/auth/token-verification). Here the takeaway is that authorization stays uniform downstream of whichever verifier matched.

## Caching and identity

Authorization makes responses caller-specific: two callers asking the same question can legitimately get different answers, because one is allowed something the other is not. That collides with response caching, where a result computed for one caller could be replayed to another. FastMCP's [`CachingMiddleware`](/servers/middleware) is built for this — its default cache key already partitions by identity, so an entry written for one caller is never served to another, with no configuration.

The default key is the request method, plus an auth partition, plus the serialized request params. The auth partition is the piece that keeps identities apart: an unauthenticated request uses a single `anon` partition, and an authenticated request is partitioned by the SHA-256 hash of its bearer token — the hash, never the raw token. A privileged caller's filtered `tools/list` therefore stays out of an anonymous caller's cache in both directions. The [caching](/concepts/caching) page covers the full model.

You take over that partitioning only when you pass a custom `CacheKeyFn`. A custom key **replaces** the default entirely — the auth partition is not merged back in — so when your cached values depend on who is calling, you must fold identity into the key yourself. Use the same dimension the default uses: the bearer token, hashed. The first argument is the TTL in milliseconds.

```typescript server.ts theme={null}
import { createHash } from 'node:crypto'
import { FastMCP, CachingMiddleware } from '@prefecthq/fastmcp-ts/server'

const server = new FastMCP({ name: 'secure-server' })

server.use(
  new CachingMiddleware(60_000, (ctx) => {
    const token = ctx.mcpContext.auth?.token
    const id = token ? createHash('sha256').update(token).digest('hex') : 'anon'
    return `${ctx.method}:${id}:${JSON.stringify(ctx.request)}`
  }),
)
```

`ctx.mcpContext.auth?.clientId` is a coarser dimension: two tokens can share a `clientId` yet differ in scope, so a `clientId` key can merge identities that must stay apart — prefer the hashed token. The general rule follows from the verification-versus-authorization split: identity established once at the edge has to be carried into every decision downstream that depends on it, and a cache is one of those decisions.
