Metadata
One .meta() call names a route for the OpenAPI spec and can hand the same route to an agent as an MCP tool. It changes nothing about how the route runs.
How does a route get a decent name in the API reference, and how does an agent find it? Both answers live in the same object.
One call, both consumers
import { route } from "questpie/services";
import { z } from "zod";
export default route()
.post()
.schema(
z.object({
startDate: z.string().datetime(),
endDate: z.string().datetime(),
}),
)
.meta({
title: "Get revenue stats",
description: "Revenue for a date range, completed appointments only.",
tags: ["reports"],
mcp: {
expose: true,
name: "reports.revenue",
annotations: { readOnlyHint: true },
},
})
.handler(async ({ input, collections }) => {
const { docs } = await collections.appointments.find({
where: {
status: "completed",
scheduledAt: {
gte: new Date(input.startDate),
lte: new Date(input.endDate),
},
},
limit: 10_000,
});
return { count: docs.length };
});What OpenAPI reads
The spec comes from @questpie/openapi. It reads three keys and ignores the
rest.
| You write | It becomes |
|---|---|
title | The operation summary. |
description | The operation description. |
tags | The operation's tags, registered on the spec too. |
| nothing | The route key, tagged Routes: <first>. |
Your .schema() and .outputSchema() become the request and response schemas
without any help from .meta(). A raw route gets a permissive request body and
a note saying it returns whatever it likes.
`.access(true)` shows up in the spec
An explicit true emits security: [] on the operation, which opts it out of
the spec's security scheme. A function rule, false, or no rule at all
inherits the scheme instead.
What MCP reads
The tool surface comes from @questpie/mcp. A route becomes a tool only when
all of this holds.
| Condition | Why |
|---|---|
mcp.expose === true | Opt in, one route at a time. Nothing is automatic. |
| The route is JSON mode | A raw route has no input schema to advertise. |
| The MCP module allows it | Its own policy still gates the tool. |
The tool's input schema is your .schema(). For a route with URL params, the
tool takes { params, input } instead, so an agent can fill in :id.
mcp key | Effect |
|---|---|
expose | Set true to make the tool exist. |
name | The tool name. Defaults to routes.<key>. |
title | A human label. Falls back to the outer title. |
description | What the tool does. Falls back to the outer one. |
annotations | The standard MCP hints, see below. |
annotations carries readOnlyHint, destructiveHint, idempotentHint and
openWorldHint. All four ride out to the agent as standard MCP tool hints.
None of them changes how QUESTPIE runs the route.
Your access rule still runs
Exposing a route as a tool does not bypass .access(). The MCP module checks
its own policy, then evaluates your route's rule against the agent's session
before the handler runs.
Anything else you put there
RouteMeta has an open index signature, so extra keys are allowed. They ride
through introspection untouched. Keep them serializable. Nothing in the
framework reads them, which makes them a good seam for a module of your own.
Related
Access control
One .access() call decides who may run a route. Leave it off and anyone can. That is the opposite of how a collection behaves, and it is the mistake worth checking for twice.
Types and tooling
The inference helpers, the two functions that run a route without an HTTP request, and the one that lists every route in the app.