src/tools/. There is no framework to configure, no plugin system to learn. You create a file, register it in one place, and it appears in Claude’s tool list.
This guide walks through the complete pattern: the file structure, handler signatures, registration, audit logging, and how to test your new tool.
The tool file pattern
Every tool exports four things:| Export | Type | Purpose |
|---|---|---|
name | string | Tool identifier Claude uses when calling it |
description | string | Natural language description Claude uses to decide when to invoke it |
schema | Zod shape object | Parameter definitions — descriptions become Claude’s parameter docs |
handler | async function | The implementation |
src/tools/gist_kb.js
Handler signature options
Different tools receive different parameters depending on what they need. The server passes only what’s required — this limits each tool’s access to credentials it doesn’t use.| Signature | Used by | When to use |
|---|---|---|
handler(args) | ssh_exec | No environment access needed |
handler(args, apiKey) | tailscale_status | Needs one specific API key |
handler(args, allowedDomains, env) | fetch_external, github_search | Needs domain allowlist + env tokens |
handler(args, env) | gist_kb, browser | Needs full environment object |
Step-by-step: creating a new tool
This example builds adisk_usage tool that checks free disk space on a remote host. It’s new — not already in the codebase — and demonstrates the full pattern cleanly.
Create the tool file
Create The file is entirely self-contained. It imports only what it needs (
src/tools/disk_usage.js:z from Zod, spawn from Node’s child_process), defines its own schema, and handles its own errors.Register the tool in mcp-server.js
Open The four arguments to
src/mcp-server.js and add two things: the import at the top, and a server.tool(...) call in the body.server.tool() are always: name, description, schema, and an async wrapper that calls the handler and passes the result to auditLog.Add audit logging
Every tool registration in
mcp-server.js follows this wrapper pattern:src/mcp-server.js
auditLog writes to reacher-audit.log with the tool name, timestamp, arguments, and result. Sensitive keys (authorization headers, tokens) are stripped automatically before writing. You do not need to redact values yourself — just always call auditLog in the wrapper, never inside the tool handler.Verify with tools/list
Send a You should see your tool’s name, description, and the full parameter schema in the response.
tools/list request to confirm your tool appears:Test in Claude
Start a new Claude conversation and ask something that naturally invokes your tool:
“How much disk space is left on homelab?”Claude will call
disk_usage with hostname: "homelab" and return the result. If it doesn’t pick up the tool, check that your description clearly states the tool’s purpose and when to use it — Claude reads that string to decide whether to invoke it.Zod schema reference
Theschema export is a plain object whose values are Zod validators. The MCP SDK converts it to a JSON Schema for Claude automatically.
Accessing environment variables
If your tool needs API keys or config values from.env, accept env as a second parameter and read from it:
mcp-server.js, pass env when calling the handler:
env object is process.env (or a subset of it) passed into createMCPServer(env) at startup.
Add any new environment variables to
.env.example with a comment explaining what they’re for. This keeps your setup reproducible.