Skip to content

.mcp()

Serve your procedures as MCP tools with .mcp(), opt in per procedure, and authenticate agents with API keys.

Updated View as Markdown

.mcp() serves the router you already have as an MCP server over the Streamable HTTP transport, so Claude, IDEs, and agents can call your procedures as tools. One router definition covers four surfaces: RPC, REST, OpenAPI, and MCP.

Before you begin

Install the two optional peer dependencies:

bun add orpc-mcp @orpc/zod

Expose a procedure

Exposure is opt-in per procedure. Tag one with the mcp meta helper and it appears. Leave it untagged and MCP clients never see it:

import { mcp, Outer } from "@outerjs/server";
import { z } from "zod";

new Outer({ db: pglite() })
  .schema(v1_0)
  .procedure(
    "post.search",
    (base) =>
      base
        .meta(mcp.tool({ description: "Search posts by title" }))
        .input(z.object({ q: z.string() }))
        .handler(({ input, context }) =>
          context.db.query.post.findMany({ where: { title: { contains: input.q } } }),
        ),
    { permission: "authenticated" },
  )
  .mcp()
  .build();

mcp.tool(), mcp.resource(), and mcp.prompt() map a procedure to the matching MCP capability. Your input schema becomes the tool’s JSON Schema, so the description and argument names an agent sees come straight from the procedure.

Because exposure is opt-in, the reserved _admin namespace — which can read, update, and delete any row in any table — and every file.* route stay invisible whether or not you remember to exclude them.

Dots are not legal in MCP tool names, so a dot-namespaced procedure is exposed with underscores. post.search is listed and called as post_search.

Authenticate agents

The MCP endpoint resolves a session exactly the way /rpc/** does, so permissions, context.user, and ownerColumn behave identically. An untagged permission is not a loophole: { permission: "authenticated" } on a tool rejects an anonymous MCP client.

Browsers can authenticate with a cookie. Headless clients use an API key:

POST https://your-app.com/mcp
Authorization: Bearer <key>

The key authenticates as the user it belongs to, so an agent has exactly that user’s permissions and no more.

Clients that support only the MCP OAuth discovery flow, rather than a configurable header, need Better Auth’s separate mcp plugin. That is not wired into .mcp() today.

Options

.mcp({ path: "/mcp", instructions: "Search and summarise posts." })
Field Default Description
enabled true when called Set false to gate it behind an env flag without removing the call.
path /mcp Where the endpoint is mounted.
serverInfo instance name + latest schema version Identity reported to clients during initialize.
instructions Free-form guidance returned during initialize.
enableDnsRebindingProtection false Reject Origin and Host values outside the allowlists.
allowedOrigins / allowedHosts Exact-match allowlists used when protection is on.

Turn on enableDnsRebindingProtection when the endpoint is reachable from a browser. A missing Origin header always passes, for non-browser clients, but a present one outside the allowlist is rejected with 403.

Next steps

Navigation

Type to search…

↑↓ navigate↵ selectEsc close