MCP
MCP at the Askr server boundary, with validated input and explicit failure states.
Example
Create one MCP server, register typed primitives on it, and let the selected transport supply auth, cancellation, sessions, and progress.
import { schema } from '@askrjs/schema';
import { createMcpServer } from '@askrjs/server/mcp';
export const mcp = createMcpServer({
name: 'project-tools',
version: '1.0.0',
}).tool(
'lookup-project',
{ input: schema.object({ id: schema.uuid() }) },
async (context, { id }) => ({
content: [{ type: 'text', text: await lookup(id, context.signal) }],
})
);Server model
createMcpServer(options) from @askrjs/server/mcp builds a single in-process registry of tools, resources, resource templates, and prompts, addressed by name or URI. Registration methods (tool, resource, resourceTemplate, prompt) return the same server instance, so you chain them at startup instead of scattering registration across files. The server itself is transport-agnostic: it only knows how to validate a JSON-RPC message and call handle(message, environment), leaving HTTP, SSE, and stdio delivery to separate adapters. options.pageSize (default 50, minimum 1) controls how many items tools/list, resources/list, and prompts/list return per page.
Tools and resources
server.tool(name, options, handler) registers a callable with an optional input and output schema; if you omit input, an empty object schema is used, and arguments are validated with safeParse before your handler ever runs. server.resource(uri, options, handler) binds a fixed URI to a handler that returns one McpContent or an array of them, while server.resourceTemplate(template, options, handler) matches variable segments like {id} against incoming URIs and hands the extracted variables to the handler. A tool handler's thrown errors are caught and turned into a result with isError: true rather than a JSON-RPC transport error, so callers always get a structured response for tools/call.
Transports
Two transports ship today: registerMcpRoutes from @askrjs/server/mcp mounts HTTP (plus optional SSE) routes on an existing Router, and connectMcpStdio from @askrjs/node/mcp wires the same McpServer to process.stdin/process.stdout for CLI-style clients. Both adapters do the same job: turn a raw message plus a transport-specific McpRequestEnvironment (dependencies, auth, signal, optional send) into a call to mcp.handle(...). Because the server has no transport code of its own, adding a new one is a matter of constructing that environment object correctly, not reimplementing dispatch.
Operational checklist
Before shipping, confirm pageSize matches what your clients expect for large tool/resource lists, and that every primitive requiring auth sets options.auth to an AuthRequirement rather than relying on transport-level checks. For HTTP, decide whether you need stateful: true for session-scoped push notifications, and set allowedOrigins/allowedHosts if the endpoint is reachable from a browser. For stdio, make sure diagnostics is wired to a stream your process supervisor captures, since stdio errors are written there rather than thrown.