The same notes server in TypeScript with the official SDK, zod schemas and the stdio transport.
WHEN TO USE IT
When your team works in Node, or you want to publish the server to npm so others can run it with npx.
Formats change. If a field stops working, check it against Anthropic's documentation.
Read it before you install it. A MCP server runs with your user account's permissions, on its own, every time it is triggered. That is what makes it useful, and why a stranger's should never be installed unread — this one included.
The package is @modelcontextprotocol/server. The older @modelcontextprotocol/sdk is the v1 line, and these imports will not resolve against it.
mkdir notes-mcp && cd notes-mcp
npm init -y && npm pkg set type=module
npm install @modelcontextprotocol/server zod
registerTool takes a name, a description with a zod inputSchema, and the handler. Log with console.error — console.log writes to stdout and breaks the protocol.
import { readdir, readFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const NOTES = process.env.NOTES_DIR ?? join(homedir(), "notes");
async function markdownFiles() {
const entries = await readdir(NOTES, { recursive: true });
return entries.filter((name) => name.endsWith(".md")).sort();
}
const server = new McpServer({ name: "notes", version: "1.0.0" });
server.registerTool(
"search_notes",
{
description: "Find notes containing the query (case-insensitive). Returns file:line: text.",
inputSchema: z.object({
query: z.string().describe("Text to look for"),
limit: z.number().int().positive().default(10),
}),
},
async ({ query, limit }) => {
const hits = [];
for (const file of await markdownFiles()) {
const lines = (await readFile(join(NOTES, file), "utf8")).split("\n");
lines.forEach((line, i) => {
if (hits.length < limit && line.toLowerCase().includes(query.toLowerCase())) {
hits.push(`${file}:${i + 1}: ${line.trim()}`);
}
});
}
return { content: [{ type: "text", text: hits.join("\n") || "No matches." }] };
},
);
await server.connect(new StdioServerTransport());
console.error("notes MCP server running on stdio");
Same shape as any stdio server: name, --, then the command.
claude mcp add --transport stdio --env NOTES_DIR=$HOME/notes notes \
-- node /absolute/path/to/notes-mcp/server.mjs
claude mcp list
MORE MCP SERVERS
The authority
Anthropic's documentation for MCP Servers
This template is a starting point. Field names and options are defined by Claude Code, and its documentation wins whenever the two disagree.
https://code.claude.com/docs/en/mcp