← Level 6
Level 6· Lektion 2 von 8

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/sdk is the official Anthropic library.
  • zod is for input validation (mandatory, not optional).
  • tsx lets 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

  1. Imports the SDK.
  2. Creates a server with name + version.
  3. Tells the client: "I have a tool called echo."
  4. When the client calls echo, validates input with Zod and returns an echo.
  5. 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".

You're reading without an account. Login saves your progress so you can pick up where you left off. Log in →