{
    "id": "bhaskar/build-your-first-mcp-server",
    "kind": "plan",
    "title": "Build Your First MCP Server (TypeScript)",
    "summary": "Give Claude new tools: a read-only MCP server over your notes folder with list, read and search, tested with the MCP Inspector.",
    "author": "Admin",
    "stars": 0,
    "url": "https://ownlyplans.com/bhaskar/build-your-first-mcp-server/",
    "stack": [
        "MCP",
        "Node.js",
        "TypeScript"
    ],
    "tools": [
        "claude-code",
        "cursor"
    ],
    "difficulty": "intermediate",
    "license": "cc-by-4.0",
    "version": "1.0",
    "overview": "MCP is how you plug your own data and tools into Claude and other AI apps. This plan builds a real, small server from an empty folder: three tools over a folder of Markdown notes, safe by design (read-only, no path escapes), tested in the official Inspector and then connected to Claude Code.\r\n\r\nOnce you have done it once, the same shape works for anything: your database, an internal API, a CLI you use every day.\r\n\r\nBefore you start: Node.js 20+, Claude Code (or Claude Desktop) and a folder with a few .md files. About 1 hour.",
    "steps": [
        {
            "title": "Create the project",
            "body": "Make an empty folder and save CLAUDE.md in it. Then run:\r\n\r\nnpm init -y\r\nnpm i @modelcontextprotocol/sdk zod\r\nnpm i -D typescript @types/node\r\n\r\nCreate a notes folder next to it with three or four short .md files to test with.",
            "prompt": "Read CLAUDE.md. Set up this folder as a TypeScript ES module project: tsconfig.json targeting ES2022 with module and moduleResolution set to Node16, outDir build, rootDir src. Add \"type\": \"module\", a \"build\" script (tsc) and a \"bin\" entry pointing at build/index.js. Do not write server code yet.",
            "done": "npm run build runs without errors (on an empty src/index.ts).",
            "files": [
                "CLAUDE.md",
                "MCP TypeScript SDK"
            ]
        },
        {
            "title": "Start a server that does nothing (yet)",
            "body": "Get the skeleton right first: a server object, a stdio transport, and logging that goes to stderr only. Printing to stdout breaks the protocol, which is the number one beginner bug.",
            "prompt": "Using the official TypeScript SDK (check its README for the current API), write src/index.ts that creates an MCP server named \"notes\" version 0.1.0 and connects it over stdio. Read NOTES_DIR from the environment and exit with a clear message on stderr if it is missing or not a folder. Log \"notes server ready\" to stderr.",
            "done": "NOTES_DIR=./notes node build/index.js starts and waits silently, with \"notes server ready\" on stderr.",
            "files": [
                "MCP TypeScript SDK"
            ]
        },
        {
            "title": "Add a safe path helper",
            "body": "Before any tool reads a file, make it impossible to read outside the notes folder. This is the security core of the server, so it gets its own function and its own test.",
            "prompt": "Add src/paths.ts with resolveNote(name: string): string. It must resolve the path inside NOTES_DIR (use realpath), only allow .md files, and throw a friendly error for anything outside NOTES_DIR (../, absolute paths, symlinks that point out). Add a tiny test script (node --test) with cases: a valid note, ../secret.md, /etc/passwd, a symlink pointing out.",
            "done": "All four test cases behave as expected.",
            "files": []
        },
        {
            "title": "Tool 1 and 2: list_notes and read_note",
            "body": "Two tools with zod input schemas and short, specific descriptions. The model decides which tool to call by reading those descriptions, so write them like documentation for a colleague.",
            "prompt": "Register two tools. list_notes (no input): returns each note file name with its first heading and size in KB. read_note ({ name: string }): returns the full text of one note using resolveNote. Return errors as text content with isError true instead of throwing. Keep each description under 20 words.",
            "done": "npm run build passes and both tools are registered.",
            "files": [
                "CLAUDE.md"
            ]
        },
        {
            "title": "Tool 3: search_notes",
            "body": "Search is what makes the server genuinely useful: the assistant can find the right note without reading all of them.",
            "prompt": "Add search_notes ({ query: string, limit?: number default 5, max 20 }). Case-insensitive search over all .md files in NOTES_DIR; return the best matches with file name, line number and a one-line snippet around each hit, ranked by number of hits. Cap total output at about 4,000 characters and say when results were cut.",
            "done": "Searching a word you know is in two notes returns both with line numbers.",
            "files": []
        },
        {
            "title": "Test by hand in the MCP Inspector",
            "body": "The Inspector lets you call your tools yourself before any AI touches them. Run:\r\n\r\nnpx @modelcontextprotocol/inspector node build/index.js\r\n\r\nSet NOTES_DIR in its environment panel, connect, open Tools and try each one, including bad input.",
            "prompt": "I am testing in the MCP Inspector. Give me a checklist of 8 calls to try (valid and invalid input for each tool, including a path escape attempt) and what the correct response looks like for each.",
            "done": "Every call on the checklist returns what it should, and the path escape is refused.",
            "files": [
                "MCP Inspector"
            ]
        },
        {
            "title": "Connect it to Claude Code",
            "body": "Add the server to Claude Code with an absolute path to the build and your notes folder. Then ask questions about your notes in plain language.",
            "prompt": "Give me the exact claude mcp add command for this server (stdio, absolute path to build/index.js, NOTES_DIR set with --env). Then suggest three natural questions I can ask Claude that should make it use each tool.",
            "done": "In Claude Code, /mcp shows notes as connected and a question about your notes triggers search_notes.",
            "files": [
                "Model Context Protocol docs"
            ]
        },
        {
            "title": "Polish and share",
            "body": "A good README is what makes other people (and future you) able to use it. Then publish it to GitHub and share the repo here on OwnlyPlans as an MCP server.",
            "prompt": "Write README.md: what the server does, the three tools with example inputs, setup for Claude Code and Claude Desktop (claude_desktop_config.json example), the safety rules, and limits. Keep it under 120 lines.",
            "done": "A friend can set it up from the README alone.",
            "files": []
        }
    ],
    "files": [
        {
            "type": "markdown",
            "name": "CLAUDE.md",
            "note": "Project root. Cursor users: save as AGENTS.md.",
            "file": "claude",
            "url": "https://ownlyplans.com/bhaskar/build-your-first-mcp-server/claude/"
        },
        {
            "type": "git",
            "name": "MCP TypeScript SDK",
            "note": "Official SDK: README has the current server API.",
            "url": "https://github.com/modelcontextprotocol/typescript-sdk"
        },
        {
            "type": "git",
            "name": "MCP Inspector",
            "note": "Official tool to call your server by hand.",
            "url": "https://github.com/modelcontextprotocol/inspector"
        },
        {
            "type": "link",
            "name": "Model Context Protocol docs",
            "note": "Concepts: tools, resources, prompts, transports.",
            "url": "https://modelcontextprotocol.io"
        }
    ],
    "install": "",
    "how_to_use": "Call get_step with step 1, do it, check \"done when\", then continue."
}