Was ist MCP — und warum macht es Claude Code so erweiterbar?
Das Model Context Protocol (MCP) ist ein offener Standard von Anthropic, der definiert, wie KI-Modelle wie Claude mit externen Datenquellen und Tools kommunizieren. Kurz gesagt: MCP ist die Schnittstelle, über die Claude Code über seine eingebauten Fähigkeiten hinauswachsen kann.
Ohne MCP kann Claude Code lesen, schreiben, Terminal-Befehle ausführen — aber nicht direkt in dein Jira schauen, deinen internen Wissensgraphen abfragen oder proprietäre APIs aufrufen, ohne dass du den Code manuell bereitstellst. Mit MCP ändert sich das grundlegend.
Die Architektur ist simpel: Ein MCP Server ist ein lokaler Prozess, der ein definiertes Protokoll über stdio oder HTTP spricht. Claude Code agiert als MCP Client und kann alle vom Server angebotenen Tools aufrufen — genau wie eingebaute Tools, aber von dir selbst definiert.
Was MCP von einfachen Shell-Skripten unterscheidet: Das Protokoll ist typsicher und selbstdokumentierend. Jedes Tool hat ein JSON-Schema, das Claude versteht — inklusive Beschreibungen, Pflichtfelder und Validierungsregeln. Claude kann dadurch nicht nur wissen, dass ein Tool existiert, sondern wie es sinnvoll eingesetzt wird.
Wann lohnt sich ein eigener MCP Server?
Nicht jede Aufgabe braucht einen MCP Server. Für einmalige Skripte reicht ein einfaches Tool-Call-Pattern. Ein eigener Server lohnt sich, wenn:
- Wiederholende API-Calls: Wenn Claude im Laufe einer Session dieselbe externe API mehrfach mit unterschiedlichen Parametern aufrufen muss — Jira, GitHub, interne Services.
- Spezialisierter Kontext: Dein Team hat proprietäre Datenquellen (interne Doku, CRM, ERP), die Claude nicht kennt. Ein MCP Server macht dieses Wissen strukturiert abrufbar.
- Konsistente Authentifizierung: API-Keys und Tokens zentral im Server verwalten statt in jedem Prompt neu einzufügen.
- Team-Nutzung: Wenn mehrere Entwickler denselben Workflow mit Claude durchführen, spart ein shared MCP Server Konfigurationsaufwand.
- Komplexe Transformationen: Rohdaten aus einer API in ein für Claude sinnvolles Format bringen — Pagination, Filterung, Aggregation.
MCP Server Tutorial: Node.js Setup
Schritt 1: Projekt initialisieren
Erstelle ein neues Node.js-Projekt und installiere das offizielle MCP SDK:
# Projektverzeichnis anlegen
mkdir my-mcp-server && cd my-mcp-server
npm init -y
# MCP SDK + TypeScript Dependencies
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node ts-node
# tsconfig.json anlegen
npx tsc --init --target es2022 --module node16 --moduleResolution node16
Deine package.json braucht einen bin-Eintrag, damit Claude Code den Server starten kann:
{
"name": "my-mcp-server",
"version": "1.0.0",
"type": "module",
"bin": {
"my-mcp-server": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
}
Schritt 2: Server-Struktur — Tools definieren
Die Kernstruktur eines MCP Servers besteht aus drei Teilen: Server-Initialisierung, Tool-Definitionen und Tool-Handler. Erstelle 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";
// Server instanziieren
const server = new Server(
{ name: "my-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
// Tools registrieren — Claude sieht diese Liste
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "hello_world",
description: "Gibt eine Begrüßung zurück. Nützlich zum Testen des Servers.",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Name der zu begrüßenden Person"
}
},
required: ["name"]
}
}
]
}));
// Tool-Handler — wird aufgerufen wenn Claude das Tool nutzt
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "hello_world") {
const greeting = `Hallo, ${args.name}! MCP Server läuft korrekt.`;
return {
content: [{ type: "text", text: greeting }]
};
}
throw new Error(`Unbekanntes Tool: ${name}`);
});
// Server starten über stdio (Standard für Claude Code)
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("MCP Server gestartet und wartet auf Verbindungen...");
}
main().catch(console.error);
console.error() für Logs verwenden, nicht console.log()! Der stdio-Transport nutzt stdout für das MCP-Protokoll. Logs in stdout würden das Protokoll korrumpieren und zu kryptischen Parse-Fehlern führen.
Schritt 3: In Claude Code registrieren
Nach dem Build (npm run build) trägst du den Server in ~/.claude/settings.json ein:
{
"mcpServers": {
"my-mcp-server": {
"command": "node",
"args": ["/absoluter/pfad/my-mcp-server/dist/index.js"],
"env": {
// Umgebungsvariablen für deinen Server
"MY_API_KEY": "dein-api-key"
}
}
}
}
Alternativ, wenn du den Server als npm-Paket installiert hast:
{
"mcpServers": {
"my-mcp-server": {
"command": "npx",
"args": ["-y", "my-mcp-server"]
}
}
}
Nach dem nächsten Start von Claude Code erscheinen deine Tools automatisch in der Tool-Liste. Claude kann sie ohne weiteres Zutun aufrufen.
Konkretes Beispiel: MCP Server für Jira
Theorie ist gut — ein echtes Beispiel ist besser. Hier bauen wir einen MCP Server, der Jira-Issues liest und erstellt. Das ist ein klassischer Anwendungsfall: repetitive API-Calls, Auth-Management, Daten-Transformation.
Abhängigkeiten installieren
npm install node-fetch
npm install -D @types/node-fetch
Vollständiger Jira MCP Server
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
// Auth via Umgebungsvariablen — NIEMALS hardcoden
const JIRA_BASE_URL = process.env.JIRA_BASE_URL ?? "";
const JIRA_EMAIL = process.env.JIRA_EMAIL ?? "";
const JIRA_TOKEN = process.env.JIRA_TOKEN ?? "";
if (!JIRA_BASE_URL || !JIRA_EMAIL || !JIRA_TOKEN) {
console.error("Fehler: JIRA_BASE_URL, JIRA_EMAIL und JIRA_TOKEN müssen gesetzt sein");
process.exit(1);
}
const authHeader = `Basic ${Buffer.from(`${JIRA_EMAIL}:${JIRA_TOKEN}`).toString("base64")}`;
async function jiraFetch(path: string, options: RequestInit = {}) {
const res = await fetch(`${JIRA_BASE_URL}/rest/api/3${path}`, {
...options,
headers: {
"Authorization": authHeader,
"Content-Type": "application/json",
"Accept": "application/json",
...(options.headers ?? {})
}
});
if (!res.ok) {
throw new Error(`Jira API Fehler ${res.status}: ${await res.text()}`);
}
return res.json();
}
const server = new Server(
{ name: "jira-mcp-server", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_jira_issues",
description: "Liest Issues aus einem Jira-Projekt. Gibt Titel, Status, Assignee und Beschreibung zurück.",
inputSchema: {
type: "object",
properties: {
projectKey: {
type: "string",
description: "Jira Projekt-Key (z.B. 'DEV' oder 'BACKEND')"
},
status: {
type: "string",
description: "Optionaler Status-Filter: 'To Do', 'In Progress', 'Done'"
},
maxResults: {
type: "number",
description: "Maximale Anzahl Ergebnisse (Standard: 20)",
default: 20
}
},
required: ["projectKey"]
}
},
{
name: "create_jira_issue",
description: "Erstellt ein neues Jira-Issue. Gibt die Issue-URL zurück.",
inputSchema: {
type: "object",
properties: {
projectKey: {
type: "string",
description: "Jira Projekt-Key"
},
title: {
type: "string",
description: "Issue-Titel (Summary)"
},
description: {
type: "string",
description: "Ausführliche Beschreibung des Issues"
},
type: {
type: "string",
enum: ["Story", "Bug", "Task", "Epic"],
description: "Issue-Typ",
default: "Task"
}
},
required: ["projectKey", "title", "description"]
}
}
]
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "get_jira_issues") {
const { projectKey, status, maxResults = 20 } = args as {
projectKey: string; status?: string; maxResults?: number;
};
let jql = `project = ${projectKey} ORDER BY updated DESC`;
if (status) jql += ` AND status = "${status}"`;
const data = await jiraFetch(
`/search?jql=${encodeURIComponent(jql)}&maxResults=${maxResults}&fields=summary,status,assignee,description`
);
const issues = data.issues.map((issue: any) => ({
key: issue.key,
title: issue.fields.summary,
status: issue.fields.status.name,
assignee: issue.fields.assignee?.displayName ?? "Nicht zugewiesen",
url: `${JIRA_BASE_URL}/browse/${issue.key}`
}));
const text = issues
.map((i: any) => `${i.key} [${i.status}] — ${i.title}\n Assignee: ${i.assignee}\n URL: ${i.url}`)
.join("\n\n");
return { content: [{ type: "text", text: text || "Keine Issues gefunden." }] };
}
if (name === "create_jira_issue") {
const { projectKey, title, description, type = "Task" } = args as {
projectKey: string; title: string; description: string; type?: string;
};
const body = {
fields: {
project: { key: projectKey },
summary: title,
description: {
type: "doc", version: 1,
content: [{ type: "paragraph", content: [{ type: "text", text: description }] }]
},
issuetype: { name: type }
}
};
const created = await jiraFetch("/issue", {
method: "POST",
body: JSON.stringify(body)
});
const url = `${JIRA_BASE_URL}/browse/${created.key}`;
return {
content: [{ type: "text", text: `Issue erstellt: ${created.key}\nURL: ${url}` }]
};
}
throw new Error(`Unbekanntes Tool: ${name}`);
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Jira MCP Server gestartet");
}
main().catch(console.error);
Trage den Server in ~/.claude/settings.json mit den Jira-Credentials ein:
{
"mcpServers": {
"jira": {
"command": "node",
"args": ["/pfad/jira-mcp-server/dist/index.js"],
"env": {
"JIRA_BASE_URL": "https://dein-unternehmen.atlassian.net",
"JIRA_EMAIL": "dein@email.com",
"JIRA_TOKEN": "dein-api-token"
}
}
}
}
Ab jetzt kann Claude in jeder Session direkt auf Jira zugreifen. Ein Prompt wie "Zeig mir alle offenen Bugs im Projekt DEV und erstelle ein neues Task für das Login-Problem" wird direkt ausgeführt — ohne Copy-Paste, ohne manuelle API-Calls.
Testing und Debugging
Debug-Modus aktivieren
Claude Code bietet einen eingebauten MCP-Debug-Modus. Starte Claude mit dem Flag:
claude --mcp-debug
Im Debug-Modus siehst du alle MCP-Nachrichten in Echtzeit: Tool-Listings, Parameter-Validierung, Antworten und Fehler. Unentbehrlich bei der Entwicklung.
Häufige Fehler und Lösungen
inputSchema entsprechen, oder das Schema hat Tippfehler. Lösung: JSON Schema gegen jsonschemavalidator.net prüfen. Häufigster Fehler: required-Felder im Schema nicht unter properties definiert.ListTools stimmt nicht mit dem Namen im CallTool-Handler überein. Lösung: Namen als Konstante definieren statt doppelt als String. Auch Leerzeichen und Sonderzeichen im Namen vermeiden — nur a-z, 0-9 und _.settings.json oder Build-Artefakt fehlt. Lösung: node /absoluter/pfad/dist/index.js direkt im Terminal ausführen — dann siehst du den Fehler direkt. Den Build-Schritt nie vergessen.console.log() statt console.error() für Debug-Output. Das MCP-Protokoll nutzt stdout exclusiv. Jede nicht-JSON-Zeile auf stdout bricht die Verbindung. Alle Logs immer auf stderr.Manuell testen ohne Claude
Du kannst deinen Server direkt testen indem du JSON-Nachrichten über stdin sendest:
# Server starten und Tools listen
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js
# Tool aufrufen
echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_jira_issues","arguments":{"projectKey":"DEV"}}}' | node dist/index.js
MCP Server veröffentlichen
Ein gut gebauter MCP Server ist für andere Teams wertvoll. Das npm-Ökosystem ist die natürliche Heimat für MCP Server — einfach installierbar, versionierbar, ohne Setup-Aufwand für Nutzer.
Vorbereitung
# npm Account benötigt: npmjs.com
npm login
# package.json für publish vorbereiten
# "files" definiert was ins npm-Paket kommt
Füge in package.json hinzu:
{
"name": "@dein-username/jira-mcp-server",
"version": "1.0.0",
"description": "MCP Server für Jira Cloud Integration",
"files": ["dist"],
"keywords": ["mcp", "mcp-server", "jira", "claude", "model-context-protocol"],
"repository": {
"type": "git",
"url": "https://github.com/dein-username/jira-mcp-server"
}
}
Publizieren
# Bauen und testen
npm run build
node dist/index.js # kurz starten um zu prüfen
# Dry Run — zeigt was hochgeladen würde
npm publish --dry-run
# Veröffentlichen
npm publish --access public
Nach dem Publish können andere Teams deinen Server direkt nutzen, ohne Build-Schritt:
{
"mcpServers": {
"jira": {
"command": "npx",
"args": ["-y", "@dein-username/jira-mcp-server"],
"env": { "JIRA_TOKEN": "..." }
}
}
}
Das Model Context Protocol ist noch jung, aber das Ökosystem wächst schnell. Wer heute einen hochwertigen MCP Server für ein verbreitetes Tool baut und veröffentlicht, setzt einen Standard, bevor es andere tun.
Der nächste Schritt: Wenn du Claude Code bereits für Automatisierungen nutzt, lohnt sich auch ein Blick auf die n8n-Integration — damit lassen sich MCP-Workflows mit visuellen Low-Code-Pipelines kombinieren.
Eigene KI-Workflows automatisieren?
Mit unserem Trial bekommst du Zugang zu vorkonfigurierten MCP-Servern, Workflow-Templates und einem Team das dir beim Setup hilft — kein Cold Start.
14 Tage kostenlos starten