MCP tools you wrote: search_notes · read_note · called by Claude Code in a real session →
build-mcp-server · real session

The two tools you wrote, called by Claude Code on your behalf.

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.

SDK: @modelcontextprotocol/sdk v1.29 (MIT) Transport: stdio Code: ~120 LoC, one file Mode: 100% local · zero LLM in the server · zero API key

The workspace left = your project · centre = real Claude Code session · bottom-left = the config you paste into the IDE

~/projects/notes-mcp your project
notes-mcp/
├── package.json # @modelcontextprotocol/sdk + zod
├── tsconfig.json
├── src/
│ └── server.ts # your two setRequestHandler(…) calls
└── notes/ # whatever .md files you want to expose
    ├── 2026-04-30-q1-retro.md
    ├── 2026-05-18-onboarding-research.md
    └── 2026-05-21-launch-plan.md
claude -p --mcp-config ./claude_desktop_config.json --strict-mcp-config --allowedTools mcp__notes__search_notes mcp__notes__read_note --output-format stream-json --verbose real run · exit 0
User
Find the May note about the Notebook Pro launch plan and tell me the three KPIs it tracks (with the numeric thresholds).
Discovery · harness
ToolSearch { "query": "notes search list note", "max_results": 10 }
Tool result
Claude Code’s built-in tool registry loads the schemas of the two MCP tools your server advertised in tools/list. This event is the harness, not your server — your MCP tools have not been called yet.
Tool call · 1/2 · your MCP server
mcp__notes__search_notes { "query": "Notebook Pro launch plan" }
Tool result
{
  "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 …"
    }
  ]
}
Tool call · 2/2 · your MCP server
mcp__notes__read_note { "path": "2026-05-21-launch-plan.md" }
Tool result
# 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)
Assistant · final answer
The note is 2026-05-21 — Notebook Pro launch plan (Owner: Maya). It tracks three KPIs through July 31, 2026:
  1. KPI-1 — Conversion rate (personal → Pro): ≥ 6% within 14 days of first Pro prompt (baseline last quarter was 3.7%).
  2. KPI-2 — D7 active retention of Pro converters: ≥ 60%.
  3. KPI-3 — Support contact rate: ≤ 4 Pro tickets per 100 conversions (anything above 8 forces a pause review before broad availability).
~/.claude.json  ·  claude_desktop_config.json paste once

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"
      }
    }
  }
}

src/server.ts the file you came to write — two setRequestHandler(…) calls, one StdioServerTransport

src/server.ts official SDK · no wrapper
 1 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