Registering Tools — Build Your First Server
← Back to Dashboard
1. Project Scaffolding2. Registering Tools3. Adding Resources & Prompts4. Connecting & Publishing
Registering Tools
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, writeFile } from "fs/promises";
import { join } from "path";
const server = new McpServer({
name: "notes-server",
version: "1.0.0"
});
// Tool 1: Search notes by keyword (using registerTool — replaces deprecated server.tool)
server.registerTool(
"search_notes",
{
title: "Search Notes",
description: "Search all markdown notes for a keyword. Returns matching filenames and snippets.",
inputSchema: z.object({
query: z.string().describe("The keyword to search for"),
maxResults: z.number().optional().default(5).describe("Max results to return")
}),
annotations: { readOnlyHint: true }
},
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.registerTool(
"create_note",
{
title: "Create Note",
description: "Create a new markdown note file with the given title and content.",
inputSchema: z.object({
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
| Pattern | When to Use | Example |
|---|---|---|
| Simple Tool | Single action, no side effects | search_notes - reads data |
| Mutating Tool | Creates, updates, or deletes data | create_note - writes files |
| Async Tool | Calls external APIs with latency | fetch_weather - HTTP request |
| Streaming Tool | Returns progress updates | run_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.⌨ HANDS-ON LABVerify Server TypeScript SDK Imports
⭐ +125 XPInspect the tool registration syntax using the official TypeScript MCP SDK and Zod.
1Verify @modelcontextprotocol/sdk server module can be imported.
OBJECTIVE 1 / 1 — type "hint" if stuck
🧪 Knowledge Check
Press 1-4 to select1 of 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