The exact deliverable: a server you wrote with the official @modelcontextprotocol/sdk (no wrapper), registered with two real tools, plugged into an AI IDE via one JSON config block, and called by the AI assistant in a real session to answer a question whose answer only exists in your local notes. The transcript below is a real claude -p --output-format stream-json --verbose run against this server — tool name + arguments + a snippet of the returned JSON are all real. The first tool_use line (ToolSearch) is the Claude Code harness loading the schemas of your MCP tools; the two MCP calls that actually answer the question come immediately after.
tools/list. This event is the harness, not your server — your MCP tools have not been called yet.{
"root": ".../example/notes-mcp/notes",
"query": "Notebook Pro launch",
"matches": 1,
"hits": [
{
"path": "2026-05-21-launch-plan.md",
"snippet": "# 2026-05-21 — Notebook Pro launch plan Owner: Maya …"
}
]
}
# 2026-05-21 — Notebook Pro launch plan Owner: Maya · Reviewers: Sam, Priya · Status: in flight ## KPIs (tracked through 2026-07-31) - **KPI-1 — Conversion rate:** personal → Pro **≥ 6%** within 14 days of first Pro prompt. Baseline last quarter: 3.7%. - **KPI-2 — D7 active retention** of Pro converters **≥ 60%** … - **KPI-3 — Support contact rate:** Pro tickets per 100 conversions **≤ 4** … (full markdown returned, truncated here for display)
Add this block to your IDE’s MCP config, restart the IDE, and the two tools appear in the tool list:
{
"mcpServers": {
"notes": {
"command": "npx",
"args": [
"-y", "tsx",
"/abs/path/notes-mcp/src/server.ts"
],
"env": {
"NOTES_DIR": "/abs/path/your/notes"
}
}
}
}
setRequestHandler(…) calls, one StdioServerTransport1 import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 import { 4 CallToolRequestSchema, 5 ListToolsRequestSchema, 6 } from "@modelcontextprotocol/sdk/types.js"; 7 import { z } from "zod"; 8 9 const server = new Server( 10 { name: "notes-mcp", version: "0.1.0" }, 11 { capabilities: { tools: {} } }, 12 ); 13 14 // 1. Advertise the menu the IDE shows the model. 15 server.setRequestHandler(ListToolsRequestSchema, async () => ({ 16 tools: [ 17 { name: "search_notes", description: "Substring search across …", inputSchema: {…} }, 18 { name: "read_note", description: "Full text of one note …", inputSchema: {…} }, 19 ], 20 })); 21 22 // 2. Run the order when the model picks something off the menu. 23 server.setRequestHandler(CallToolRequestSchema, async (request) => { 24 const { name, arguments: args } = request.params; 25 if (name === "search_notes") return await runSearch(searchInput.parse(args)); 26 if (name === "read_note") return await runRead(readInput.parse(args)); 27 return { isError: true, content: [{ type: "text", text: `Unknown tool: ${name}` }] }; 28 }); 29 30 await server.connect(new StdioServerTransport()); // stdio · no network listener