# Introduction Source: https://picomcp.github.io/picomcp/docs/ > What picomcp is, what it is not, and the ten lines that explain it. picomcp is a TypeScript framework for building [Model Context Protocol](https://modelcontextprotocol.io) servers. An MCP server gives an AI application (Claude, Cursor, VS Code, your own agent) **tools** it can call, **resources** it can read and **prompts** it can offer to the user. ```ts import { pico } from "picomcp"; import { z } from "zod"; pico({ name: "weather" }) .tool("forecast", { description: "Get the forecast for a city.", input: z.object({ city: z.string() }), run: ({ city }) => getForecast(city), }) .serve(); ``` That is a complete server. Run it with `node server.ts` and connect it to any MCP client. ## Why picomcp - **Small.** About 4 kB gzipped, zero dependencies. The whole library fits in your head and in a code review. - **Typed from your schema.** Pass a Zod, Valibot or ArkType schema. Your handler gets typed arguments, the client gets a JSON Schema, invalid calls get a readable error. - **Runs anywhere.** Node 22+, Bun, Deno, Cloudflare Workers and every runtime with `fetch`. No Node built-ins in the core. - **Two transports, one server.** stdio for desktop clients, Streamable HTTP for remote servers. Switch with one option. - **Testable without a client.** `server.call("forecast", { city: "Lyon" })` runs a tool in-process, fully typed. - **Verified against the official SDK.** The test suite drives picomcp with `@modelcontextprotocol/sdk` over both transports. ## What picomcp is not picomcp implements the server side of MCP and nothing else. It does not include a client, an agent loop or an LLM SDK. It favours the stateless request/response half of the protocol, which is what most servers need. Server-initiated features such as sampling and elicitation are not supported yet: see [Protocol coverage](https://picomcp.github.io/picomcp/docs/protocol) for the exact list. ## Next steps - [Quickstart](https://picomcp.github.io/picomcp/docs/quickstart): build and connect a server in five minutes. - [Tools](https://picomcp.github.io/picomcp/docs/tools): everything a tool can do. - [For AI agents](https://picomcp.github.io/picomcp/docs/agents): give your coding agent the docs in one URL. --- # Quickstart Source: https://picomcp.github.io/picomcp/docs/quickstart > Scaffold, test and connect your first MCP server in five minutes. You need Node 22.18 or later (it runs TypeScript files directly), Bun, or Deno. ## Scaffold a project ```sh npx picomcp init my-server cd my-server npm install ``` This creates a ready-to-run server: | File | Purpose | | --- | --- | | `server.ts` | Your server definition: tools, resources, prompts | | `main.ts` | Starts the server over stdio | | `server.test.ts` | A test that calls a tool in-process | Prefer to start from nothing? Install the two packages and write `server.ts` yourself: ```sh npm install picomcp zod ``` ## Write a tool Open `server.ts`: ```ts import { pico } from "picomcp"; import { z } from "zod"; export const server = pico({ name: "my-server", version: "0.1.0" }) .tool("greet", { description: "Greets someone by name.", input: z.object({ name: z.string().describe("Who to greet") }), run: ({ name }) => `Hello, ${name}!`, }); ``` - `description` is what the model reads to decide when to call the tool. Write it for the model. - `input` is any [Standard Schema](https://picomcp.github.io/picomcp/docs/schemas). `name` is typed as `string` inside `run`. - `run` returns a string here. It can also return objects, content blocks or throw: see [Tools](https://picomcp.github.io/picomcp/docs/tools). ## Test it ```ts import assert from "node:assert/strict"; import { test } from "node:test"; import { server } from "./server.ts"; test("greet says hello", async () => { const result = await server.call("greet", { name: "Ada" }); assert.deepEqual(result.content, [{ type: "text", text: "Hello, Ada!" }]); }); ``` ```sh npm test ``` `server.call` is type-checked: a typo in the tool name or a missing argument fails to compile. ## Connect it For Claude Code: ```sh claude mcp add my-server -- node /absolute/path/to/my-server/main.ts ``` Then ask Claude to greet someone. For Claude Desktop, Cursor, VS Code and others, see [Connect a client](https://picomcp.github.io/picomcp/docs/clients). ## Inspect it The MCP Inspector lets you call tools by hand and see the raw messages: ```sh npx @modelcontextprotocol/inspector node main.ts ``` --- # Connect a client Source: https://picomcp.github.io/picomcp/docs/clients > Configuration for Claude Code, Claude Desktop, Cursor, VS Code and the MCP Inspector. Every MCP client starts a local server the same way: it runs a command and talks to it over stdio. Remote servers are reached by URL over Streamable HTTP. Use absolute paths in every config below. ## Claude Code ```sh # local, over stdio claude mcp add weather -- node /absolute/path/main.ts # remote, over HTTP claude mcp add --transport http weather https://example.com/mcp ``` Add `--scope project` to write the server to `.mcp.json` and share it with your team. ## Claude Desktop Edit `claude_desktop_config.json` (Settings → Developer → Edit Config), then restart the app: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] } } } ``` ## Cursor Create `.cursor/mcp.json` in your project, or `~/.cursor/mcp.json` for every project: ```json { "mcpServers": { "weather": { "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "url": "https://example.com/mcp" } } } ``` ## VS Code Create `.vscode/mcp.json`: ```json { "servers": { "weather": { "type": "stdio", "command": "node", "args": ["/absolute/path/main.ts"] }, "remote-weather": { "type": "http", "url": "https://example.com/mcp" } } } ``` ## MCP Inspector ```sh npx @modelcontextprotocol/inspector node /absolute/path/main.ts ``` To inspect an HTTP server, start it with `serve({ http: 3000 })`, open the Inspector, pick **Streamable HTTP** and enter `http://127.0.0.1:3000/mcp`. The Inspector runs on localhost, which picomcp allows by default. ## Bun and Deno Replace `node` with your runtime: ```json { "command": "bun", "args": ["/absolute/path/main.ts"] } ``` ```json { "command": "deno", "args": ["run", "--allow-net", "/absolute/path/main.ts"] } ``` ## Troubleshooting **The client shows no tools.** Run the command from the config yourself. If it exits or prints an error, the client sees the same thing. **"Unexpected token" or JSON errors in the client log.** Something wrote to stdout. picomcp redirects `console.log` to stderr while serving over stdio, but other libraries may write to `process.stdout` directly. **Node says it cannot run `.ts` files.** Upgrade to Node 22.18 or later, or run the server with Bun. --- # Tools Source: https://picomcp.github.io/picomcp/docs/tools > Define tools, validate input, return text, objects or media, report errors and handle cancellation. A tool is a function the model can call. Register one with `server.tool(name, config)`: ```ts server.tool("search_issues", { title: "Search issues", description: "Search open GitHub issues in a repository. Use it before filing a new issue.", input: z.object({ repo: z.string().describe("owner/name"), query: z.string(), limit: z.number().int().max(50).default(10), }), annotations: { readOnlyHint: true, openWorldHint: true }, run: async ({ repo, query, limit }, { signal }) => { const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:${repo}`)}&per_page=${limit}`, { signal }); const { items } = await res.json(); return items.map((i: { title: string; html_url: string }) => `${i.title} — ${i.html_url}`).join("\n"); }, }); ``` ## Configuration | Option | Type | Description | | --- | --- | --- | | `description` | `string` | What the tool does and when to use it. The model reads this. | | `title` | `string` | Display name for clients. | | `input` | Standard Schema | Arguments schema. Omit it for tools without arguments. | | `output` | Standard Schema | Structured result schema. See [Structured output](#structured-output). | | `annotations` | `ToolAnnotations` | Hints for clients: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. | | `run` | `(input, ctx) => value` | The handler. Can be async. | Tool names must be 1 to 128 characters long and use only letters, digits, `_`, `.` and `-`. Registering an invalid or duplicate name throws immediately, so mistakes show up when the server starts, not when a model calls the tool. ## Input validation picomcp validates arguments with your schema before calling `run`. Defaults and transforms apply, so `limit` above is always a number inside `run`. When validation fails, the tool is not called. The client receives a tool error that names the bad field, so the model can fix its call and retry: ```text Invalid arguments for tool "search_issues": limit: Too big: expected number to be <=50 ``` ## Return values Return whatever is natural. picomcp turns it into an MCP result: | `run` returns | The client receives | | --- | --- | | `string` | One text block | | an object, array or number | Its JSON as text. Objects are also sent as `structuredContent`. | | a content block, or an array of them | Those blocks as they are | | `{ content: [...] }` | The result as it is, for full control | | `undefined` | An empty result | Build content blocks with the helpers: ```ts import { audio, image, link, text } from "picomcp"; run: async () => [ text("Here is the chart:"), image(await renderPng(), "image/png"), // Uint8Array or base64 string link("file:///reports/q3.csv", "q3.csv", { mimeType: "text/csv" }), ]; ``` ## Structured output Declare an `output` schema when clients or code consume the result, not only the model: ```ts server.tool("get_price", { input: z.object({ sku: z.string() }), output: z.object({ sku: z.string(), cents: z.number().int(), currency: z.literal("EUR") }), run: async ({ sku }) => ({ sku, cents: await priceOf(sku), currency: "EUR" }), }); ``` picomcp then: 1. advertises the schema as `outputSchema` in `tools/list`, 2. type-checks the return value of `run` against it, 3. validates the value at runtime and sends it as `structuredContent`, with a JSON text copy for older clients. If the value does not match, the client receives a tool error instead of malformed data. ## Errors Throw from `run`. The error message becomes a tool error (`isError: true`) that the model can read and react to: ```ts run: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new Error(`No user with id ${id}. Call list_users to see valid ids.`); return user; }; ``` Write error messages for the model: say what went wrong and what to do next. Tool errors are part of a normal conversation. Protocol errors are not: use `McpError` only when the request itself is wrong, for example when the user lacks access: ```ts import { McpError } from "picomcp"; throw new McpError(-32001, "Not authorised for this workspace"); ``` ## Cancellation The second argument of `run` carries an `AbortSignal`. It aborts when the client sends `notifications/cancelled`, typically because the user pressed stop. Pass it to `fetch` and other APIs that accept one: ```ts run: async ({ url }, { signal }) => { const res = await fetch(url, { signal }); return res.text(); }; ``` Once a request is cancelled, picomcp sends no response for it, as the protocol requires. ## Metadata `ctx.meta` holds the `_meta` object the client sent with the request, if any. Clients use it for tracing and progress tokens. ## Writing good tools - **One job per tool.** `create_invoice` and `send_invoice` beat `invoice(action)`. - **Describe arguments.** `.describe()` on each field ends up in the JSON Schema the model sees. - **Return less.** Models pay for every token. Return the fields that matter, not the whole API response. - **Annotate honestly.** `readOnlyHint: true` lets clients skip confirmation prompts. Never set it on a tool that writes. --- # Resources Source: https://picomcp.github.io/picomcp/docs/resources > Expose files, records and live data the client can read, with typed URI templates. Resources are data the client can read and attach to a conversation: files, database rows, API responses. Tools are called by the model; resources are usually picked by the user or the application. ```ts server.resource("config://app", { description: "The app configuration", read: () => loadConfig(), }); ``` ## URI templates Put `{variables}` in the URI to serve a family of resources. The variables are typed from the string itself: ```ts server.resource("github://{owner}/{repo}/readme", { mimeType: "text/markdown", read: async ({ owner, repo }) => { // ^? { owner: string; repo: string } const res = await fetch(`https://raw.githubusercontent.com/${owner}/${repo}/HEAD/README.md`); return res.text(); }, }); ``` | Syntax | Matches | Example | | --- | --- | --- | | `{name}` | One path segment, no `/`, `?` or `#` | `users://{id}` matches `users://42` | | `{+name}` | Anything, including `/` | `files://{+path}` matches `files://src/index.ts` | Values are URL-decoded before they reach `read`. Static resources appear in `resources/list`, templates in `resources/templates/list`. ## Return values | `read` returns | Contents sent | | --- | --- | | `string` | Text, with `mimeType` or `text/plain` | | `Uint8Array` | Base64 blob, with `mimeType` or `application/octet-stream` | | an array of `{ uri, text }` or `{ uri, blob }` | Those contents as they are | | any other value | Its JSON, as `application/json` | `read` receives the context as its second argument, with the requested `uri` added: ```ts read: (params, { uri, signal }) => fetch(toHttp(uri), { signal }).then((r) => r.text()); ``` ## Options | Option | Description | | --- | --- | | `name` | Identifier shown to users. Defaults to the URI. | | `title` | Display name. | | `description` | What the resource contains. | | `mimeType` | Content type of what `read` returns. | | `annotations` | `audience`, `priority` and `lastModified` hints. | ## Not found When no resource matches a URI, picomcp answers with the protocol's `-32002 Resource not found` error. To reject a URI that matches a template but points at nothing, throw an `McpError` with the same code: ```ts import { McpError } from "picomcp"; read: async ({ id }) => { const user = await db.users.find(id); if (!user) throw new McpError(-32002, `Resource not found: users://${id}`); return user; }; ``` ## Linking from tools A tool can point the model at a resource instead of inlining it, which keeps large data out of the context until it is needed: ```ts import { link } from "picomcp"; run: async () => link("reports://2026-q3", "Q3 report", { mimeType: "application/pdf" }); ``` --- # Prompts Source: https://picomcp.github.io/picomcp/docs/prompts > Offer reusable prompt templates that users pick from the client, with validated arguments. Prompts are templates the user picks from the client, often as slash commands. They turn a few arguments into one or more messages. ```ts server.prompt("review", { title: "Code review", description: "Review a diff for bugs and style issues.", args: z.object({ diff: z.string().describe("The diff to review"), focus: z.string().optional().describe("What to pay attention to"), }), run: ({ diff, focus }) => `Review this diff${focus ? `, focusing on ${focus}` : ""}:\n\n${diff}`, }); ``` The client lists `diff` as required and `focus` as optional, with their descriptions, both derived from the schema. ## Arguments are strings MCP sends prompt arguments as strings. Use string schemas, or coerce: ```ts args: z.object({ count: z.coerce.number().int().min(1) }); ``` Invalid arguments are rejected with an `Invalid params` error before `run` is called. ## Return values | `run` returns | Messages sent | | --- | --- | | `string` | One user message | | `PromptMessage[]` | Those messages | | `{ description, messages }` | The full result | Use an array for multi-turn templates or to include media: ```ts import { image, text } from "picomcp"; run: async ({ id }) => [ { role: "user", content: text("What is wrong with this screenshot?") }, { role: "user", content: image(await screenshot(id), "image/png") }, ]; ``` --- # Schemas Source: https://picomcp.github.io/picomcp/docs/schemas > Use Zod, Valibot, ArkType or any Standard Schema library, and how JSON Schema is generated. picomcp does not ship a schema library. It accepts any library that implements [Standard Schema](https://standardschema.dev), and generates the JSON Schema that MCP clients need through [Standard JSON Schema](https://standardschema.dev/json-schema). You keep the library you already use, and picomcp stays small. ## Zod 4 Works as is: ```ts import { z } from "zod"; server.tool("forecast", { input: z.object({ city: z.string().describe("City name") }), run: ({ city }) => getForecast(city), }); ``` ## ArkType Works as is: ```ts import { type } from "arktype"; server.tool("forecast", { input: type({ city: "string" }), run: ({ city }) => getForecast(city), }); ``` ## Valibot Valibot keeps JSON Schema support in a separate package. Wrap the schema with `toStandardJsonSchema`: ```ts import * as v from "valibot"; import { toStandardJsonSchema } from "@valibot/to-json-schema"; server.tool("forecast", { input: toStandardJsonSchema(v.object({ city: v.string() })), run: ({ city }) => getForecast(city), }); ``` ## Any other library If a library validates through Standard Schema but cannot produce JSON Schema (Zod 3, for example), attach one yourself with `withJsonSchema`: ```ts import { withJsonSchema } from "picomcp"; server.tool("forecast", { input: withJsonSchema(schema, { type: "object", properties: { city: { type: "string" } }, required: ["city"], }), run: ({ city }) => getForecast(city), }); ``` Without it, registering the tool throws an error that tells you exactly this, so the problem never reaches a client. ## How types flow - `run` receives the schema's **output** type, after defaults and transforms. - `server.call()` accepts the schema's **input** type, so fields with defaults are optional in tests. - With an `output` schema, `run` must return the schema's input type. ## Tips for schemas models understand - Describe every field. Descriptions are the documentation the model reads. - Prefer enums to free text when the set of values is known. - Use defaults for optional behaviour so the model can omit arguments. - Keep objects flat. Deeply nested arguments are harder for models to fill in. --- # Transports Source: https://picomcp.github.io/picomcp/docs/transports > stdio for local clients, Streamable HTTP for remote ones, and the fetch handler that powers both. A transport carries JSON-RPC messages between the client and your server. picomcp supports the two that MCP defines. Your tools do not change between them. ## stdio ```ts await server.serve(); ``` The client starts your server as a child process and exchanges newline-delimited JSON over stdin and stdout. This is what desktop clients and coding agents use for local servers. stdout belongs to the protocol. While serving over stdio, picomcp redirects `console.log`, `console.info` and `console.debug` to stderr, so a stray log line cannot break the connection. Clients usually show stderr in their logs. ## Streamable HTTP ```ts const running = await server.serve({ http: 3000 }); console.error(`Listening on ${running.url}`); // http://127.0.0.1:3000/mcp ``` `serve({ http })` starts a server on Bun, Deno or Node, whichever is running. Options: | Option | Default | Description | | --- | --- | --- | | `port` | `3000` | Port to listen on. `0` picks a free one. | | `hostname` | `"127.0.0.1"` | Use `"0.0.0.0"` to accept connections from other machines. | | `path` | `"/mcp"` | Endpoint path. Other paths answer 404. | It returns `{ url, close() }`. ## The fetch handler `server.fetch` is a Web-standard `(Request) => Promise` function. Hand it to any runtime or framework that speaks `fetch`: ```ts Bun.serve({ port: 3000, fetch: server.fetch }); ``` ```ts Deno.serve({ port: 3000 }, server.fetch); ``` ```ts export default { fetch: server.fetch }; // Cloudflare Workers ``` `server.fetch` does not look at the path. Route requests to it yourself. More platforms in [Deploy](https://picomcp.github.io/picomcp/docs/deploy). ## How picomcp speaks HTTP picomcp runs Streamable HTTP in stateless JSON mode: - `POST` with a JSON-RPC message or batch → `200` with a JSON answer, or `202` for notifications. - `GET` and `DELETE` → `405`, which tells clients that no server-to-client stream is offered. - `OPTIONS` → CORS preflight for allowed origins. - An unsupported `MCP-Protocol-Version` header → `400`. There are no sessions to store, so any instance can answer any request. This makes picomcp fit serverless platforms, load balancers and autoscaling without extra work. ## Origins and DNS rebinding Browsers send an `Origin` header. A malicious page could use it to reach a server on your machine, so picomcp checks it: - Requests without `Origin` (CLIs, servers, desktop apps) are always allowed. - By default only `localhost`, `127.0.0.1` and `[::1]` origins are allowed, on any port. - Other origins get `403`. Allow more with the `origins` option: ```ts pico({ name: "notes", origins: ["https://claude.ai", "https://app.example.com"] }); pico({ name: "public", origins: "*" }); // any origin: only for public, unauthenticated servers ``` ## Custom transports `server.handle(message)` processes one JSON-RPC message, or a batch, and returns the response. It returns `undefined` for notifications. Use it to plug picomcp into WebSockets, message queues or anything else: ```ts socket.on("message", async (data) => { const response = await server.handle(JSON.parse(data)); if (response) socket.send(JSON.stringify(response)); }); ``` --- # Testing Source: https://picomcp.github.io/picomcp/docs/testing > Test tools in-process with full types, then test the protocol end to end. ## Call tools in-process `server.call(name, input)` runs a tool exactly as a client would, without a transport or a subprocess. Validation, defaults, output checks and error handling all apply. ```ts import { describe, expect, it } from "vitest"; import { server } from "./server.ts"; describe("forecast", () => { it("returns the forecast", async () => { const result = await server.call("forecast", { city: "Lyon" }); expect(result.isError).toBeUndefined(); expect(result.structuredContent).toMatchObject({ city: "Lyon" }); }); it("explains invalid input to the model", async () => { const result = await server.call("forecast", { city: "Lyon", days: 30 }); expect(result.isError).toBe(true); }); }); ``` It works with any test runner: Vitest, `node:test`, `bun test`, `deno test`. ## Type-checked calls When tools are registered by chaining, `call` knows their names and inputs: ```ts const server = pico({ name: "weather" }).tool("forecast", { input: z.object({ city: z.string(), days: z.number().default(3) }), run: ({ city, days }) => `${city}: ${days} days`, }); server.call("forecast", { city: "Lyon" }); // ✓ days is optional: it has a default server.call("forecast", {}); // ✗ city is missing server.call("forcast", { city: "Lyon" }); // ✗ no tool named forcast ``` If you register tools in separate statements, the type of `server` cannot track them and `call` accepts any name. Chain the calls, or reassign: `const app = server.tool(...)`. ## Test the protocol `server.handle` takes raw JSON-RPC, which is useful to check listings and schemas: ```ts const res = await server.handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }); expect(res).toMatchSnapshot(); ``` A snapshot of `tools/list` is a cheap way to notice when a schema change alters what models see. ## Test with a real client For end-to-end tests, drive the server with the official SDK client over HTTP: ```ts import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const running = await server.serve({ http: { port: 0 } }); const client = new Client({ name: "test", version: "1.0.0" }); await client.connect(new StreamableHTTPClientTransport(new URL(running.url!))); const { tools } = await client.listTools(); await client.close(); await running.close(); ``` picomcp's own test suite does exactly this, over both stdio and HTTP. --- # Deploy Source: https://picomcp.github.io/picomcp/docs/deploy > Run a remote MCP server on Cloudflare Workers, Deno Deploy, Bun, Node, Hono or Next.js, with authentication. A remote server is reached by URL over Streamable HTTP. Because picomcp is stateless, it runs on serverless platforms with no extra setup. ## Cloudflare Workers ```ts import { server } from "./server.ts"; export default { fetch(request: Request) { if (new URL(request.url).pathname !== "/mcp") return new Response("Not found", { status: 404 }); return server.fetch(request); }, }; ``` ```sh npx wrangler deploy src/worker.ts ``` ## Deno Deploy ```ts import { server } from "./server.ts"; Deno.serve((request) => server.fetch(request)); ``` ## Bun ```ts Bun.serve({ port: Number(process.env.PORT ?? 3000), routes: { "/mcp": server.fetch }, }); ``` ## Node ```ts await server.serve({ http: { port: Number(process.env.PORT ?? 3000), hostname: "0.0.0.0" } }); ``` ## Hono ```ts import { Hono } from "hono"; const app = new Hono(); app.all("/mcp", (c) => server.fetch(c.req.raw)); export default app; ``` ## Next.js ```ts // app/mcp/route.ts import { server } from "@/lib/mcp"; export const POST = server.fetch; export const GET = server.fetch; export const OPTIONS = server.fetch; ``` ## Authentication picomcp does not handle auth itself: wrap `server.fetch` and check the request before it reaches your tools. ```ts export default { async fetch(request: Request, env: { TOKEN: string }) { if (request.headers.get("authorization") !== `Bearer ${env.TOKEN}`) { return new Response("Unauthorized", { status: 401, headers: { "www-authenticate": "Bearer" } }); } return server.fetch(request); }, }; ``` Clients send the header from their config, for example: ```sh claude mcp add --transport http notes https://notes.example.com/mcp --header "Authorization: Bearer $TOKEN" ``` For OAuth, put your provider's middleware in front of `server.fetch` and follow the [MCP authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization). ## Checklist - Serve over HTTPS. - Require authentication unless the server is public and read-only. - Set `origins` to the browser origins you expect, if any. - Keep secrets in environment variables, never in tool descriptions or results. - Treat tool arguments as untrusted input: the model can be steered by content it reads. --- # API reference Source: https://picomcp.github.io/picomcp/docs/api > Every export of picomcp, with signatures. ```ts import { pico, McpError, PROTOCOL_VERSIONS, text, image, audio, link, withJsonSchema } from "picomcp"; import type { Server, ToolConfig, ResourceConfig, PromptConfig, Context, CallToolResult } from "picomcp"; ``` ## pico(info) Creates a server. ```ts function pico(info: ServerInfo): Server; interface ServerInfo { name: string; // machine-friendly name, e.g. "weather" version?: string; // defaults to "0.0.0" title?: string; // display name instructions?: string; // how the model should use the server as a whole origins?: string[] | "*"; // allowed browser origins for HTTP; defaults to localhost } ``` ## server.tool(name, config) Registers a tool and returns the server. See [Tools](https://picomcp.github.io/picomcp/docs/tools). ```ts interface ToolConfig { title?: string; description?: string; input?: I; // Standard Schema output?: O; // Standard Schema; enables structuredContent annotations?: ToolAnnotations; run(input: Infer, ctx: Context): Return | Promise; } interface ToolAnnotations { title?: string; readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; } ``` Throws a `TypeError` for an invalid or duplicate name, or a schema without JSON Schema support. ## server.resource(uri, config) Registers a resource, or a resource template when `uri` contains `{variables}`. Returns the server. See [Resources](https://picomcp.github.io/picomcp/docs/resources). ```ts interface ResourceConfig { name?: string; title?: string; description?: string; mimeType?: string; annotations?: Annotations; read(params: UriParams, ctx: Context & { uri: string }): ResourceReturn | Promise; } ``` ## server.prompt(name, config) Registers a prompt and returns the server. See [Prompts](https://picomcp.github.io/picomcp/docs/prompts). ```ts interface PromptConfig { title?: string; description?: string; args?: A; // Standard Schema of an object with string fields run(args: Infer, ctx: Context): string | PromptMessage[] | { description?: string; messages: PromptMessage[] }; } ``` ## server.call(name, input?) Runs a tool in-process and returns its `CallToolResult`. Type-checked when tools were registered by chaining. Rejects for an unknown tool name. See [Testing](https://picomcp.github.io/picomcp/docs/testing). ```ts call(name: string, input?: unknown): Promise; interface CallToolResult { content: ContentBlock[]; structuredContent?: Record; isError?: boolean; } ``` ## server.serve(options?) Starts the server over stdio, or over Streamable HTTP when `http` is set. See [Transports](https://picomcp.github.io/picomcp/docs/transports). ```ts serve(options?: { http?: number | { port?: number; hostname?: string; path?: string } }): Promise<{ url?: string; close(): Promise }>; ``` ## server.fetch(request) Web-standard Streamable HTTP handler. Bound to the server, so it can be passed around as a value. ```ts fetch(request: Request): Promise; ``` ## server.handle(message) Processes one JSON-RPC message or a batch. Returns the response, or `undefined` for notifications. ```ts handle(message: unknown): Promise; ``` ## Context The second argument of every handler. ```ts interface Context { signal: AbortSignal; // aborted when the client cancels meta: Record | undefined; // the request's _meta } ``` ## Content helpers ```ts text(text: string, annotations?: Annotations): ContentBlock; image(data: string | Uint8Array, mimeType: string): ContentBlock; audio(data: string | Uint8Array, mimeType: string): ContentBlock; link(uri: string, name: string, extra?: { title?: string; description?: string; mimeType?: string }): ContentBlock; ``` `data` is base64 or raw bytes. Bytes are encoded for you on every runtime. ## withJsonSchema(schema, jsonSchema, output?) Attaches a JSON Schema to a Standard Schema that cannot generate one. See [Schemas](https://picomcp.github.io/picomcp/docs/schemas). ```ts withJsonSchema(schema: S, jsonSchema: JsonSchema, output?: JsonSchema): StandardSchemaV1, Infer>; ``` ## McpError Throw it from a handler to answer with a JSON-RPC error instead of a result. ```ts new McpError(code: number, message: string, data?: unknown); ``` | Code | Meaning | | --- | --- | | `-32602` | Invalid params | | `-32002` | Resource not found | | `-32000` to `-32099` | Free for your own server errors | ## PROTOCOL_VERSIONS The MCP protocol versions picomcp speaks, newest first: `["2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"]`. ## Type helpers | Type | Description | | --- | --- | | `Infer` | Output type of a schema | | `InferInput` | Input type of a schema | | `UriParams<"a://{x}/{+y}">` | `{ x: string; y: string }` | | `StandardSchemaV1` | The Standard Schema interface, inlined | --- # Protocol coverage Source: https://picomcp.github.io/picomcp/docs/protocol > Exactly which parts of the MCP specification picomcp implements, and which it does not yet. picomcp targets MCP `2025-11-25` and negotiates down to `2025-06-18`, `2025-03-26` and `2024-11-05`. Every feature marked as supported is covered by tests, including end-to-end tests against the official TypeScript SDK client. ## Supported | Area | Feature | | --- | --- | | Lifecycle | `initialize` with version negotiation, `notifications/initialized`, `ping` | | Tools | `tools/list`, `tools/call`, input validation, `outputSchema` and `structuredContent`, annotations, titles | | Content | text, image, audio, resource links, embedded resources | | Resources | `resources/list`, `resources/templates/list`, `resources/read`, text and binary contents | | Prompts | `prompts/list`, `prompts/get`, arguments derived from schemas | | Utilities | `notifications/cancelled` with `AbortSignal` | | JSON-RPC | Batches, error codes, responses to client results ignored | | stdio | Newline-delimited JSON, stdout protected from stray logs | | Streamable HTTP | Stateless JSON responses, `202` for notifications, `405` for `GET`, CORS, `Origin` checks, `MCP-Protocol-Version` checks | ## Not supported yet | Feature | Why | | --- | --- | | Sampling, elicitation, roots | They need the server to send requests to the client. picomcp's stateless design does not hold a channel open for that yet. | | Logging, progress and `list_changed` notifications | Same reason: server-to-client messages. | | Resource subscriptions | Same reason. | | Completions | Planned. | | Pagination cursors | Lists are returned in full, which suits servers with up to hundreds of entries. | | Sessions (`Mcp-Session-Id`) and SSE streams | Not needed in stateless mode. | | Tasks | Experimental in the spec. | Want one of these? [Open an issue](https://github.com/picomcp/picomcp/issues) and describe your use case. Features land when they can be added without making picomcp large. --- # For AI agents Source: https://picomcp.github.io/picomcp/docs/agents > Machine-readable docs, llms.txt, Markdown pages and rules to give your coding agent. Coding agents write a lot of MCP servers. These docs are built to be read by them as well as by you. ## Machine-readable docs | URL | Contents | | --- | --- | | [`/llms.txt`](https://picomcp.github.io/picomcp/llms.txt) | Index of every page with a one-line summary, in the [llms.txt](https://llmstxt.org) format | | [`/llms-full.txt`](https://picomcp.github.io/picomcp/llms-full.txt) | The whole documentation as one Markdown file | | `/docs/.md` | Any page as Markdown. Add `.md` to its URL, e.g. [`/docs/tools.md`](https://picomcp.github.io/picomcp/docs/tools.md) | Every page also has a **Copy as Markdown** button, to paste it into a chat. These files are in English, the reference language and the one models follow best. Each translation also has its own `llms-full.txt`, for example `/fr/llms-full.txt`. ## Point your agent at the docs Paste this into your agent's instructions, for example in `AGENTS.md` or `CLAUDE.md` at the root of your project: ```md ## MCP servers This project builds MCP servers with picomcp. - Read https://picomcp.github.io/picomcp/llms-full.txt before writing picomcp code. - Define servers with `pico({ name })` and chain `.tool()`, `.resource()`, `.prompt()`. - Use Zod 4 schemas with `.describe()` on every field. - Write tool descriptions and error messages for the model: say what the tool does, when to use it, and what to do after an error. - Never write to stdout in a stdio server. Use `console.error` for logs. - Test tools with `server.call(name, input)`. Do not spawn a client in unit tests. ``` ## Give your agent a docs server The docs are also available as an MCP server, [`picomcp-docs`](https://www.npmjs.com/package/picomcp-docs), built with picomcp. It gives agents three tools, `list_docs`, `search_docs` and `read_doc`, and every page as a resource. In Claude Code: ```sh claude mcp add picomcp-docs -- npx -y picomcp-docs ``` In Cursor, Windsurf, Claude Desktop and most other clients, add it to the MCP config file (`.cursor/mcp.json` for Cursor): ```json { "mcpServers": { "picomcp-docs": { "command": "npx", "args": ["-y", "picomcp-docs"] } } } ``` The source is in [`packages/docs`](https://github.com/picomcp/picomcp/tree/main/packages/docs), about 100 lines long. ## Facts agents get wrong These are the mistakes models make most often when they write MCP servers, and how they work in picomcp: - **stdout is the protocol in stdio mode.** A `console.log` would corrupt it. picomcp redirects console output to stderr while serving over stdio, but `process.stdout.write` is still unsafe. - **Tool errors are results, not exceptions.** Throwing inside `run` produces `isError: true` with your message. The model sees it and can retry. Do not catch errors only to return `"Error: ..."` strings. - **Prompt arguments are strings.** Use `z.coerce.number()` for numeric prompt arguments. - **JSON Schema comes from the schema library.** Zod 4 and ArkType need nothing. Valibot needs `toStandardJsonSchema`. Others need `withJsonSchema`. - **`server.fetch` ignores the path.** Route `/mcp` to it yourself, or use `serve({ http })`, which only answers on `/mcp`. - **Typed `server.call` requires chaining.** `pico().tool().tool()` keeps the types. Separate statements lose them. ## Contributing with an agent The repository has an [`AGENTS.md`](https://github.com/picomcp/picomcp/blob/main/AGENTS.md) with the rules for working on picomcp itself: the size budget, the test commands and the design constraints.