Recipe-Inhalt ist auf Englisch. Englisches Original lesen →
← Alle Recipes
Phase 6 · Build Your Own MCP Server·6 steps

Build Your Own MCP, minimal server skeleton in TypeScript

From npm init to a working MCP server with one tool, in 60 lines of TypeScript. The skeleton you can fork for everything in Phase 6-10.

6 steps0%
Du liest ohne Account. Mit Login speichern wir Step-Fortschritt + Notes.

Build Your Own MCP, minimal server skeleton

You don't need a framework. The official @modelcontextprotocol/sdk is enough. By the end of this recipe you have a working MCP server with one tool, runnable via npx -y your-package from any MCP client.

This recipe is a tutorial, you will read it, your AI will write the code with you. There are no installable skills here. Phase 6-10 is about teaching, not pasting. Phase 11+ ships ready-to-deploy code patterns.

Schritt 1: Bootstrap the package

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install '@modelcontextprotocol/sdk@^1' 'zod@^3'
npm install -D typescript @types/node tsx vitest

About version pinning: The MCP SDK is on 1.x as of mid-2026 (1.29.x was current at this recipe's last update). A 2.x line is on the SDK's roadmap and will likely be a breaking change, pinning to ^1 keeps you on the current API surface and lets you upgrade deliberately when v2 ships. Same idea for Zod: ^3 because Zod 4 made the input-validation API around .strict() / .passthrough() slightly different.

Then create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ES2022",
    "moduleResolution": "node",
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "declaration": false
  },
  "include": ["src/**/*"]
}

And in package.json, add:

{
  "type": "module",
  "main": "dist/server.js",
  "bin": { "my-mcp-server": "./dist/server.js" },
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/server.ts",
    "test": "vitest run"
  }
}

The bin field makes npx -y my-mcp-server work after publish. The type: module tells Node to use ESM.

Schritt 2: Write the server

src/server.ts:

#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  type Tool,
} from '@modelcontextprotocol/sdk/types.js';

const helloTool: Tool = {
  name: 'hello',
  description: 'Say hello to a name. Returns a friendly greeting.',
  inputSchema: {
    type: 'object',
    properties: {
      name: { type: 'string', description: 'Who to greet.' },
    },
    required: ['name'],
  },
};

const server = new Server(
  { name: 'my-mcp-server', version: '0.1.0' },
  { capabilities: { tools: {} } },
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [helloTool],
}));

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  const args = (req.params.arguments ?? {}) as Record<string, unknown>;
  if (req.params.name === 'hello') {
    const name = String(args.name || 'world');
    return { content: [{ type: 'text', text: `Hello, ${name}!` }] };
  }
  return {
    content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }],
    isError: true,
  };
});

const transport = new StdioServerTransport();
await server.connect(transport);

That's the whole server. 50 lines. List tools, handle a call, connect stdio. The MCP SDK does the wire protocol.

Schritt 3: Build + test locally

npm run build
chmod +x dist/server.js

Now register it as a local MCP server in Claude Code:

claude mcp add my-server -s user -- node /absolute/path/to/dist/server.js

Open a fresh Claude Code session and ask "say hello to me with name=Matthias". Claude calls your hello tool. You see the response.

Schritt 4: Add a second tool with input validation

The hello tool trusted whatever the LLM sent. Production tools validate. Add zod:

import { z } from 'zod';

const greetSchema = z.object({
  name: z.string().min(1).max(80),
  language: z.enum(['en', 'de', 'es']).default('en'),
});

const greetTool: Tool = {
  name: 'greet',
  description: 'Greet someone in their language.',
  inputSchema: {
    type: 'object',
    properties: {
      name: { type: 'string' },
      language: { type: 'string', enum: ['en', 'de', 'es'] },
    },
    required: ['name'],
  },
};

In the call handler:

if (req.params.name === 'greet') {
  const parsed = greetSchema.safeParse(args);
  if (!parsed.success) {
    return {
      content: [{ type: 'text', text: `Bad input: ${parsed.error.message}` }],
      isError: true,
    };
  }
  const greetings = { en: 'Hello', de: 'Hallo', es: 'Hola' };
  return {
    content: [{ type: 'text', text: `${greetings[parsed.data.language]}, ${parsed.data.name}!` }],
  };
}

The pattern: Zod schema for runtime validation, JSON schema in the Tool definition for the LLM. They duplicate intentionally. JSON schema teaches the LLM how to call, Zod catches the rare bad call that slips through.

Schritt 5: Verify

Run academy_validate_step. The validator checks package.json in cwd has @modelcontextprotocol/sdk as a dependency AND a bin or main entry. If you skipped the bin field, add it now, without it npx -y cannot find the entry point.

Schritt 6: What the next 4 recipes cover

This is the foundation. The rest of Phase 6 layers in:

  • 6.2 Tool design, naming, descriptions, idempotency, error responses. The difference between a tool the LLM uses and one it ignores.
  • 6.3 Input validation. Zod patterns that don't fight the LLM. .strip() policy, error response shape.
  • 6.4 Testing MCP tools, vitest setup, in-memory transport for integration tests, smoke tests.
  • 6.5 Logging pattern, stderr-only, structured. stdout corrupts the wire protocol, this is the most common production bug in homemade MCP servers.

Phase 7 then adds dual-transport (stdio + HTTP), OAuth, multi-tenant. Phase 8 deploys it. Phase 9 publishes it. Phase 10 turns it into a SaaS.

If you want the production-grade code patterns directly (rate-limit middleware, idempotency tables, RLS schema, blue-green deploy scripts), those are Phase 11-15 in the Team plan, every recipe ships with installable Skills + ready-to-deploy code. Phase 6-10 here is the teaching path; Phase 11-15 is the production library.

Client-Check · auf Deinem Rechner ausführen
cat package.json 2>/dev/null | python3 -c "import json,sys; p=json.load(sys.stdin); deps=list((p.get(\"dependencies\") or {}).keys()); print(\"sdk:\", \"@modelcontextprotocol/sdk\" in deps); print(\"bin:\", bool(p.get(\"bin\"))); print(\"main:\", bool(p.get(\"main\")))" 2>/dev/null || echo "no package.json in cwd"
Erwartet: sdk: True, plus either bin or main is True.
Falls hängen geblieben: Run `npm init -y && npm install @modelcontextprotocol/sdk zod`, then add `"bin": { "your-server": "./dist/server.js" }` to package.json.
Weekly review, synthesize a weTool design, what makes a good