# Configuration (/docs/agents/mcp/config)

---
title: Configuration
description: config/mcp.ts is the whole catalog. It names every collection, global, route and resource an agent may reach, and the rule and OAuth scope on each one.
kind: reference
package: "@questpie/mcp"
---

`mcpConfig(...)` takes one object. Every key is optional, and every one of them
opens something up rather than locking it down.

| Key         | Type                 | With no value           |
| ----------- | -------------------- | ----------------------- |
| `name`      | `string`             | `"questpie"`            |
| `version`   | `string`             | `"0.0.0"`               |
| `crud`      | `McpCrudConfig`      | no collection or global |
| `routes`    | `McpRoutesConfig`    | no route                |
| `resources` | `McpResourcesConfig` | no schema resource      |
| `http`      | `McpHttpConfig`      | app origin only         |
| `stdio`     | `McpStdioConfig`     | no trusted maintenance  |
| `execution` | `McpExecutionConfig` | the shipped limits      |

`name` and `version` are what the client shows for the server itself.

## crud

`crud` holds two maps and one number.

| Key           | Type                              | Default |
| ------------- | --------------------------------- | ------- |
| `collections` | `Record<string, McpEntityPolicy>` | `{}`    |
| `globals`     | `Record<string, McpEntityPolicy>` | `{}`    |
| `maxLimit`    | `number`                          | `100`   |

`maxLimit` is the ceiling the `list` tool clamps its `limit` to. A caller who
asks for more gets `maxLimit` rows.

A key that names nothing in your app is ignored. A collection you never name
has no tools.

## The entity policy

Every entry in `collections`, `globals` and `routes` is the same shape.

| Key                  | Type                                           | What it does                                           |
| -------------------- | ---------------------------------------------- | ------------------------------------------------------ |
| `expose`             | `boolean`                                      | `false` hides the entity. An entry defaults to `true`. |
| `operations`         | `Record<string, boolean \| McpAccessRule>`     | Which operations exist, and the rule on each.          |
| `requiredScopes`     | `string \| string[] \| false`                  | OAuth scopes for every operation on this entity.       |
| `operationScopes`    | `Record<string, McpRequiredScopes>`            | The same, per operation.                               |
| `fields`             | `{ include?: string[]; exclude?: string[] }`   | Column allow and deny lists.                           |
| `description`        | `string`                                       | Replaces the generated tool description.               |
| `workload`           | `{ capabilities: string[]; handoff?: string }` | Authority facts for a remote workload.                 |
| `operationWorkloads` | `Record<string, …>`                            | The same, per operation.                               |

Two shorthands exist. `posts: false` hides the collection. `posts: true` sets
`expose` and nothing else. It names no operation, so it still produces no
tools. Only `operations` creates a tool.

<Callout type="warn" title="`expose: true` is not enough on its own">
	The catalog keeps an operation when its value is present and is not `false`.
	An entity with an empty `operations` map drops out entirely. Write the
	operation names you want.
</Callout>

### Operation names

| Entity     | Operations             | Kind   |
| ---------- | ---------------------- | ------ |
| Collection | `list`, `count`, `get` | read   |
| Collection | `create`, `update`     | write  |
| Collection | `delete`               | delete |
| Global     | `get`                  | read   |
| Global     | `update`               | write  |
| Route      | `execute`              | invoke |

The kind sets the tool's MCP annotations and its default OAuth scope. Reads get
`readOnlyHint: true`. `delete` gets `destructiveHint: true`. `update` and
`delete` get `idempotentHint: true`. A route tool carries only the annotations
you wrote in `meta.mcp.annotations`.

### Rules on an operation

An operation value is `true`, `false`, or a function. The function receives one
argument.

```ts
interface McpAccessRuleContext {
	transport: "http" | "stdio" | "workload";
	accessMode: "user" | "system";
	session?: RequestContext["session"];
	scopes?: string[]; // only an OAuth caller carries these
	ctx: AppContext & Partial<RequestContext>;
}
```

Return `true` to allow. The entity's own `.access()` rules still run after it,
unless the caller is in `system` mode. See [Access
control](/docs/schema/access-control).

### Scopes

An OAuth caller must hold the scope an operation requires. The requirement
resolves in this order, and the first one that is set wins.

1. `operationScopes[name]`
2. `requiredScopes`
3. the default `<resource>:<name>:<verb>`

