| @@ -1,9 +1,146 @@ |
| | # mcp-tools |
| | |
| | -A small MCP server with exact, read-only helpers for models, running as a Cloudflare Worker. Tools: `convert_units`, `time_in_zone` and `text_stats`. It serves MCP over Streamable HTTP at `/mcp`. |
| | +A small [Model Context Protocol](https://modelcontextprotocol.io) server on Cloudflare Workers. It gives models three exact, read-only tools for things they tend to get wrong: unit arithmetic, the current time somewhere else, and counting words. |
| | |
| | +The server speaks Streamable HTTP at `/mcp`, holds no sessions, and counts tool calls per day in a KV namespace. |
| | + |
| | +## Tools |
| | + |
| | +### `convert_units` |
| | + |
| | +Converts between units of length, mass, volume (US customary) and temperature. Units can be symbols or names: `km`, `kilometres`, `°F`, `pounds`. |
| | + |
| | +```json |
| | +{ "value": 26.2, "from": "miles", "to": "km" } |
| | +``` |
| | + |
| | +```json |
| | +{ "quantity": "length", "value": 26.2, "from": "mi", "result": 42.1648128, "to": "km" } |
| | +``` |
| | + |
| | +Converting between quantities, an unknown unit, or a temperature below absolute zero returns an error result that says what to send instead. |
| | + |
| | +### `time_in_zone` |
| | + |
| | +The local date, time, weekday and UTC offset in an IANA time zone, now or at a given instant. A city that names one zone (`tokyo`, `new york`) works too. |
| | + |
| | +```json |
| | +{ "zone": "tokyo", "at": "2026-10-14T17:00:00Z" } |
| | +``` |
| | + |
| | +```json |
| | +{ |
| | + "zone": "Asia/Tokyo", |
| | + "instant": "2026-10-14T17:00:00.000Z", |
| | + "date": "2026-10-15", |
| | + "time": "02:00:00", |
| | + "weekday": "Thursday", |
| | + "utc_offset": "+09:00", |
| | + "utc_offset_minutes": 540, |
| | + "formatted": "Thursday, October 15, 2026 at 2:00:00 AM GMT+9" |
| | +} |
| | +``` |
| | + |
| | +Leave out `at` for the current time. An instant without `Z` or an offset is refused, since it would be read in the server's own zone. |
| | + |
| | +### `text_stats` |
| | + |
| | +Characters (as a reader sees them, so an emoji counts once), words, sentences, paragraphs and reading time. |
| | + |
| | +```json |
| | +{ "text": "MCP gives models tools. This server has three of them." } |
| | +``` |
| | + |
| | +```json |
| | +{ |
| | + "characters": 54, |
| | + "characters_excluding_spaces": 45, |
| | + "words": 10, |
| | + "sentences": 2, |
| | + "paragraphs": 1, |
| | + "reading_time_seconds": 3, |
| | + "reading_time": "under a minute" |
| | +} |
| | +``` |
| | + |
| | +`words_per_minute` sets the reading speed (default 238). |
| | + |
| | +## Connecting a client |
| | + |
| | +The endpoint is `http://localhost:8787/mcp` locally, or `https://mcp-tools.<your subdomain>.workers.dev/mcp` once deployed. No auth. |
| | + |
| | +**MCP Inspector.** Run `npx @modelcontextprotocol/inspector`, choose the Streamable HTTP transport, and enter the URL. |
| | + |
| | +**Claude Code.** |
| | + |
| | ```sh |
| | +claude mcp add --transport http mcp-tools http://localhost:8787/mcp |
| | +``` |
| | + |
| | +**Claude Desktop.** Desktop starts local servers over stdio, so bridge with `mcp-remote` in `claude_desktop_config.json`: |
| | + |
| | +```json |
| | +{ |
| | + "mcpServers": { |
| | + "mcp-tools": { "command": "npx", "args": ["-y", "mcp-remote", "http://localhost:8787/mcp"] } |
| | + } |
| | +} |
| | +``` |
| | + |
| | +**curl.** Each message is its own POST, and `Accept` must list both types: |
| | + |
| | +```sh |
| | +curl -s http://localhost:8787/mcp \ |
| | + -H 'content-type: application/json' \ |
| | + -H 'accept: application/json, text/event-stream' \ |
| | + -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"convert_units","arguments":{"value":100,"from":"C","to":"F"}}}' |
| | +``` |
| | + |
| | +`GET /stats?days=7` returns calls and errors per tool for the last seven UTC days. |
| | + |
| | +## Running it |
| | + |
| | +Node 24 or later. |
| | + |
| | +```sh |
| | npm install |
| | -npm test |
| | -npm run dev # http://localhost:8787/mcp |
| | +npm test # node:test, no Workers runtime needed |
| | +npm run typecheck |
| | +npm run dev # wrangler dev on http://localhost:8787 |
| | ``` |
| | + |
| | +`npx wrangler deploy` deploys it. The `USAGE` namespace in `wrangler.jsonc` has no id, so the first deploy creates it. |
| | + |
| | +## Design notes |
| | + |
| | +- **Stateless.** Every POST gets a fresh `McpServer` and a `WebStandardStreamableHTTPServerTransport` with no session id and JSON responses. Any isolate can answer any request; nothing needs a Durable Object. |
| | +- **Tools are pure.** Each tool in `src/tools/` is a name, a description written for the model, zod input and output schemas, and a function from input to output. `src/server.ts` is the only file that knows about MCP. |
| | +- **Errors are results.** A tool throws `ToolError` with advice ("Use an IANA name such as Europe/Paris"), and the server returns it with `isError: true` so the model can read it and try again. |
| | +- **Counting stays off the request path.** Usage writes go through `ctx.waitUntil`, after the response. The counts are approximate, which is fine for seeing what gets used. |
| | + |
| | +The longer versions are in the knowledge base under `.rebasely/kb/`, and the component map is `.rebasely/architecture/system.yaml`. |
| | + |
| | +## Layout |
| | + |
| | +``` |
| | +src/ |
| | + index.ts Worker entry |
| | + worker.ts routes: /mcp, /stats, CORS |
| | + server.ts createServer(): registers the tools with McpServer |
| | + usage.ts per-day counters in KV |
| | + tools/ |
| | + convert-units.ts |
| | + time-in-zone.ts |
| | + text-stats.ts |
| | + errors.ts ToolError |
| | + types.ts defineTool and the Tool type |
| | +test/ node:test suites, plus an in-memory client and KV fakes |
| | +.rebasely/ |
| | + architecture/ components, views and flows |
| | + kb/ concepts, decisions and guides |
| | + workflows/ci.yml typecheck and tests on every push |
| | +``` |
| | + |
| | +## License |
| | + |
| | +MIT |