# Dein erster MCP-Server in 30 Minuten

**Ein praktischer Quickstart für das Model Context Protocol mit TypeScript**

## Was ist MCP?

Das Model Context Protocol (MCP) ist ein offener Standard von Anthropic, der LLMs wie Claude Zugriff auf externe Tools und Datenquellen ermöglicht. Statt alles in den Prompt zu packen, stellt ein MCP-Server strukturierte Schnittstellen bereit — der LLM kann dann gezielt Tools aufrufen, Ressourcen abfragen oder Prompts nutzen.

## Voraussetzungen

- **Node.js 18+** (LTS empfohlen)
- **TypeScript-Grundkenntnisse** (optional, aber hilfreich)
- **Ein MCP-Client** zum Testen — z.B. Claude Desktop oder Claude Code

## Projekt-Setup

```bash
# Neues Projekt anlegen
mkdir mein-mcp-server
cd mein-mcp-server

# package.json initialisieren
npm init -y

# MCP SDK installieren
npm install @modelcontextprotocol/sdk

# TypeScript + Dev-Dependencies
npm install -D typescript @types/node tsx

# TypeScript-Config
npx tsc --init
```

**Wichtig für `tsconfig.json`:** Setze `"module": "NodeNext"` und `"moduleResolution": "NodeNext"` für moderne ESM-Kompatibilität.

## Dein erster Server: Ein einfaches Rechner-Tool

Erstelle `src/server.ts`:

```typescript
#!/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";

// Server-Instanz erstellen
const server = new Server(
  {
    name: "mein-rechner",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {}, // Wir bieten Tools an
    },
  }
);

// Handler: Liste aller verfügbaren Tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "addiere",
        description: "Addiert zwei Zahlen",
        inputSchema: {
          type: "object",
          properties: {
            a: { type: "number", description: "Erste Zahl" },
            b: { type: "number", description: "Zweite Zahl" },
          },
          required: ["a", "b"],
        },
      },
    ],
  };
});

// Handler: Tool-Ausführung
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "addiere") {
    const { a, b } = args as { a: number; b: number };

    // Einfache Validierung
    if (typeof a !== "number" || typeof b !== "number") {
      throw new Error("a und b müssen Zahlen sein");
    }

    const ergebnis = a + b;

    return {
      content: [
        {
          type: "text",
          text: `${a} + ${b} = ${ergebnis}`,
        },
      ],
    };
  }

  throw new Error(`Unbekanntes Tool: ${name}`);
});

// Server über stdio starten
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // Wichtig: Error-Logs nur nach stderr, nie nach stdout!
  console.error("MCP-Server läuft auf stdio");
}

main().catch((error) => {
  console.error("Server-Fehler:", error);
  process.exit(1);
});
```

**Was passiert hier?**

1. **Server-Instanz:** Name + Version + Capabilities (wir bieten `tools` an)
2. **ListToolsRequestSchema:** Der Client fragt: "Welche Tools hast du?" → Wir antworten mit Schema
3. **CallToolRequestSchema:** Der Client sagt: "Führe Tool X mit Argumenten Y aus" → Wir liefern Ergebnis
4. **stdio-Transport:** Kommunikation über stdin/stdout (Standard für lokale Server)

## Server testen

### 1. Build-Script in `package.json` ergänzen

```json
{
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/server.ts"
  }
}
```

### 2. Server lokal testen (ohne Client)

```bash
npm run dev
```

Du solltest sehen: `MCP-Server läuft auf stdio` (auf stderr). Der Prozess wartet jetzt auf JSON-RPC-Nachrichten auf stdin.

**Manueller Test (optional):**

In einer zweiten Shell kannst du eine JSON-RPC-Nachricht schicken:

```bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | npm run dev
```

Du solltest die Tool-Liste als JSON zurückbekommen.

### 3. Anbindung an Claude Desktop / Claude Code

Erstelle oder bearbeite die MCP-Client-Config:

**Claude Desktop**:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

**Claude Code**:
- `~/.config/claude-code/mcp_config.json` (Linux/macOS)
- `%APPDATA%\claude-code\mcp_config.json` (Windows)

**Config-Beispiel:**

```json
{
  "mcpServers": {
    "mein-rechner": {
      "command": "node",
      "args": ["/absoluter/pfad/zu/mein-mcp-server/dist/server.js"]
    }
  }
}
```

**Wichtig:**
- Pfad muss absolut sein (keine relativen Pfade, kein `~`)
- Verwende die kompilierte `dist/server.js` (nach `npm run build`)
- Client neu starten nach Config-Änderung

### 4. Im Client testen

Öffne Claude Desktop/Code und schreibe:

> "Addiere 42 und 27 mit dem verfügbaren Tool"

Claude sollte das `addiere`-Tool nutzen und dir `42 + 27 = 69` ausgeben.

## Error-Handling-Basics

```typescript
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    // ... deine Tool-Logik ...
  } catch (error) {
    // Fehler strukturiert zurückgeben
    return {
      content: [
        {
          type: "text",
          text: `Fehler bei Tool-Ausführung: ${error instanceof Error ? error.message : String(error)}`,
        },
      ],
      isError: true,
    };
  }
});
```

**Logging-Regel:** Nutze `console.error()` für Debug-Logs — **niemals** `console.log()`, weil stdout für JSON-RPC-Nachrichten reserviert ist.

## Typische Stolperfallen

### 1. stdout vs. stderr

**Problem:** Server schreibt Debug-Logs nach stdout → JSON-RPC-Stream wird korrumpiert → Client bricht ab.

**Lösung:** Alle Logs nach `console.error()`, nie `console.log()`.

### 2. stdio vs. SSE

**stdio** (Standard Input/Output):
- Für lokale Server (Desktop-Tools, CLI-Tools)
- Kommunikation über stdin/stdout
- Server läuft als Child-Process

**SSE** (Server-Sent Events):
- Für Remote-Server über HTTP
- Nutzt `SSEServerTransport`
- Braucht HTTP-Server (z.B. Express)

Nicht mischen! Wenn du HTTP willst, nutze SSE-Transport + separaten HTTP-Server.

### 3. Async-Initialisierung

Wenn dein Server beim Start externe Ressourcen laden muss (Datenbank, API-Keys):

```typescript
async function main() {
  // ERST Ressourcen laden
  await ladeKonfiguration();

  // DANN Transport starten
  const transport = new StdioServerTransport();
  await server.connect(transport);
}
```

Nie `server.connect()` vor async-Init aufrufen — sonst bekommst du Race Conditions.

### 4. JSON-Schema-Validierung

Der `inputSchema` wird vom Client NUR zur UI-Generierung genutzt — **keine automatische Validierung**. Du musst im `CallToolRequestSchema`-Handler selbst prüfen:

```typescript
if (typeof args.a !== "number") {
  throw new Error("Parameter 'a' muss eine Zahl sein");
}
```

## Nächste Schritte

- **Ressourcen hinzufügen:** Statt nur Tools kannst du auch `resources` (Dateien, Daten) und `prompts` (Prompt-Templates) anbieten
- **Produktiv-Server:** Schau dir das offizielle `modelcontextprotocol/servers`-Repo an
- **Community-Server:** Liste auf `awesome-mcp-servers`

---

**Weiterführende Ressourcen**:
- MCP-Spezifikation: modelcontextprotocol.io/docs
- TypeScript SDK: npmjs.com/package/@modelcontextprotocol/sdk
- Offizielle Server-Beispiele: github.com/modelcontextprotocol/servers
