-
Notifications
You must be signed in to change notification settings - Fork 0
Adding A Tool
The server follows a strict, repeatable shape for every tool. Use this checklist to add a new read-only MCP tool end-to-end.
src/tools/<tool-name>.ts — kebab-case filename, one tool per file. Export a single register<ToolName>(server: McpServer, chronova: ChronovaClient) function.
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { ChronovaClient } from "../lib/chronova-client.js";
import { formatToolError } from "../lib/errors.js";
import type { MyNewResponse } from "../lib/types.js";
export function registerMyNewTool(
server: McpServer,
chronova: ChronovaClient,
): void {
server.registerTool(
"my_new_tool",
{
description: "Short, action-oriented description for the agent.",
inputSchema: z.object({
// … Zod schema, all fields documented with .describe()
}),
annotations: { readOnlyHint: true }, // required — all tools are read-only
},
async (args) => {
try {
const response = await chronova.get<{ data: MyNewResponse }>(
"users/current/my-endpoint",
{ /* query params */ },
);
return {
content: [
{ type: "text" as const, text: JSON.stringify(response.data, null, 2) },
],
};
} catch (error) {
return formatToolError(error);
}
},
);
}Conventions enforced by ESLint and code review:
-
No
any(@typescript-eslint/no-explicit-any: error). -
No
ts-ignore(@typescript-eslint/ban-ts-comment: error). -
No
console.log— useconsole.errororconsole.warn(to stderr) if you must. - No empty catch blocks.
- Unused arguments must be prefixed with
_to silenceno-unused-vars. - Always set
annotations.readOnlyHint: true.
If the Chronova endpoint returns a shape the existing types don't cover, add a new interface here. The server passes JSON through unchanged, so field names should match the wire format (snake_case).
src/tools/index.ts exports registerAllTools(server, chronova). Import your new registrar and call it from registerAllTools. Because both src/server.ts (HTTP) and src/stdio.ts (stdio) call registerAllTools, a new tool only needs to be added in one place.
import { registerMyNewTool } from "./my-new-tool.js";
export function registerAllTools(server: McpServer, chronova: ChronovaClient): void {
registerGetAiInsights(server, chronova);
registerGetDeveloperContext(server, chronova);
registerGetProductivitySummary(server, chronova);
registerGetRecentActivity(server, chronova);
registerMyNewTool(server, chronova); // <- new tool
}If the Chronova endpoint needs a new error code (e.g. 402, 503 with retry semantics), extend mapHttpStatusToError in src/lib/errors.ts rather than special-casing inside the handler.
Add a describe("<tool_name>") block to tests/integration/tools.test.ts. The pattern is uniform:
describe("my_new_tool", () => {
it("should return <expected shape>", async () => {
mockApi.respond("users/current/my-endpoint", {
status: 200,
body: { data: mockMyNewResponse },
});
const result = (await callTool(mcpServer, "my_new_tool", { /* args */ })) as {
content: Array<{ type: string; text: string }>;
};
const parsed = JSON.parse(result.content[0].text);
expect(parsed.<field>).toBe(<expected>);
});
it("should return error for 401 response", async () => {
mockApi.respond("users/current/my-endpoint", {
status: 401,
body: { error: "Unauthorized" },
});
const result = (await callTool(mcpServer, "my_new_tool", { /* args */ })) as {
content: Array<{ type: string; text: string }>;
isError: boolean;
};
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Unauthorized");
});
});For a new status-code path (e.g. 503 with Retry-After), add a test in tests/integration/errors.test.ts using the same mockApi.respond(path, { status, body, headers }) pattern.
Mock fixtures are defined inline in tools.test.ts — there are no external fixture files.
tests/integration/server.test.ts asserts that tools/list returns exactly 4 tools. After adding a tool, update the count and add the new tool name to the sorted expected list in the should list exactly N tools test.
The wiki is auto-regenerated, but you should add a dedicated page under .wiki/tools/:
-
<tool-name>.md— description, parameters, response shape, error behavior, test coverage. - Update
.wiki/tools/index.md"Tools at a glance" table to include the new row. - Update
.wiki/domain/chronova-api.mdif you added a new endpoint or response type.
npm run type-check # tsc --noEmit
npm run lint # eslint .
npm test # vitest run (full integration suite)The CI pipeline (.github/workflows/test.yml) runs all three in parallel on every push and PR.