TypeScript skeleton, an MCP server in 40 lines
Minimal code. Not the best architecture, but the clearest entry point.
What you install
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
@modelcontextprotocol/sdkis the official Anthropic library.zodis for input validation (mandatory, not optional).tsxlets you run TypeScript directly without a build step.
The minimal server
File src/index.ts:
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const server = new Server(
{ name: "my-mcp-server", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
const EchoSchema = z.object({
message: z.string(),
});
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "echo",
description: "Returns the passed message.",
inputSchema: {
type: "object",
properties: { message: { type: "string" } },
required: ["message"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (req) => {
if (req.params.name === "echo") {
const { message } = EchoSchema.parse(req.params.arguments);
return { content: [{ type: "text", text: `Echo: ${message}` }] };
}
throw new Error(`Unknown tool: ${req.params.name}`);
});
const transport = new StdioServerTransport();
await server.connect(transport);
That's it. 30 lines of code, a fully functional MCP server.
What the code does
- Imports the SDK.
- Creates a server with name + version.
- Tells the client: "I have a tool called
echo." - When the client calls
echo, validates input with Zod and returns an echo. - Connects via stdin/stdout to the client.
That's the basis. Every MCP server follows this pattern, no matter how complex it gets.
Testing
In package.json add this script:
"scripts": { "dev": "tsx src/index.ts" }
Then configure Claude Code:
claude mcp add my-server -s user -- npx -y tsx /path/to/project/src/index.ts
Restart Claude Code, type /mcp, you should see "my-server". Type:
Use my-server's echo tool with the message "Hello"
Claude calls echo, returns "Echo: Hello".
The two beginner traps
1. console.log as debug. Doesn't work for stdio servers. console.log lands on stdout, the client reads that as a protocol message, the server crashes. Instead: use console.error(...), that goes to stderr and doesn't interfere.
2. Missing input validation. If the client sends something wrong and you don't validate, your server crashes. The agent sees the crash as "Tool failed", does weird things. Always Zod schemas. Always.
What's missing now
This is the skeleton. For a real server you need:
- More tools (instead of
echo, something useful) - External API calls (with error handling, retry, rate limit)
- Auth if needed (API keys, OAuth)
- Tests
- Logging
- Deployment (npm publish or Docker)
That's coming in the next lessons. The skeleton is the body, not the sprint.
Next lesson
Tool design. How you write good tools that the model uses correctly. That's the difference between "my server exists" and "my server gets used".