# MCP

`.mcp()` serves the router you already have as an [MCP](https://modelcontextprotocol.io) 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:

```bash
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:

```ts
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](/guide/auth#api-keys):

```
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

```ts
.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

- [API keys](/guide/auth#api-keys) — issue tokens for headless clients
- [Procedures](/guide/procedures) — write the procedures you expose
