index.ts
Source file: 03-mcp/templates/typescript-server/src/index.ts. Copy it from the repository, or use the copy button on the code block.
#!/usr/bin/env nodeimport { readFile, writeFile } 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";
// stdout carries the MCP protocol. Log only to stderr.const log = (...args: unknown[]) => console.error("[notes]", ...args);
const NOTES_FILE = process.env.NOTES_FILE ?? join(homedir(), ".notes-mcp.json");
interface Note { title: string; body: string; created: string;}
async function loadNotes(): Promise<Note[]> { try { return JSON.parse(await readFile(NOTES_FILE, "utf8")) as Note[]; } catch (err) { if ((err as NodeJS.ErrnoException).code === "ENOENT") return []; throw err; }}
async function saveNotes(notes: Note[]): Promise<void> { await writeFile(NOTES_FILE, JSON.stringify(notes, null, 2), "utf8");}
export function searchNotes(notes: Note[], query: string, limit: number): Note[] { const q = query.toLowerCase(); return notes .filter((n) => `${n.title} ${n.body}`.toLowerCase().includes(q)) .sort((a, b) => b.created.localeCompare(a.created)) .slice(0, limit);}
const server = new McpServer({ name: "notes", version: "0.1.0" });
server.registerTool( "search_notes", { title: "Search notes", description: "Search saved notes by keyword in title or body. Returns up to `limit` matches, newest first. Use before add_note to avoid duplicates.", inputSchema: z.object({ query: z.string().min(1).describe("Keyword or phrase, case-insensitive"), limit: z.number().int().min(1).max(20).default(5).describe("Maximum results (1-20)"), }), annotations: { readOnlyHint: true, openWorldHint: false }, }, async ({ query, limit }) => { const hits = searchNotes(await loadNotes(), query, limit); const text = hits.length ? hits.map((n) => `- ${n.title} (${n.created}): ${n.body.slice(0, 200)}`).join("\n") : `No notes match "${query}". Try a broader keyword.`; return { content: [{ type: "text", text }] }; },);
server.registerTool( "add_note", { title: "Add note", description: "Save a new note. Fails if a note with the same title already exists.", inputSchema: z.object({ title: z.string().min(1).max(120).describe("Unique note title"), body: z.string().min(1).max(10_000).describe("Note content (Markdown allowed)"), }), annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, }, async ({ title, body }) => { const notes = await loadNotes(); if (notes.some((n) => n.title.toLowerCase() === title.toLowerCase())) { return { isError: true, content: [{ type: "text", text: `A note titled "${title}" already exists. Choose a different title or use search_notes to find it.` }], }; } notes.push({ title, body, created: new Date().toISOString() }); await saveNotes(notes); log(`saved note "${title}"`); return { content: [{ type: "text", text: `Saved note "${title}".` }] }; },);
server.registerResource( "all-notes", "notes://all", { title: "All notes", description: "Every saved note as JSON", mimeType: "application/json" }, async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(await loadNotes(), null, 2) }] }),);
server.registerPrompt( "weekly-review", { title: "Weekly review", description: "Summarise this week's notes", argsSchema: z.object({ focus: z.string().optional().describe("Optional topic to focus on") }), }, ({ focus }) => ({ messages: [ { role: "user", content: { type: "text", text: `Search my notes from this week and write a weekly review${focus ? ` focused on ${focus}` : ""}.`, }, }, ], }),);
const transport = new StdioServerTransport();await server.connect(transport);log(`running on stdio (NOTES_FILE=${NOTES_FILE})`);