Build your first MCP server: a 90-minute TypeScript tutorial for Claude Desktop
Step-by-step tutorial: build a working Model Context Protocol server in TypeScript, expose it to Claude Desktop, and learn the gotchas other guides skip.
The bottom line
A working MCP server in 90 minutes is a realistic target if a reader has Node 20+, a terminal they’re comfortable in, and Claude Desktop installed. This tutorial walks through building a weather server in TypeScript, registering it with Claude Desktop via stdio transport, and confirming end-to-end that Claude can call the tool. The worked example follows the same SDK + stdio architecture the official tutorial uses 1 , simplified to one tool, so the reader can cross-reference the architecture at any step. The article uses the SDK’s lower-level Server API; the official tutorial uses the higher-level McpServer API. Both are valid and documented.
The Model Context Protocol was introduced by Anthropic in November 2024 2 and has since been adopted across the major AI platforms (Claude, ChatGPT, Cursor, Windsurf) as the standard way an LLM client discovers and calls external tools. This tutorial ships a server that runs locally and exposes data to Claude, not one architected for production.
Python and C# implementations, MCP client code, and production deployment patterns are separate articles.
What you’ll build
The end state is a TypeScript MCP server that exposes one tool, get-weather, returning a stub response when Claude Desktop calls it. The reader will learn the three MCP primitives, write the server code in a single file, register it with Claude Desktop’s config, and verify the round trip by asking Claude a weather question.
A stub keeps debugging tractable. If Claude returns the wrong answer, the bug is in the wiring, not the upstream API. Once the loop works, swapping the stub for a live fetch call against any free weather API is a five-minute follow-up.
Prerequisites
Before starting, the reader needs:
- Node.js 20 or newer. Run
node --versionin a terminal to confirm. The official MCP TypeScript tutorial states Node 16+; the current@modelcontextprotocol/sdkpackage (v2.x line, alpha at time of writing) listsengines.node: ">=20"in its package.json, so Node 20 LTS is the stated minimum for the SDK 3 . - A package manager. npm ships with Node and is fine; pnpm or yarn work the same.
- Claude Desktop installed. Free; download from claude.ai. Open it at least once so the config-file path is created.
- A terminal for pasting commands. Terminal.app on macOS, any Linux terminal, or PowerShell or WSL on Windows.
- A code editor. VS Code, Cursor, or anything with TypeScript support.
That’s it. No API keys, no cloud accounts, no Docker.
The three primitives
MCP defines three primitives a server can expose to a client 4 . Understanding which is which saves an hour of confusion later.
Tools are functions the LLM invokes. The server declares the tool name, a description for the model to read, and an input schema. When the model decides to call the tool, it sends a JSON-RPC tools/call request, and the server executes the function and returns the result. Tools are how an LLM gets side-effects: writing to a database, hitting an API, mutating a file.
Resources are read-only data the LLM can fetch by URI. The server lists what’s available; the client decides what to read. Resources fit data that exists at a known address: a file, a database row, a Confluence page. The model does not control whether to read a resource the same way it controls whether to call a tool.
Prompts are templates the server makes available for invocation. They’re parameterised reusable instructions: “summarise this commit”, “draft a release note from these PRs”. Prompts ship with the server so any client can offer the same standard ways of asking.
For a first server, tools are usually where the value is. This tutorial implements one tool and ignores resources and prompts; both can be added later with the same SDK pattern.
Image: Model Context Protocol official build-a-server tutorial (modelcontextprotocol.io/docs/develop/build-server), used for editorial coverage of the protocol architecture this tutorial implements.
Step 1: Project setup
Create a new directory and initialise a Node project:
mkdir mcp-weather-server
cd mcp-weather-server
npm init -y
Install the MCP TypeScript SDK and the dev-time TypeScript toolchain:
npm install @modelcontextprotocol/sdk
npm install --save-dev typescript @types/node tsx
The @modelcontextprotocol/sdk package is the official TypeScript SDK 5 . It exposes the Server class, the stdio transport, and the type definitions for the protocol’s JSON-RPC messages. Using tsx to run the file means the reader skips a separate tsc build step during development.
Add a tsconfig.json for sensible defaults:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*"]
}
Set the package type to module so Node treats .js output as ES modules. Open package.json and add "type": "module" at the top level.
Image: MCP TypeScript SDK GitHub repository (github.com/modelcontextprotocol/typescript-sdk), used for editorial coverage of the SDK this tutorial builds against.
Step 2: Write the server
Create src/index.ts. The full file is roughly 60 lines:
#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'weather-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
},
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get-weather',
description:
'Get current weather for a city. Returns a temperature in Celsius and a one-line condition.',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name, e.g. "Bengaluru" or "London"',
},
},
required: ['city'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'get-weather') {
throw new Error(`Unknown tool: ${request.params.name}`);
}
const city = String(request.params.arguments?.city ?? 'unknown');
// Deterministic stub. Replace with a real fetch() once the wiring works.
const responseText = `Weather in ${city}: 28°C, partly cloudy.`;
return {
content: [
{
type: 'text',
text: responseText,
},
],
};
});
const transport = new StdioServerTransport();
await server.connect(transport);
Three things to flag while reading this code.
The Server constructor takes a service identity and a capabilities object. The capabilities object tells the client what primitives the server exposes; here, tools: {} declares the server speaks the tools protocol but ships no resources or prompts.
The two request handlers (ListToolsRequestSchema and CallToolRequestSchema) are the load-bearing pieces. List tells Claude what the server can do; call executes a tool. The SDK handles the JSON-RPC parsing; the developer writes only the body.
The StdioServerTransport connects the server to its parent process (Claude Desktop, in this case) over standard input and standard output. There is no port to open and no HTTP server to start. The parent spawns the server as a child, and the two communicate over the child’s stdin/stdout streams.
Sanity-check that the file runs without crashing:
npx tsx src/index.ts
The process should start and sit waiting for input. Press Ctrl+C to exit. If it crashes, the error is almost always a missing import or a TypeScript syntax mistake; fix and re-run.
Step 3: Register it with Claude Desktop
Claude Desktop reads MCP server configurations from a JSON file at a fixed path 6 :
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json(when Claude Desktop is available on Linux; check the install path your build uses)
Open or create that file. Add a mcpServers block:
{
"mcpServers": {
"weather": {
"command": "npx",
"args": ["tsx", "/absolute/path/to/mcp-weather-server/src/index.ts"]
}
}
}
Two things matter in this config block.
The command and args together describe how Claude Desktop should spawn the server. Claude Desktop runs <command> <args> as a child process and pipes its stdin/stdout to the MCP transport. Using npx tsx here lets the reader skip a separate compile step; for production, replace this with node /absolute/path/to/dist/index.js after running tsc.
The path must be absolute. A relative path will resolve against Claude Desktop’s working directory, not the project folder, and the spawn will fail silently. On macOS and Linux, run pwd inside the project folder and copy the absolute path into the config. On Windows, use the full path with backslashes escaped or forward slashes.
Save the file and fully quit Claude Desktop. On macOS that means Cmd+Q, not just closing the window; on Windows it means right-clicking the system-tray icon and choosing Quit. Reopen Claude Desktop. The new server should be discovered on startup.
Image: Anthropic Claude Desktop installation documentation (support.claude.com), used for editorial coverage of the per-OS config file paths.
Step 4: Verify the round trip
Open a new chat in Claude Desktop. Look for the tools indicator. Claude Desktop surfaces connected MCP servers in the chat UI; the exact location varies by version, but a small icon near the input field shows the count of available tools.
Ask Claude a weather question:
What’s the weather in Bengaluru?
Claude should recognise the get-weather tool from the list, decide to call it, send a tools/call request to the server, and reply using the stub response. The expected reply is something like: “The weather in Bengaluru is 28°C and partly cloudy.” The wording will vary because Claude paraphrases tool output, but the temperature and condition come from the stub.
If Claude responds with general weather knowledge instead of calling the tool, the server is not connected. Move to the gotchas section.
Common gotchas (read this section before debugging)
The first server connection rarely works on the first try. Most failures fall into one of five buckets, all of them solvable in under five minutes.
Config-file path is wrong. Claude Desktop fails silently when it cannot find or parse the config file. The most common cause is a typo in the directory name or saving the file somewhere that looks right in Finder/Explorer but isn’t where Claude reads from. Double-check the path matches the OS-specific path above.
Path in the config block is relative, not absolute. npx tsx src/index.ts works in the terminal because the working directory is the project root. Claude Desktop spawns the server from a different working directory, so a relative path resolves to nothing. Always paste the full absolute path into the args array.
Logging to stdout breaks stdio transport. This is the gotcha that costs the most time. Stdio transport uses standard output to send JSON-RPC messages back to Claude. Any console.log() call in the server code injects garbage into that channel and the client rejects the response. Use console.error() for any debug logging; stderr is free for the developer to use, stdout is reserved for protocol messages. The MCP TypeScript SDK does not warn about this; the symptom is “tool calls fail with no obvious error” and the cause is usually a stray console.log left in during debugging.
Claude Desktop wasn’t fully quit. Closing the chat window or hitting the red close button on macOS does not exit the app. The MCP config is read at process start; a backgrounded instance keeps its old config. Cmd+Q on macOS or right-click-Quit from the system tray on Windows. Then reopen.
JSON-RPC schema mismatch. The MCP protocol is built on JSON-RPC 2.0 7 , and the SDK enforces request and response shapes. Returning a string instead of { content: [...] } from the call handler raises a schema error, which usually surfaces as the model claiming the tool returned nothing. The handler must return the exact response shape the SDK expects: a content array of typed items, not a plain string or object. The example code above shows the right shape.
If the round trip still fails after working through the five buckets, the next debugging step is to spawn the server from the terminal exactly the way Claude Desktop will spawn it, send a hand-crafted JSON-RPC request to its stdin, and observe the response on stdout. The MCP repository’s reference implementations 8 include simple servers that can be diffed against to find the missing piece.
Image: github.com/modelcontextprotocol/servers reference servers repository, used for editorial coverage of the comparison set this tutorial’s debugging step references.
What to build next
Once the round trip works, four extensions are worth considering, in roughly increasing complexity.
Replace the stub with a real fetch call. Pick a free weather API, call it from inside the call handler, and return the live temperature and condition. This proves the tutorial’s wiring is solid; if the LLM-to-tool path works, swapping the implementation is mechanical.
Add a second tool. A get-forecast tool with a different input schema is the simplest extension. The pattern from get-weather carries over: list it in the list handler, branch on the name in the call handler.
Try the HTTP transport. Stdio is the right choice for a desktop client like Claude Desktop, but a remote MCP server uses HTTP with Server-Sent Events for streaming. MCP supports an HTTP / Streamable-HTTP transport for remote servers, documented separately on modelcontextprotocol.io; once the stdio version works, swapping to HTTP is a transport change, not an architecture rewrite.
Compare with another language SDK. Microsoft’s C# tutorial 9 walks the same architecture in a different language. Reading both clarifies which parts of MCP are protocol (universal) and which are SDK ergonomics (language-specific), a useful distinction once a developer starts writing servers in production.
For a deeper dive on the AI-coding workflow this server can plug into, the related-reading callout below covers the editor-side tooling story.
Starting with a tiny server and a stub implementation makes the protocol legible. That legibility is what makes the harder topics tractable: authentication, multi-tenancy, tool composition across servers. Without it, MCP looks like one more configuration format. With it, the whole protocol fits in the developer’s head.
How this article was made: an autonomous AI pipeline researched, drafted, fact-checked, and reviewed this piece, aggregating publicly-available information from the sources consulted below. AI (artificial intelligence) can make mistakes, so please cross-check the consulted sources before acting on anything here. Neural Tech Daily is not liable for decisions or outcomes based on this article.
Sources consulted
Cited Sources
- 1. Model Context Protocol — official "Build a server" tutorial (canonical reference for the architecture, three primitives, and stdio + HTTP transport options) (accessed ) ↩
- 2. Anthropic — Introducing the Model Context Protocol (announcement, 25 November 2024) (accessed ) ↩
- 3. modelcontextprotocol/typescript-sdk — official TypeScript SDK (Node version requirements + package documentation) (accessed ) ↩
- 4. Model Context Protocol — Introduction (canonical definitions of tools, resources, and prompts as the three server primitives) (accessed ) ↩
- 5. modelcontextprotocol/typescript-sdk — npm package metadata for @modelcontextprotocol/sdk (Server class, StdioServerTransport, type schemas) (accessed ) ↩
- 6. Anthropic — Claude Desktop installation and MCP configuration guide (config-file paths per OS) (accessed ) ↩
- 7. JSON-RPC 2.0 specification — the wire protocol MCP messages are framed in (accessed ) ↩
- 8. modelcontextprotocol/servers — official reference server implementations (Filesystem, Git, Memory, Time, etc., per the active list at time of writing) for diffing against a custom server (accessed ) ↩
- 9. Microsoft Developer Blog — Build a Model Context Protocol server in C# (cross-language tutorial covering the same architecture in .NET) (accessed ) ↩
Anonymous · no cookies set