COMPUTERZUGRIFFSSYSTEM

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 sind
  • tools/call — der Client ruft ein Werkzeug mit Argumenten auf
  • resources/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:

Terminal window
mkdir mcp-server && cd mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

Fü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:

Terminal window
claude mcp add my-server node /absolute/path/to/dist/index.js

Was 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