Ihren ersten MCP-Server bauen
Das Model Context Protocol (MCP) ist das nächste, was das KI-Ökosystem einem universellen Adapter hat. Es definiert, wie KI-Modelle Werkzeuge entdecken und aufrufen und wie diese Werkzeuge strukturierte Ergebnisse zurückgeben. Sobald ein MCP-Server läuft, kann jeder kompatible Client — Claude, Claude Code oder jedes Agenten-Framework, das MCP spricht — ihn ohne zusätzlichen Verbindungscode verwenden.
Diese Anleitung zeigt Schritt für Schritt, wie man einen echten MCP-Server aufbaut — kein Spielzeugbeispiel, sondern ein Muster, das man in der Produktion einsetzen kann.
Was MCP eigentlich ist
MCP ist ein JSON-RPC-2.0-Protokoll über stdio (oder HTTP+SSE für Remote-Server). Client und Server tauschen eine kleine Menge von Nachrichtentypen aus:
tools/list— der Client fragt, welche Werkzeuge verfügbar sindtools/call— der Client ruft ein Werkzeug mit Argumenten aufresources/list/resources/read— der Client liest Dateien, Datenbanken oder anderen Kontext
Der Server deklariert seine Werkzeuge im Voraus mit einem JSON-Schema für die Eingabe jedes Werkzeugs. Der Client verwendet dieses Schema, um gültige Aufrufe zu konstruieren.
Diese Einfachheit ist der Punkt. MCP ist kein Framework. Es ist ein Vertrag, der es ermöglicht, alles einzuwickeln — eine Postgres-Datenbank, einen Kubernetes-Cluster, ein GitHub-Repo — und als typisierte, aufrufbare Funktionen bereitzustellen.
Einrichtung
Sie benötigen Node.js 18+ und ein TypeScript-Projekt:
mkdir mcp-server && cd mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zodnpm install -D typescript @types/node tsxFügen Sie eine tsconfig.json hinzu:
{ "compilerOptions": { "target": "ES2022", "module": "Node16", "moduleResolution": "Node16", "strict": true, "outDir": "./dist" }, "include": ["src"]}Und ein Startskript in package.json:
{ "scripts": { "dev": "tsx src/index.ts", "build": "tsc", "start": "node dist/index.js" }}Den Server aufbauen
Erstellen Sie src/index.ts. Die Struktur ist immer dieselbe: Server initialisieren, Werkzeuge registrieren, mit stdio-Transport verbinden.
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";
const server = new McpServer({ name: "my-mcp-server", version: "1.0.0",});Registrieren Sie nun Werkzeuge. Jedes Werkzeug hat einen Namen, eine Beschreibung und ein Eingabeschema:
server.tool( "get_weather", "Get current weather conditions for a city", { city: z.string().describe("The city name to get weather for"), units: z.enum(["celsius", "fahrenheit"]).default("celsius"), }, async ({ city, units }) => { // Your actual implementation here const data = await fetchWeather(city, units); return { content: [ { type: "text", text: `Weather in ${city}: ${data.temp}°${units === "celsius" ? "C" : "F"}, ${data.condition}`, }, ], }; });Verbinden Sie den Server mit dem stdio-Transport:
const transport = new StdioServerTransport();await server.connect(transport);Das ist das vollständige Server-Skelett. Alles weitere ist das Hinzufügen von Werkzeugen.
Werkzeug-Entwurfsmuster
Die Qualität Ihres MCP-Servers hängt davon ab, wie gut Sie die Werkzeuge entwerfen.
Schmale, komponierbare Werkzeuge statt breiter Monolithen
Ein Werkzeug, das eine Sache macht, ist nützlicher als eines, das viele Dinge macht.
Vermeiden Sie dies:
server.tool("manage_database", "Do anything with the database", { operation: z.enum(["read", "write", "delete", "schema"]), query: z.string(), // ... viele optionale Parameter})Bevorzugen Sie dies:
server.tool("query_records", "Run a SELECT query and return rows as JSON", { table: z.string(), where: z.string().optional().describe("SQL WHERE clause, e.g. 'status = active'"), limit: z.number().default(50),})
server.tool("get_schema", "Return the column definitions for a table", { table: z.string(),})Eingaben präzise beschreiben
Das Modell liest Ihre describe()-Strings, um zu wissen, was übergeben werden soll:
{ date_range: z.string().describe( "ISO 8601 date range in the format YYYY-MM-DD/YYYY-MM-DD. " + "Example: 2025-01-01/2025-03-31" ),}Strukturierten Text zurückgeben, kein rohes JSON
Modelle parsen Text zuverlässiger als rohe JSON-Blobs in Werkzeugergebnissen:
return { content: [ { type: "text", text: [ `Found ${rows.length} records:`, ...rows.map(r => `- ${r.id}: ${r.name} (${r.status})`), ].join("\n"), }, ],};Fehlerbehandlung
Geben Sie Fehler als Werkzeugergebnisse zurück, nicht als Ausnahmen:
async ({ table, where }) => { try { const rows = await db.query(table, where); return { content: [{ type: "text", text: formatRows(rows) }] }; } catch (err) { return { content: [ { type: "text", text: `Query failed: ${err instanceof Error ? err.message : "unknown error"}. ` + `Check that the table name is correct and the WHERE clause uses valid column names.`, }, ], isError: true, }; }}Ein echtes Beispiel: GitHub-Werkzeugserver
Hier ist ein konkretes Werkzeug, das die GitHub-API einwickelt:
import { Octokit } from "@octokit/rest";
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
server.tool( "get_file_contents", "Read the contents of a file from a GitHub repository", { owner: z.string().describe("Repository owner (username or org)"), repo: z.string().describe("Repository name"), path: z.string().describe("File path within the repository"), ref: z.string().optional().describe("Branch, tag, or commit SHA. Defaults to the default branch."), }, async ({ owner, repo, path, ref }) => { try { const { data } = await octokit.rest.repos.getContent({ owner, repo, path, ref, });
if (Array.isArray(data)) { // It's a directory — return listing const listing = data.map(f => `${f.type === "dir" ? "📁" : "📄"} ${f.name}`).join("\n"); return { content: [{ type: "text", text: `Directory listing for ${path}:\n${listing}` }], }; }
if (data.type !== "file" || !data.content) { return { content: [{ type: "text", text: `${path} is not a readable file` }], isError: true, }; }
const content = Buffer.from(data.content, "base64").toString("utf-8"); return { content: [ { type: "text", text: `Contents of ${owner}/${repo}/${path}:\n\`\`\`\n${content}\n\`\`\``, }, ], }; } catch (err: unknown) { const message = err instanceof Error ? err.message : "Unknown error"; return { content: [{ type: "text", text: `Failed to read ${path}: ${message}` }], isError: true, }; } });Mit Claude Desktop verbinden
Bearbeiten Sie die Konfiguration unter ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) oder %APPDATA%\Claude\claude_desktop_config.json (Windows):
{ "mcpServers": { "my-server": { "command": "node", "args": ["/absolute/path/to/dist/index.js"], "env": { "GITHUB_TOKEN": "your_token_here" } } }}Starten Sie Claude Desktop neu. Ihre Werkzeuge erscheinen automatisch in der Werkzeugauswahl.
Mit Claude Code verbinden
MCP-Server verbinden sich mit Claude Code über die CLI:
claude mcp add my-server node /absolute/path/to/dist/index.jsWas als nächstes zu bauen ist
Ein MCP-Server ist im Wesentlichen eine Fähigkeitsgrenze: Was kann der Agent sehen und tun?
- Datenbankserver — Schreibgeschützten Abfragezugriff auf Postgres, SQLite oder DynamoDB bereitstellen
- Code-Analyseserver — tree-sitter oder einen Language-Server einwickeln
- Monitoring-Server — Metriksysteme wie Prometheus oder Datadog einbinden
- Dokumentationsserver — Interne Dokumentation indexieren und bereitstellen
Das MCP-Ökosystem ist noch jung. Die meisten nützlichen Server wurden noch nicht gebaut. Bauen Sie den Server, der ein echtes Problem in Ihrer Umgebung löst.
Verwandte Artikel
- Einführung in die Agentische Entwicklung
- Tool-Nutzungsmuster: Zuverlässige Agent-Tool-Schnittstellen entwickeln
- Multi-Agenten-Muster: Orchestratoren, Worker und Pipelines