So `collections.posts.list` defaults to `collections:posts:read`, and
`globals.siteSettings.update` to `globals:siteSettings:write`. A route defaults
to `routes:<key>:invoke`. Setting any level to `false` requires no scope at
all.

A coarse `<resource>:read` or `<resource>:write` umbrella also satisfies a
granular scope with the same resource and verb. There is no umbrella for
`delete` or `invoke`. A caller who is not an OAuth caller holds no scopes and
skips this gate. The whole flow is on [MCP over OAuth
2.1](/docs/agents/mcp-oauth).

### Fields

`fields` filters the columns a tool exposes. It applies to the tool's input
schema, its results, and the entity's schema resource.

```ts
posts: {
	operations: { list: true, get: true },
	fields: { exclude: ["internalNotes", "authorEmail"] },
}
```

`include` is an allow list and `exclude` is a deny list. A column named in both
is dropped. Leave `include` unset and your own fields stay, plus `id`,
`createdAt`, `updatedAt`, `deletedAt` and `_status`. Set `include` and only the
columns you listed survive, those five included.

<Callout type="info" title="This is the way to hide a column">
	A CRUD tool mirrors your columns. Trusting the model not to ask for a secret
	is not protection. Name the column in `fields.exclude` and it is gone from
	arguments, results and schemas.
</Callout>

## routes

`routes.routes` is a map keyed by route key. The key is the file path under
`routes/` without the extension or method suffix, so
`routes/reports/generate.post.ts` is `reports/generate` and
`routes/reports/[id].get.ts` is `reports/[id]`.

```ts
routes: {
	routes: {
		"reports/generate": { operations: { execute: true } },
		"reports/[id]": { operations: { execute: true } },
	},
}
```

A route needs two opt-ins. `meta.mcp.expose === true` on the route file, and an
`execute` operation here. Miss either one and there is no tool. Only JSON
routes qualify, because a raw route has no schema to advertise. See
[Metadata](/docs/code/routes/metadata).

The tool is named `meta.mcp.name` when you set one. Otherwise it is `routes.`
plus the key with slashes, colons and brackets turned into dots, so
`reports/[id]` becomes `routes.reports.id`.

<Callout type="warn" title="Two tools cannot share a name">
	A custom tool and a route that resolve to the same name make the server throw
	`Duplicate MCP tool name` when it starts. Rename one of them.
</Callout>

## resources

Resources publish JSON schemas an agent can read without calling a tool. They
are opt-in per entity, by name.

```ts
resources: {
	collections: { posts: true },
	globals: { siteSettings: true },
	routes: { "reports/generate": true },
}
```

| Map                     | Published at                                               |
| ----------------------- | ---------------------------------------------------------- |
| `resources.collections` | `questpie://schema/collections` and `…/collections/{name}` |
| `resources.globals`     | `questpie://schema/globals` and `…/globals/{name}`         |
| `resources.routes`      | `questpie://schema/routes` and `…/routes/{key}`            |

A collection resource needs one of `list`, `count` or `get` released, and a
global resource needs `get`. Every read runs the same rule, scope and access
gates the tools do. The server advertises the resources capability only when at
least one is published.

## http, stdio and execution

These three tune the server rather than the catalog. `http` and `stdio` are on
[Transports](/docs/agents/mcp/transports), and `execution` is on [Limits and
errors](/docs/agents/mcp/limits).

One field is worth naming here. `http.accessMode` exists on the type, and the
bundled route never reads it. HTTP always runs as `user`.

## Two configs at once

`createMcpServer(app, { config })` takes a config of its own. It does not
deep-merge entity entries. An entry it names replaces your file's entry for
that name outright, and names it leaves alone survive. That server also gets
its own snapshot, so it never changes what OAuth advertises.

## TypeScript

```ts
import type {
	McpConfig,
	McpCrudConfig,
	McpRoutesConfig,
	McpResourcesConfig,
	McpEntityPolicy,
	McpAccessRule,
	McpAccessRuleContext,
	McpRequiredScopes,
	McpTransportKind, // "http" | "stdio" | "workload"
	McpAccessMode, // "user" | "system"
} from "@questpie/mcp";
```

`mcpConfig(...)` returns its argument unchanged, so your default export keeps
the exact type you wrote.
