Skip to content

route

route() wires one HTTP/MCP endpoint. We recommend keeping handler inline so the language server can infer input and return types.

typescript
import {route, err} from 'callspec';
import {predicates as p} from 'runtyp';
const product = p.object({
id: p.string(),
name: p.string(),
priceCents: p.number(),
});
const products = [
{id: 'sku-1', name: 'Widget', priceCents: 999},
{id: 'sku-2', name: 'Gadget', priceCents: 1299},
];
export const getProductById = route({
input: p.object({id: p.string()}),
output: product,
meta: {summary: 'Get product by ID', tags: ['catalog']},
auth: 'none',
mcp: true,
handler: async (input, _ctx) => {
const found = products.find((item) => item.id === input.id);
if (!found) return err.NOT_FOUND();
return found;
},
});
typescript
route({ input?, output?, meta, handler, … })
OptionDefaultDescription
inputp.object({})Request body pred. Omit when there are no fields (extra keys rejected).
outputvoidSuccessful response pred. Omit when a successful handler returns void or undefined.
metaDocs/OpenAPI/MCP labels — see Route meta below.
handlerYour route logic — see Handler below.
errorsDomain errors — see Builtin errors.
auth'bearer'Who can call the route — see Auth and scope.
scope'public'Who sees it in contracts — see Auth and scope.
mcpMCP tool exposure — see MCP below.

Your route implementation. Callspec validates input before the handler runs. The output pred types the success return and defines the contract for docs and codegen — it is not re-validated on the HTTP response.

The function always takes two arguments — validated input and request ctx (Request context). Return a success value, or err.* / a registered domain error for expected failures (Error handling). Bare throw becomes INTERNAL_ERROR.

With bearer auth, annotate ctx with your context type (Authentication). The wired route also exposes .handler(input, ctx) for unit tests (Unit testing).

Returns a wired route (WiredRoute) for spec({ routes }).

Omit mcp to keep the route HTTP-only. Set mcp: true (as in the example above) to list it as a tool. The tool name defaults to the route key (getProductById); the tool title is meta.summary.

Use the object form to override the tool name or pass MCP annotations through to tools/list (Callspec does not validate the keys):

typescript
export const getProductById = route({
// …
mcp: {
name: 'catalog_get_product',
annotations: {readOnlyHint: true, idempotentHint: true},
},
handler: async (input, _ctx) => { /* … */ },
});

name must be unique among tools on that mount. Any route with mcp set turns on {mount}/mcp. Connect, auth, and onCall: MCP Server.

Every route needs meta with at least summary and tags. These show up in the docs UI route list, OpenAPI operation text, and MCP tool titles.

FieldRequiredDescription
summaryyesShort label — docs sidebar, OpenAPI summary, MCP tool title.
tagsyesGrouping in the docs UI and OpenAPI tags (e.g. ['catalog'], ['users']).
descriptionnoLonger prose for OpenAPI/MCP when the summary is not enough.

Optional — only when you have a real reason to extract the function. See Handler for the contract; prefer inline handler on route().

typescript
import {route, type HandlerFor} from 'callspec';
const preds = {input, output, meta, auth: 'none'} as const;
const impl: HandlerFor<typeof preds, Ctx> = async (input, _ctx) => {
return {id: input.id, name: '…', priceCents: 0};
};
export const getProductById = route({...preds, handler: impl});
ExportPurpose
route({ …, handler })Wired route for spec; handler also on .handler for tests
HandlerFor<typeof preds, Ctx?>Explicit handler type for a separate binding

Next: spec