Rebasely Beta: data may be purged

examples/MCP-tools Public

Markdown · 48 lines · 3.3 KiB · at 2d86268

---id: tool-designtype: conceptowner: justincomponent: toolsdescribes:  - src/tools/**  - src/server.tsreviewed: 2026-10-13---# Tool designA tool is one module in `src/tools/` that exports the result of `defineTool` (`src/tools/types.ts`). It has no idea MCP exists: `src/server.ts` registers every tool in `TOOLS` with the SDK's `McpServer`, and that is the only place the two meet.## What a tool has- **name**: snake_case, a verb and an object where one fits (`convert_units`, `text_stats`). Models see the name first, and in a long tool list it is often all they skim.- **title**: a short human label for client UIs.- **description**: written for the model, not for us. It says what the tool does, when to reach for it ("whenever the user needs an exact figure"), and the limits that matter (units must share a quantity, US customary volumes). If a model would otherwise guess, the description tells it not to.- **input**: a zod object. Every field has a `.describe()` with an example value. Defaults live in the schema (`words_per_minute` defaults to 238), so they show up in `tools/list` and the tool never sees a missing value.- **output**: a zod object too. The SDK publishes it as `outputSchema` and checks every result against it before it leaves the server, so a tool cannot drift from its contract quietly.- **run**: a pure function from parsed input and a `ToolContext` (just `now` today) to output. No I/O, no clock reads, no globals that change. That is what makes the unit tests one line each.## Lenient in, strict outInputs accept what a model is likely to send: `pounds`, `°F` and `Kilometres` all resolve in `convert_units`; `tokyo` resolves to `Asia/Tokyo` in `time_in_zone`. Outputs are always canonical: the unit symbol, the IANA name, ISO 8601 in UTC. A model that echoes the output back as input gets the same answer.We stop being lenient where guessing would be wrong. `time_in_zone` refuses an instant without an offset, because the Worker would read it in its own zone, and refuses a city that matches more than one zone.## ResultsA successful call returns `structuredContent` and the same object as JSON in a text block, as the spec recommends for clients that do not read structured content yet. Numbers are rounded to twelve significant digits so float noise (`0.30000000000000004`) never reaches the model.## FailuresA tool throws `ToolError` (`src/tools/errors.ts`) for anything the caller can fix. The server turns it into a result with `isError: true` and the message as text; see [[0002-tool-errors-are-results]]. Write the message as advice: what was wrong, then what would work.```Unknown time zone "Mars/Olympus_Mons". Use an IANA name such as Europe/Paris, Asia/Tokyo or America/New_York.```Arguments that fail the input schema never reach `run`; the SDK answers them with an `isError` result too. Any other exception is a bug: the server logs it and tells the model that retrying the same input will not help.## AnnotationsEvery tool is registered with `readOnlyHint`, `idempotentHint` and `openWorldHint: false`. Clients may use these to skip a confirmation prompt. If a tool ever writes something or calls out to the network, it must not inherit them; change `register` in `src/server.ts` to take them from the tool.To add one, follow [[adding-a-tool]].