Overview
Your collections, routes and access rules already describe the app. These pages project that description into an MCP server an agent can call and an OpenAPI document a person can read.
There is no second model to maintain here. You add a module, name what an agent may reach, and run codegen. Both surfaces read the same files the admin and the typed client read.
| Page | What it covers |
|---|---|
| MCP | The Model Context Protocol server from @questpie/mcp, and what becomes a tool. |
| MCP over OAuth 2.1 | How an outside agent signs in as a real person, and what its scopes reach. |
| OpenAPI and Scalar | The OpenAPI 3.1 document and the Scalar reference UI from @questpie/openapi. |
Two surfaces, two jobs
OpenAPI describes. It emits a document covering your collections, globals, routes, auth and search. The document decides nothing. Listing a path grants no access to it.
MCP executes. An agent calls a tool and the app performs the operation. That
call runs the same .access() rules your REST traffic runs.
Turn both on
The starters already register openApiModule. MCP is opt-in, so add it
yourself.
import { adminModule } from "@questpie/admin/modules/admin";
import { mcpModule } from "@questpie/mcp/modules/mcp";
import { openApiModule } from "@questpie/openapi";
export default [adminModule, mcpModule, openApiModule] as const;Then say what an agent may reach. Name the collection and name each operation.
import { mcpConfig } from "@questpie/mcp";
export default mcpConfig({
crud: {
collections: {
posts: { operations: { list: true, get: true } },
},
},
});questpie generateWhat you now serve
Three endpoints, under your handler's base path, /api in the starters.
| Endpoint | What it is |
|---|---|
POST /api/mcp | The MCP endpoint. It answers 401 without a signed-in caller. |
GET /api/openapi.json | The OpenAPI 3.1 document, cached and served with an ETag. |
GET /api/docs | The Scalar reference, built from that document. |
An agent connecting as a user who may read posts sees two tools,
collections.posts.list and collections.posts.get. It sees no other
collection, and it cannot create, update or delete a post.
Exposure and permission are separate
config/mcp.ts decides which tools exist. .access() decides whether a call
succeeds. Both have to pass.
Nothing is exposed until you list it
The tool catalog starts empty. A collection you never named contributes no
tools, whatever its access rules say. Naming the collection is not enough
either. Each operation has to be switched on under operations. That is why
posts: true on its own still yields nothing.
The rules you already wrote still run
Every tool call re-checks the collection's .access() rules for the calling
session. So does the tool list. A user who may not read posts never sees
collections.posts.list in the first place.
OAuth can only subtract
An external agent authenticates over OAuth 2.1 and carries consented scopes.
The scope gate runs beside the access rules, never instead of them. The
effective permission is scopes ∩ RBAC, so a scope cannot widen what that user
is allowed to do.
stdio is the exception
The stdio transport demands explicit authority. Set stdio.trustedMaintenance
and the process runs as system, which skips the access checks entirely. See
MCP.
What QUESTPIE does not ship
There is no agent runtime here. Provider choice, approvals and what an agent is allowed to decide belong to the app that owns those concepts. QUESTPIE gives you the pieces underneath: MCP tools, jobs, a queue and a sandbox for untrusted code.
Next
MCP covers the surface itself. Every config key, custom tools, and the transports. Access control sits under all of it, because every tool call ends there.