Skip to content

Protect a FastMCP App

Use the FastMCP adapter when a FastMCP app should reject invalid, expired, insufficient, or revoked Caracal mandates. Python uses caracalai-fastmcp; TypeScript uses @caracalai/fastmcp.

  • A mandate-aware MCP resource, stable audience, tool scopes, issuer, and zone ID.
  • A shared production revocation store.
  • A FastMCP hook that can reject before tool code runs; verification after dispatch is too late.
Terminal window
pip install "caracalai-fastmcp[fastmcp]" caracalai-revocation
Terminal window
npm install @caracalai/fastmcp @caracalai/verify @caracalai/revocation

Use caracalai-revocation-redis (Python) or the Redis store in @caracalai/revocation (TypeScript) for shared production revocation. The in-memory store is only for local development and tests.

from caracalai_fastmcp import CaracalAuth
from caracalai_revocation import InMemoryRevocationStore
auth = CaracalAuth(
issuer="https://sts.pipernet.example",
audience="resource://pipernet",
expected_zone_id="0195f2a9-1b22-7c3d-9e4f-5a6b7c8d9e0f",
required_scopes=["mcp:tool:call"],
required_targets=["resource://pipernet"],
revocations=InMemoryRevocationStore(),
require_session=True,
)
async def startup():
await auth.warmup()
from caracalai_fastmcp import CaracalAuthError
async def handle_tool_call(token: str, payload: dict):
try:
claims = await auth(token)
except CaracalAuthError as err:
return {"error": err.code, "error_description": err.description, "error_hint": err.hint}
return {
"subject": claims.sub,
"result": await run_tool(payload),
}

Wire this check into the FastMCP auth or request hook used by your server. The important boundary is that the mandate is verified before the tool handler performs work.

import { extractBearer, verifyFastMcpToken, FastMcpAuthError } from '@caracalai/fastmcp'
import { createMandateVerifier } from '@caracalai/verify'
import { InMemoryRevocationStore } from '@caracalai/revocation'
const verifier = createMandateVerifier({
issuer: 'https://sts.pipernet.example',
audience: 'resource://pipernet',
zoneId: '0195f2a9-1b22-7c3d-9e4f-5a6b7c8d9e0f',
revocations: new InMemoryRevocationStore(),
})
async function handleToolCall(authorization: string, payload: unknown) {
const token = extractBearer(authorization)
if (!token) return { error: 'missing_token' }
try {
const context = await verifyFastMcpToken(token, verifier, {
requiredScopes: ['mcp:tool:call'],
requiredTargets: ['resource://pipernet'],
requireSession: true,
})
return { subject: context.sub, result: await runTool(payload) }
} catch (err) {
if (err instanceof FastMcpAuthError) return { error: err.code }
throw err
}
}

The boundary is the same in both languages: verify the mandate before the tool handler performs work.

TestExpected result
Missing bearer tokenmissing_token or framework 401.
Wrong audienceinvalid_token.
Missing scopeinsufficient_scope.
Revoked sessionsession_revoked.
Mandate without a Session with require_session=True / requireSession: truesession_required.

Expected result: the tool handler runs only for a mandate matching issuer, audience, zone, target, scope, Session requirements, and current revocation state.

For exact call signatures, use FastMCP Adapter reference.

Use Protect an MCP Server only when the framework hook cannot use this dedicated adapter.