# MCP (/docs/agents/mcp)

---
title: MCP
description: mcpModule turns your app into a Model Context Protocol server. Collections, globals and opted-in routes become tools an agent calls, under the access rules you already wrote.
kind: guide
package: "@questpie/mcp"
---

An agent connects and asks what it can do. What should it see?

You answer that in one file. Nothing is reachable until you name it there, so
the empty answer is the safe one.

## Add the module

```ts title="src/questpie/server/modules.ts"
import { mcpModule } from "@questpie/mcp/modules/mcp";

export default [mcpModule];
```

```bash
questpie generate
```

The endpoint is live at `/mcp`, under your handler's base path, so `/api/mcp`
in the starters. It speaks Streamable HTTP and keeps no session between calls.

<Callout type="warn" title="A fresh endpoint has no tools">
	Ask it for a tool list and you get an empty array. Every collection, global
	and route stays hidden until `config/mcp.ts` names it. Adding the module
	exposes nothing on its own.
</Callout>

## Name what an agent may reach

Create `config/mcp.ts` and default-export `mcpConfig(...)`. Name each entity,
then name each operation on it.

```ts title="src/questpie/server/config/mcp.ts"
import { mcpConfig } from "@questpie/mcp";

export default mcpConfig({
	name: "acme-cms",
	version: "1.0.0",
	crud: {
		collections: {
			posts: {
				operations: { list: true, get: true, create: true, update: true },
				fields: { exclude: ["internalNotes"] },
			},
		},
		globals: {
			siteSettings: { operations: { get: true } },
		},
	},
	routes: {
		routes: { "reports/generate": { operations: { execute: true } } },
	},
	resources: {
		collections: { posts: true },
	},
});
```

Re-run codegen and reconnect. The agent now sees six tools.

| Tool                       | What it calls                               |
| -------------------------- | ------------------------------------------- |
| `collections.posts.list`   | `find()`, with `where`, `sort` and `limit`  |
| `collections.posts.get`    | `findOne()` by id                           |
| `collections.posts.create` | `create()`, input `{ data }`                |
| `collections.posts.update` | `updateById()`, input `{ id, data }`        |
| `globals.siteSettings.get` | `get()`                                     |
| `routes.reports.generate`  | the route handler, input is its `.schema()` |

`count` and `delete` never appear, because the config did not ask for them.
`internalNotes` is gone from every argument and every result. `posts` also gets
a schema resource, readable at `questpie://schema/collections/posts`.

The route needs a second opt-in. Naming it here is half of it, and the route
file itself must carry `meta.mcp.expose: true`. See
[Metadata](/docs/code/routes/metadata).

<Callout
	type="info"
	title="Re-run codegen after the module or a tool file changes"
>
	`modules.ts` and files under `mcp-tools/` both change what codegen finds. Run
	`questpie generate`, or keep `questpie dev` running. Editing `config/mcp.ts`
	or a handler body needs no regen.
</Callout>

## Four gates, every call

A collection or global tool is reachable only when all four of these pass.

| Gate         | What it checks                                             | Where you set it              |
| ------------ | ---------------------------------------------------------- | ----------------------------- |
| Catalog      | the entity is named and the operation is not `false`       | `config/mcp.ts`               |
| MCP rule     | the operation's value, when you wrote a function           | `config/mcp.ts`               |
| OAuth scope  | an OAuth caller holds the scope this operation needs       | the user's consent screen     |
| Access rules | the entity's own `.access()`, run as the connecting caller | the collection or global file |

All four run twice. Once when the server builds its tool list, so a tool the
caller cannot use never appears. Once again when the tool is called.

A route tool swaps the fourth gate for the route's own `.access()` rule. A
custom tool has no entity behind it, so its own `access` rule and the scope
gate are the whole check.

## Rules, not just booleans

An operation takes a function as well as a boolean. The function is the MCP
gate, and it runs before your access rules.

```ts
collections: {
	posts: {
		operations: {
			list: true,
			delete: ({ session }) => session?.user?.role === "admin",
		},
	},
}
```

The MCP config decides what is reachable through MCP at all. [Access
control](/docs/schema/access-control) decides what this caller may do. Neither
one can widen the other.

## Tools of your own

CRUD is not everything. Put an `mcpTool()` under `mcp-tools/` and it becomes a
tool with a typed input and the full app context.

```ts title="src/questpie/server/mcp-tools/recalculate-stats.ts"
import { mcpTool } from "@questpie/mcp";
import { z } from "zod";

export default mcpTool("recalculate-stats", {
	description: "Recompute cached site statistics.",
	access: ({ session }) => session?.user?.role === "admin",
	scopes: false,
	inputSchema: z.object({ since: z.string().optional() }),
}).handler(async ({ input, ctx }) => {
	const updated = await ctx.services.stats.recompute(input.since);
	return {
		structuredContent: { updated },
		content: [{ type: "text", text: `Updated ${updated} rows.` }],
	};
});
```

`access` and `scopes` are both required. Leave either one out and the tool is
dropped from the catalog rather than exposed.

Run `questpie add mcp-tool recalculate-stats` to write the file and regenerate
in one step.

## Where each topic lives

| Topic                                                 | Page                                                 |
| ----------------------------------------------------- | ---------------------------------------------------- |
| Every `McpConfig` field, and what each default is     | [Configuration](/docs/agents/mcp/config)             |
| The `mcpTool` builder and its handler arguments       | [Custom tools](/docs/agents/mcp/custom-tools)        |
| The HTTP route, stdio, and servers you mount yourself | [Transports](/docs/agents/mcp/transports)            |
| Size caps, timeouts, concurrency and error codes      | [Limits and errors](/docs/agents/mcp/limits)         |
| Turning one route into a tool with `meta.mcp`         | [Metadata](/docs/code/routes/metadata)               |
| The rules the fourth gate runs                        | [Access control](/docs/schema/access-control)        |
| Doing all of it end to end, against a real client     | [Connect an AI agent](/docs/guides/connect-an-agent) |

## Next

**[MCP over OAuth 2.1](/docs/agents/mcp-oauth)** is how an agent on someone
else's machine gets a principal at all. Without one the HTTP endpoint answers
`401` and points at your discovery metadata.
