You know the architecture. You know the wire format. Now it's time to stop reading JSON by hand and use the official SDK to build real MCP servers in TypeScript — with full type-safety, schema validation, and a clean developer experience.
@modelcontextprotocol/sdk package abstracts the JSON-RPC plumbing so you can focus on what your server actually does, not how it serializes bytes.
MCP is a protocol spec — it doesn't mandate a language. There are SDKs in Python, Go, Rust, and Kotlin. But the TypeScript SDK is the reference implementation, maintained directly by Anthropic and updated first. It ships with the tightest parity to the spec, the most examples in the wild, and the deepest integration with the Claude Desktop toolchain.
There's also a structural reason TypeScript excels here: MCP messages are JSON objects with typed shapes. TypeScript's type system was designed precisely for this. When your tool's input schema says { "city": "string", "units": "celsius | fahrenheit" }, TypeScript can enforce that contract at compile time, before any request ever arrives.
You could drive a screw with a kitchen knife. It will work, sort of, sometimes. The TypeScript SDK is the proper screwdriver — purpose-built. It knows the exact shape of every MCP message, handles the transport layer, manages the handshake, and exposes a clean API surface. Python is the second screwdriver in the box; both fit, but TypeScript is the one Anthropic designed the system with.
The mcp Python package (via pip install mcp) mirrors almost everything in this day's content. The patterns — McpServer, tool registration, Zod equivalent (Pydantic), stdio transport — translate 1:1. Follow along in TypeScript for the authoritative examples, then switch to Python when building your real server.
You need four things: Node.js 18+, npm (ships with Node), TypeScript, and the @modelcontextprotocol/sdk package. That's the entire dependency surface for a minimal MCP server. Let's walk through the setup step by step.
AbortController API, and ESM compatibility. Check your version before proceeding.--save-exact to pin the SDK version — MCP spec evolves quickly.tsconfig.json with strict mode and ESM output. The MCP SDK is distributed as ESM — your config must match."type": "module" field and helpful dev scripts. The tsx tool lets you run TypeScript files directly during development without a build step.src/. We'll create index.ts in Section 6.tsx is a zero-config TypeScript runner — it compiles and runs on the fly, perfect for iteration. For distributing your server or registering it with Claude Desktop, always do a full npm run build first so Claude launches the compiled dist/index.js, not a dev-only tool.
Before you use a toolkit, understanding its structure builds intuition for where to look when things go wrong. The SDK is published as a single npm package but has a clear internal layout. Here's what's inside node_modules/@modelcontextprotocol/sdk:
The three layers that matter most to you as a server developer:
McpServer class. This is your primary interaction point. It provides .tool(), .resource(), and .prompt() registration methods with full type inference.StdioServerTransport — reads JSON-RPC from stdin, writes to stdout. This is the transport you use for Claude Desktop integration. One line to instantiate.Tool, Resource, Prompt, CallToolResult, TextContent, ImageContent — import these to type your handlers precisely.The SDK exports both a low-level Server class and a high-level McpServer class. Server gives you raw message routing — you register handlers for individual JSON-RPC methods. McpServer wraps this with ergonomic .tool(), .resource(), and .prompt() methods that handle the tools/list and tools/call routing automatically. Always start with McpServer unless you need protocol-level control.
When importing from the SDK in an ESM project, you must use the .js extension in the path even though the source file is .ts. This is Node's ESM resolution requirement — TypeScript compiles .ts → .js, and the import path must reference the final compiled filename. This trips up nearly every newcomer.
The McpServer class is the central object in any MCP server you build. Think of it as a registry + router: you register capabilities (tools, resources, prompts) on it at startup, and it handles routing incoming JSON-RPC messages to the right handler, automatically generating tools/list, resources/list, and prompts/list responses from your registrations.
One thing that surprises newcomers: McpServer is not a network server. It knows nothing about ports, HTTP, or sockets. Its only job is protocol-level: parse JSON-RPC messages, dispatch them to handlers, and serialize responses. The transport is a separate object you connect it to. This separation is intentional — it lets you swap stdio for HTTP without changing any of your business logic.
graph LR
subgraph "Your Code"
A["McpServer\n(capability registry)"] --> B[".tool() handlers\n.resource() handlers\n.prompt() handlers"]
end
subgraph "SDK Infrastructure"
C["Transport Layer\n(StdioServerTransport\nor SSEServerTransport)"] --> D["JSON-RPC\nMessage Loop"]
end
E["Claude Desktop\nor MCP Client"] <-->|"stdin/stdout\nor HTTP+SSE"| C
D <-->|"parsed messages"| A
style A fill:#1a0f3a,stroke:#8b5cf6,color:#a78bfa
style C fill:#0f2a1a,stroke:#10b981,color:#6ee7b7
Every tool you register needs an input schema — a description of what arguments it accepts. The MCP spec says this schema must be JSON Schema format. The TypeScript SDK uses Zod to define these schemas, then automatically converts them to JSON Schema for the tools/list response.
Why Zod? Because it does double duty: it generates the JSON Schema and validates and parses incoming inputs at runtime. You write the schema once, and you get both the wire-level description and the runtime type guard for free.
Imagine a restaurant order form with checkboxes and dropdowns. It tells customers what they can order (the schema) and prevents them from writing "a pet giraffe" in the entrée field (the validation). Zod is that form for your tool's inputs — it tells the model what parameters are available and validates the actual values before your handler ever runs.
.optional() makes a field nullable. .describe() adds a description to the JSON Schema — the model reads these to understand parameters.When you pass a Zod schema to server.tool(), TypeScript infers the handler's argument type automatically. If your schema is z.object({ city: z.string() }), then inside your handler, args.city is typed as string — no manual type annotation needed. Zod and TypeScript do the work together.
Theory is done. Let's build a complete, working MCP server from scratch. We'll build a Weather Server — it exposes one tool (get_weather), one resource (a static config), and one prompt (a weather report template). This covers all three MCP primitives in a realistic context.
CallToolResult with typed content.The server sends nothing — it waits. The client initiates the handshake by sending an initialize request. The transport reads it from stdin, passes it to the server's protocol layer, which responds with initialize result. The client then sends notifications/initialized. After that exchange (≈3 messages, described in Day 2), your tool/resource/prompt handlers become available for calls.
Let's break down the handler return value more precisely. Every tool handler must return a CallToolResult object with a content array. Each item in the array is a content block — the SDK supports three types:
Once your server is built, you need to register it with Claude Desktop so the application knows to launch it. Claude Desktop reads a JSON config file at a platform-specific path. You add an entry for each MCP server you want to make available.
After saving the config, fully quit and relaunch Claude Desktop — the servers are loaded at startup, not dynamically. Look for the hammer icon (🔨) in the Claude chat input area; clicking it shows available tools from all your registered servers.
1. Relative paths — Claude Desktop resolves paths from its own working directory, not yours. Always use absolute paths.
2. Not rebuilding — editing src/index.ts without running npm run build means Claude is running stale code.
3. Wrong Node path — on some systems "command": "node" resolves to the system Node, not your nvm Node. Use the full path from which node.
| Scenario | command | args | Notes |
|---|---|---|---|
| Production (built) | node |
["/abs/path/dist/index.js"] |
Fastest startup, recommended |
| Development (TS direct) | npx |
["tsx", "/abs/path/src/index.ts"] |
Slightly slower, no build needed |
| Global npm package | my-mcp-server |
[] |
Cleanest for published servers |
| Python server | python3 |
["/abs/path/server.py"] |
Same config format, different runtime |
MCP servers run in a subprocess spawned by Claude Desktop, which means you can't just console.log() and see the output in your terminal. You need deliberate debugging strategies. Here are the tools that actually work:
npx @modelcontextprotocol/inspector. Gives you a browser UI to send JSON-RPC messages, inspect capabilities, and test tools interactively without Claude Desktop.~/Library/Logs/Claude/ (macOS) or %APPDATA%\Claude\logs\ (Windows). Each MCP server gets its own log file named after the server key in your config.echo '{"jsonrpc":"2.0","method":"tools/list","id":1}' | node dist/index.js. Inspect the stdout output.npx tsc --noEmit to type-check without generating files. This catches schema mismatches, missing as const on type discriminants, and handler return type errors before runtime.pino configured to write to a file or stderr. Structured logs are searchable — plain console.error() is fine for early dev.The MCP Inspector tool is the fastest way to test a server before connecting Claude Desktop. It launches your server as a subprocess, handles the handshake, and gives you a form-based UI to call tools and read resources. Think of it as Postman for MCP — an essential part of your development workflow from Day 3 onward.
Five questions. Everything from today's content.
Select one answer per question, then submit to see your score.
console.error() is stderr. console.log() is stdout and will break your server..describe() method on a Zod schema field do?.describe() injects a description field into the JSON Schema output. When the model receives the tools/list response, it reads these descriptions to understand what each parameter means and how to use the tool correctly..js extension (e.g., "...sdk/server/mcp.js")?.ts files to .js, the import must reference the eventual .js output, even while writing TypeScript source.McpServer and Server in the SDK?Server is the base class with raw JSON-RPC method routing. McpServer wraps it with the .tool(), .resource(), and .prompt() methods that automatically handle list/call routing. You should always start with McpServer unless you need protocol-level control.