examples/MCP-tools Public
---id: adding-a-tooltype: guideowner: justincomponent: toolsdescribes: - src/tools/**reviewed: 2026-10-13---# Adding a toolRead [[tool-design]] first; this is the checklist.## 1. Write the moduleCreate `src/tools/<name-in-kebab>.ts` and export a `defineTool(...)`:```tsimport { z } from "zod";import { ToolError } from "./errors.ts";import { defineTool } from "./types.ts";// toRoman is the usual greedy walk over M, CM, D, ... I; left out here.export const romanNumeral = defineTool({ name: "roman_numeral", title: "Roman numerals", description: "Write a whole number from 1 to 3999 as a Roman numeral. Use it instead of converting by hand.", input: z.object({ value: z.number().int().describe("The number to convert, such as 2026.") }), output: z.object({ value: z.number().int(), numeral: z.string() }), run({ value }) { if (value < 1 || value > 3999) throw new ToolError(`${value} is out of range. Roman numerals cover 1 to 3999.`); return { value, numeral: toRoman(value) }; },});```- Keep `run` pure. If it needs the time, take it from the context: `run(input, { now })`.- Put limits in the schema when the model can see them there (`.int()`, `.max()`), and throw `ToolError` for the rest.- Describe every input field with an example value.## 2. Register itAdd it to `TOOLS` in `src/server.ts`. The order there is the order of `tools/list`. Update the server's `instructions` string if the summary of what the server does has changed.## 3. Test it- `test/<name>.test.ts`: call `run` directly. Cover a normal case, an edge case and every `ToolError` message.- `test/server.test.ts`: add the name to the `tools/list` assertion, and add a call through the client if the tool has anything unusual (a default, a context value).Run `npm test` and `npm run typecheck`.## 4. Document itAdd the tool to the README with an example call and result. If it changes how tools work in general, update [[tool-design]] in the same change.