MCP Security
Exposing a route as an MCP tool is like exposing it to another HTTP client: the same route, called with the caller's credentials. Your existing authentication, permission checks and rate limits do the work. If you wouldn't make a route reachable from outside, don't make it a tool.
What protects a tool call
At the MCP endpoint, every request:
- Host and origin. A request for a host outside
allowedHostsgets 403. So does one from a browser origin outsideallowedOrigins; clients that send noOrigin(Claude Code, Cursor, the SDK) are unaffected. Together they stop web pages from reaching a server through DNS rebinding. - Authentication. With
auth, every request (initialize,tools/list, eachtools/call) is checked, so a revoked token stops working at once.authenticatealso checks the token's audience against this server's resource URL (Authentication). - Scopes. A tool's
scopesrefuse a call from a principal without them, before it runs. - Which tools exist for this caller.
toolFilterhides tools a caller may not use, from the list and from calls (Multi-Tenancy). - Load. In session mode,
maxSessionscaps open sessions (503 beyond it) and idle sessions close aftersessionIdleTimeoutMs.toolTimeoutMscaps how long a call runs.
In the route, every tool call:
- runs through the route's full pipeline: middleware, contributors (your auth user loader, tenant resolution), guards, role checks, rate limits, validation and error mapping;
- carries the caller's
Authorizationand cookie headers (forwardHeaders), so the route authenticates the real caller, not the MCP server; - reaches the route with the caller's host, so tenant resolution sees the right tenant.
By construction:
- Explicit mode is the default. Only routes you mark (
@McpToolor anexposeWhenflag) become tools.hideWhenkeeps a route out whatever else says, including controllers from plugins. - Arguments are validated by the route, against its
params,queryandbodyschemas, like any request. A custom tool's arguments are validated against itsinputSchemabefore the handler runs.
What stays yours
- Permissions. Annotations (
destructiveHint) are hints for clients, not enforcement, andtoolFilterdecides what's offered, not what's allowed. Keep the permission check in the route. - Approval for risky actions.
ctx.elicitcan ask the user to confirm inside a custom tool (Asking the user). For actions that need sign-off inside your system, route them through your own approval flow and return a typed error (McpToolError('approval_pending', …)) the model can report. - The authorization server. The adapter is the resource server: it validates tokens and serves metadata. Issuing them (sign-in, consent, refresh, revocation) is your identity provider's job.
- Rate limits per caller. Route-level rate limits apply to route tools. Custom tools need their own, or a check in the handler.
- Isolation. Tools run in your app's process with its permissions. For untrusted code, use an OS-level sandbox.
Not yet supported
- Prompts, and resource subscriptions (
resources/subscribe). - Sampling (asking the client's model to generate text mid-call).