[ ABORT TO HUD ]
SEQ. 1
SEQ. 2
SEQ. 3
SEQ. 4

Registering Tools

🛠️ Build Your First Server12 min90 BASE XP

Your Server's First Superpower

Tools are the most commonly used MCP capability. Let's build a practical tool that searches a local notes directory.

Complete Tool Implementation

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { readdir, readFile } from "fs/promises";
import { join } from "path";

const server = new McpServer({
  name: "notes-server",
  version: "1.0.0"
});

// Tool 1: Search notes by keyword
server.tool(
  "search_notes",
  "Search all markdown notes for a keyword. Returns matching filenames and snippets.",
  {
    query: z.string().describe("The keyword to search for"),
    maxResults: z.number().optional().default(5).describe("Max results to return")
  },
  async ({ query, maxResults }) => {
    const notesDir = process.env.NOTES_DIR || "./notes";
    const files = await readdir(notesDir);
    const matches: string[] = [];

    for (const file of files) {
      if (!file.endsWith(".md")) continue;
      const content = await readFile(join(notesDir, file), "utf-8");
      if (content.toLowerCase().includes(query.toLowerCase())) {
        const lines = content.split("\n");
        const matchLine = lines.find(l =>
          l.toLowerCase().includes(query.toLowerCase())
        );
        matches.push(`**${file}**: ${matchLine?.trim() || "(match in body)"}`);
      }
      if (matches.length >= maxResults) break;
    }

    if (matches.length === 0) {
      return {
        content: [{ type: "text", text: `No notes found matching "${query}".` }]
      };
    }

    return {
      content: [{ type: "text", text: matches.join("\n") }]
    };
  }
);

// Tool 2: Create a new note
server.tool(
  "create_note",
  "Create a new markdown note file with the given title and content.",
  {
    title: z.string().describe("Note title (used as filename)"),
    body: z.string().describe("Markdown content of the note")
  },
  async ({ title, body }) => {
    const notesDir = process.env.NOTES_DIR || "./notes";
    const filename = title.toLowerCase().replace(/\s+/g, "-") + ".md";
    const fullPath = join(notesDir, filename);

    try {
      await writeFile(fullPath, `# ${title}\n\n${body}\n`);
      return {
        content: [{ type: "text", text: `✅ Note created: ${filename}` }]
      };
    } catch (e: any) {
      return {
        isError: true,
        content: [{ type: "text", text: `Failed to create note: ${e.message}` }]
      };
    }
  }
);

Tool Registration Patterns

PatternWhen to UseExample
Simple ToolSingle action, no side effectssearch_notes — reads data
Mutating ToolCreates, updates, or deletes datacreate_note — writes files
Async ToolCalls external APIs with latencyfetch_weather — HTTP request
Streaming ToolReturns progress updatesrun_migration — long process
💡 Key Insight: Always include .describe() on every Zod field. The LLM reads these descriptions to decide what values to pass. A missing description means the LLM guesses — and it will guess wrong.
SYNAPSE VERIFICATION
QUERY 1 // 3
What must every tool handler function return?
A raw string
A Promise<void>
An object with a 'content' array containing text/image blocks
A JSON object
Watch: 139x Rust Speedup
Registering Tools | Build Your First Server — MCP Academy