# Build an MCP Server

> Build an MCP Server: a worked guide from route registry through to a production build.

Source: [https://askrjs.com/docs/guides/build-an-mcp-server](https://askrjs.com/docs/guides/build-an-mcp-server)

Status: stable. Packages: @askrjs/askr.

**Published packages are authoritative.** Examples may lag behind a published contract. When guidance differs, verify the exports and TypeScript declarations in your installed package, then file an issue.

## Example

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

```tsx
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.

## Documentation navigation

[Previous](https://askrjs.com/docs/guides/generate-a-static-site/index.md) | [Next](https://askrjs.com/docs/guides/forms-actions-and-crud/index.md)
