MCPPromptFoo Editorial14 minUpdated Jun 12, 2026

Building Your First MCP Server: A Practical Walkthrough

A hands-on tutorial for building a working MCP server in TypeScript in under an hour — with the debugging tips nobody puts in the README.

Why build your own MCP server

The public MCP directory covers popular apps — GitHub, Notion, Google Drive — but the moment you need your own internal API, database, or vendored data source, you're building your own server. Fortunately the protocol is small enough to hold in your head, and the official TypeScript SDK is the fastest way to get moving.

The build is straightforward. What trips most people up is the debugging loop: MCP runs over stdio by default, and the errors show up in Claude Desktop's log file, not in your terminal. This guide focuses on the parts that aren't in the README.

Scaffolding the project

Create a fresh Node project and install the SDK. The template is minimal — a bin entry that runs your server over stdio and a small server config that names your tools.

  • npm init -y && npm install @modelcontextprotocol/sdk zod
  • Add a bin field to package.json pointing at dist/index.js.
  • Use tsx or tsc — tsx is easier for the dev loop.
  • Add a `start:dev` script that runs `tsx watch src/index.ts`.

Your first tool

Tools are the primary MCP primitive. Each has a name, a JSON Schema for its inputs (Zod is the ergonomic way to author these), and an async handler that returns a result. Start with something dumb — like an 'echo' tool that returns whatever the model sent. Once that works end-to-end, real tools take minutes.

The single biggest early mistake is returning bare strings from your handler. MCP expects a structured content array: [{ type: 'text', text: 'your result' }]. Return a raw string and Claude will see nothing, silently.

Resources vs tools — pick the right primitive

Tools are for actions the model performs on demand — 'search Notion', 'create a GitHub issue', 'run this query'. Resources are for data the client can browse and attach to context — files, pages, records. Most first-time servers over-use tools when a resource would be a better fit.

Rule of thumb: if the model needs to reason about what to call and when, it's a tool. If the user picks it from a list before the conversation starts, it's a resource.

Wiring to Claude Desktop

Claude Desktop reads a JSON config at a well-known path. Add an entry for your server with its absolute path, environment variables, and any args. Restart Claude — the app doesn't hot-reload MCP servers.

Once connected, Claude shows your tools in the paperclip menu. If they don't appear, 95% of the time your process is crashing on startup. Check the log file — on macOS it's under ~/Library/Logs/Claude/mcp*.log.

Debugging tips that save hours

MCP over stdio means stdout is the protocol — any console.log will corrupt the wire and Claude will disconnect. Use console.error for all logging; stderr is safe.

Add a heartbeat log at the top of your handler ('called tool X with args Y'). When something breaks you'll want to know whether the model even reached your handler.

Test your server without Claude first: the SDK ships with an inspector CLI that lets you call tools interactively. Use it until every tool round-trips correctly, then wire to Claude.

Shipping and versioning

Publish to npm with a clear name (server-<thing>) and semver from day one. Downstream users pin versions, so a breaking change without a major bump will bite them.

Document tool names, args, and quirks in the README. MCP clients don't yet render rich tool docs, so your README is what users read before installing.

FAQs

Related resources