# MCP Tool Template Zod-schema-based tool for MCP servers returning `Result`. ## Template ```typescript import { Result, ValidationError, NotFoundError } from "@outfitter/contracts"; import { z } from "zod"; // ============================================================================ // Input Schema // ============================================================================ const InputSchema = z.object({ // Always use .describe() for AI understanding query: z.string().min(1).describe("The search term to look for"), // Provide defaults where sensible limit: z.number().int().positive().default(10) .describe("Maximum number of results to return"), // Use enums for fixed choices sortBy: z.enum(["name", "date", "relevance"]).default("relevance") .describe("Field to sort results by"), // Mark optional fields explicitly tags: z.array(z.string()).optional() .describe("Filter results by these tags"), // Boolean options includeArchived: z.boolean().default(false) .describe("Whether to include archived items"), }); // ============================================================================ // Output Type // ============================================================================ interface SearchResult { id: string; title: string; score: number; } interface Output { results: SearchResult[]; total: number; hasMore: boolean; } // ============================================================================ // Error Types // ============================================================================ type ToolErrors = ValidationError | NotFoundError; // ============================================================================ // Tool Definition // ============================================================================ export const searchTool = { name: "search_items", description: `Search for items in the database. Use this tool when the user wants to: - Find items by keyword - Search for specific content - List items matching criteria Returns matching items with relevance scores.`, inputSchema: InputSchema, handler: async ( input: z.infer ): Promise> => { // Business logic const results = await performSearch({ query: input.query, limit: input.limit, sortBy: input.sortBy, tags: input.tags, includeArchived: input.includeArchived, }); return Result.ok({ results, total: results.length, hasMore: results.length === input.limit, }); }, }; // ============================================================================ // Helper Functions // ============================================================================ async function performSearch(options: { query: string; limit: number; sortBy: string; tags?: string[]; includeArchived: boolean; }): Promise { // Implementation return []; } ``` ## Registration ```typescript import { createMcpServer } from "@outfitter/mcp"; import { searchTool } from "./tools/search.js"; import { createTool } from "./tools/create.js"; const server = createMcpServer({ name: "my-server", version: "0.1.0", description: "MCP server for item management", }); // Register tools server.registerTool(searchTool); server.registerTool(createTool); // Start server server.start(); ``` ## Checklist - [ ] Every schema field has `.describe()` for AI understanding - [ ] Sensible defaults with `.default()` where appropriate - [ ] Description explains WHEN to use the tool - [ ] Returns `Result`, not raw values - [ ] Error types from taxonomy ## Patterns ### Tool with Context ```typescript export const myTool = { name: "my_tool", description: "Tool with context access", inputSchema: InputSchema, handler: async (input, ctx) => { // Log invocation ctx.logger.debug("Tool invoked", { input }); // Call handler const result = await myHandler(input, ctx); // Log outcome if (result.isErr()) { ctx.logger.error("Tool failed", { error: result.error }); } return result; }, }; ``` ### CRUD Tool Set ```typescript // List export const listItemsTool = { name: "list_items", description: "List all items. Use when user wants to see available items.", inputSchema: z.object({ limit: z.number().default(20).describe("Max items to return"), offset: z.number().default(0).describe("Number of items to skip"), }), handler: async (input) => { /* ... */ }, }; // Get export const getItemTool = { name: "get_item", description: "Get a specific item by ID. Use when user asks about a specific item.", inputSchema: z.object({ id: z.string().describe("The item ID to retrieve"), }), handler: async (input) => { /* ... */ }, }; // Create export const createItemTool = { name: "create_item", description: "Create a new item. Use when user wants to add something new.", inputSchema: z.object({ name: z.string().describe("Name for the new item"), description: z.string().optional().describe("Optional description"), }), handler: async (input) => { /* ... */ }, }; // Update export const updateItemTool = { name: "update_item", description: "Update an existing item. Use when user wants to modify an item.", inputSchema: z.object({ id: z.string().describe("The item ID to update"), name: z.string().optional().describe("New name"), description: z.string().optional().describe("New description"), }), handler: async (input) => { /* ... */ }, }; // Delete export const deleteItemTool = { name: "delete_item", description: "Delete an item. Use when user wants to remove an item.", inputSchema: z.object({ id: z.string().describe("The item ID to delete"), }), handler: async (input) => { /* ... */ }, }; ``` ### Deferred Loading ```typescript server.registerDeferredTool({ name: "heavy_analysis", description: "Perform heavy analysis (loads on demand)", load: async () => { // Only loaded when tool is first called const { analysisTool } = await import("./tools/analysis.js"); return analysisTool; }, }); ``` ## Test Template ```typescript import { describe, test, expect } from "bun:test"; import { createMcpHarness } from "@outfitter/testing"; import { searchTool } from "../tools/search.js"; const harness = createMcpHarness(searchTool); describe("search_items", () => { test("returns results for valid query", async () => { const result = await harness.invoke({ query: "test", limit: 5, }); expect(result.isOk()).toBe(true); expect(result.value.results).toBeInstanceOf(Array); }); test("uses default limit", async () => { const result = await harness.invoke({ query: "test" }); expect(result.isOk()).toBe(true); // Default limit is 10 }); test("returns ValidationError for empty query", async () => { const result = await harness.invoke({ query: "" }); expect(result.isErr()).toBe(true); expect(result.error._tag).toBe("ValidationError"); }); }); ``` ## Schema Best Practices ```typescript // DO: Use descriptive field names query: z.string().describe("Search query") // DON'T: Cryptic names q: z.string() // DO: Provide sensible defaults limit: z.number().default(10) // DON'T: Require every field limit: z.number() // DO: Use enums for fixed options format: z.enum(["json", "csv", "xml"]) // DON'T: Accept any string format: z.string() // DO: Validate ranges page: z.number().int().min(1).max(100) // DON'T: Accept any number page: z.number() ```