Askr
Guides

Build an MCP Server

Use build an mcp server with the current published Askr packages.

  • @askrjs/askr0.0.59

How to use build an mcp server

Register typed tools on one MCP server and select the HTTP, SSE, session, or stdio adapter only at deployment time.

  1. Import the published @askrjs/askr entrypoint.
  2. Keep build an mcp server configuration next to the component, route, or server composition root that owns it.
  3. Handle pending, unavailable, cancellation, and failure states, then verify the production path.
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.