Build an MCP Server
Build an MCP Server: a worked guide from route registry through to a production build.
Example
Register typed tools on one MCP server and select the HTTP, SSE, session, or stdio adapter only at deployment time.
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) }],
})
);Goal and architecture
createMcpServer<Dependencies>({ name, version }) from @askrjs/server/mcp builds an MCP server you register tools, resources, and prompts on; registerMcpRoutes(router, path, mcp, options) mounts it on an HTTP router for the streamable-HTTP transport, or connectMcpStdio() from @askrjs/node/mcp runs it over stdio for local/CLI clients. Both transports share the same McpServer instance, so the tool and resource definitions don't change based on how the server is deployed.
Implementation
.tool(name, { input, output }, handler) takes an @askrjs/schema ObjectSchema for input and an optional output schema; the handler receives an McpContext<Dependencies> with dependencies, auth, signal, and helpers like progress() and log() for long-running work. .resource(uri, options, handler) and .resourceTemplate(template, options, handler) cover static and parameterized resource reads, and .prompt(name, { arguments }, handler) returns the message list a client requests. Call notifyToolsChanged() etc. when your available tool set changes at runtime so clients that support list-change notifications stay in sync.
Failure states
A tool handler should set isError: true on its McpToolResult for a caught, expected failure rather than throwing raw — clients render that distinctly from a transport-level error. If a tool requires elevated access, set auth in its McpPrimitiveOptions so McpContext.auth is checked before the handler runs, not inside the handler body after work has already started. For HTTP transport, registerMcpRoutes's allowedOrigins/allowedHosts options are your actual security boundary against browser-based cross-origin calls — leaving them unset is not a safe default for anything beyond local development.
Verification
Connect a real MCP client over both transports you support and confirm tool discovery lists exactly the tools you registered, with the schemas you expect. Exercise signal cancellation on a long-running tool and confirm it actually aborts the underlying work rather than just returning early while a fetch keeps running server-side. If you're using sessionStore for stateful HTTP sessions, kill a session mid-request and confirm terminateSession() behavior matches what your client expects on reconnect.