Wer ernsthaft mit KI-Agenten arbeitet, stößt schnell an eine Grenze: Die Modelle wissen vieles, aber sie kennen deine interne Datenbank nicht. Dein Monitoring-System. Deine Jira-Tickets. Deine proprietären APIs. Genau hier setzt das Model Context Protocol (MCP) an — ein offener Standard, mit dem du Claude und andere KI-Agenten mit eigenen Tools und Datenquellen ausstatten kannst.

In diesem Guide lernst du, wie du eigene MCP-Server mit TypeScript und dem offiziellen @modelcontextprotocol/sdk baust und direkt in Claude Code integrierst. Wir gehen von den Grundlagen bis zu produktionsreifen Patterns für Datenbankzugriffe, GitHub-Integration und interne Tools.

1. MCP Grundlagen & Architektur

Das Model Context Protocol wurde von Anthropic entwickelt und im November 2024 als Open-Source-Standard veröffentlicht. Die Idee ist elegant: Anstatt jede KI-Anwendung mit eigenen API-Wrappern zu bauen, gibt es ein standardisiertes Protokoll für die Kommunikation zwischen KI-Modellen und externen Systemen.

MCP ARCHITEKTUR
Claude Code (Host)  ↔  MCP Client  ↔  MCP Server (dein Code)
↓ stdio / HTTP+SSE Transport ↓
Datenbank   Externe API   Dateisystem   Interne Tools

Die drei Kernkonzepte: Tools, Resources, Prompts

Ein MCP-Server kann drei Arten von Fähigkeiten bereitstellen. Je nach Anwendungsfall nutzt du eine oder eine Kombination davon:

Tool Tools — Aktionen ausführen

Tools sind Funktionen, die Claude aufrufen kann, um Aktionen in der Welt durchzuführen. Datenbankabfragen, API-Aufrufe, Dateien schreiben, Code ausführen. Das Modell übergibt Parameter, der MCP-Server führt die Aktion aus und gibt das Ergebnis zurück.

  • Datenbankabfragen (SELECT, INSERT, UPDATE)
  • REST-API-Aufrufe (GitHub, Jira, Stripe, ...)
  • Shell-Befehle und Skripte ausführen
  • E-Mails senden, Kalendereinträge erstellen

Resource Resources — Daten bereitstellen

Resources sind Datenquellen, die Claude lesen kann — ähnlich wie GET-Endpunkte in einer REST-API. Sie haben URIs wie db://users/123 oder file://config.yaml und liefern Inhalte, die Claude in seinen Kontext laden kann.

  • Datenbankeinträge und Tabellenschemas
  • Konfigurationsdateien und Dokumentation
  • Logs und Monitoring-Daten
  • Produktkataloge und interne Wissensdatenbanken

Prompt Prompts — Wiederverwendbare Templates

Prompts sind vordefinierte Prompt-Templates mit Parametern, die Claude nutzen kann. Sie ermöglichen konsistente Workflows für wiederkehrende Aufgaben.

  • Code-Review-Templates für bestimmte Sprachen
  • Commit-Message-Generierung nach eigenem Standard
  • Incident-Analyse-Workflows
  • Onboarding-Sequenzen für neue Mitarbeiter

stdio vs. HTTP Transport

Eigenschaft stdio Transport HTTP+SSE Transport
Einsatz Lokale Development-Tools Remote-Services, Teams
Deployment Als lokaler Prozess Als Web-Service
Latenz Sehr gering (lokal) Netzwerk-abhängig
Multi-User Nur ein Client Mehrere Clients
Sicherheit Kein Netzwerk HTTPS + Auth nötig
Einstieg Sehr einfach Mehr Aufwand

Empfehlung für den Start: Beginne immer mit stdio Transport. Du kannst später auf HTTP wechseln, wenn du den Server im Team teilen oder remote betreiben möchtest. Für die meisten Entwickler-Workflows reicht stdio vollkommen aus.

2. Erster MCP Server in TypeScript

Der schnellste Weg zum ersten MCP-Server. Wir brauchen nur das offizielle SDK und ein paar Zeilen TypeScript. Die Lernkurve ist überraschend flach — in 15 Minuten läuft dein erster Server.

Setup und Abhängigkeiten

Terminal
# Neues Projekt anlegen mkdir mein-mcp-server && cd mein-mcp-server npm init -y # MCP SDK und TypeScript installieren npm install @modelcontextprotocol/sdk npm install -D typescript @types/node tsx # TypeScript-Config npx tsc --init --target ES2022 --module commonjs --outDir dist --strict

Der minimale MCP-Server

Hier ist der vollständige Code für einen funktionierenden MCP-Server mit einem ersten Tool:

src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; // Server-Instanz erstellen const server = new McpServer({ name: 'mein-erster-mcp-server', version: '1.0.0', }); // Erstes Tool: Einfacher Echo-Service server.tool( 'echo', 'Gibt den eingegebenen Text zurück (Test-Tool)', { message: z.string().describe('Der Text, der zurückgegeben werden soll'), }, async ({ message }) => ({ content: [ { type: 'text', text: `Echo: ${message}`, }, ], }) ); // Server starten mit stdio Transport async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('MCP Server gestartet (stdio)'); } main().catch(console.error);

Build und lokaler Test

package.json (scripts)
{ "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", "zod": "^3.23.8" } }
Terminal — Build
# TypeScript kompilieren npm run build # Manueller Test: JSON-RPC-Request per stdin senden echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | node dist/index.js

Erwartete Ausgabe: Du siehst eine JSON-Antwort mit dem echo-Tool in der Liste. Das bedeutet: Dein Server läuft und kommuniziert korrekt über stdio.

3. Tools definieren mit Zod-Schemas

Einfache Tools sind der Einstieg — aber in echten Projekten brauchst du komplexere Input-Validierung, Fehlerbehandlung und mehrere Tools in einem Server. Zod ist hier das Werkzeug der Wahl: Es liefert TypeScript-Typen und Runtime-Validierung in einem.

Komplexe Input-Schemas mit Zod

src/tools/database-tools.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; export function registerDatabaseTools(server: McpServer) { // Tool 1: Benutzer suchen server.tool( 'search_users', 'Sucht Benutzer in der Datenbank nach verschiedenen Kriterien', { query: z.string().min(1).describe('Suchbegriff (Name, E-Mail oder ID)'), limit: z .number() .int() .min(1) .max(100) .default(10) .describe('Maximale Anzahl Ergebnisse (1-100)'), status: z .enum(['active', 'inactive', 'all']) .default('active') .describe('Filtert nach Benutzerstatus'), include_deleted: z .boolean() .default(false) .describe('Gelöschte Benutzer einschließen'), }, async ({ query, limit, status, include_deleted }) => { try { // Datenbankabfrage (hier: Prisma-Beispiel) const users = await prisma.user.findMany({ where: { OR: [ { name: { contains: query, mode: 'insensitive' } }, { email: { contains: query, mode: 'insensitive' } }, ], ...(status !== 'all' && { isActive: status === 'active' }), ...(include_deleted === false && { deletedAt: null }), }, take: limit, select: { id: true, name: true, email: true, isActive: true, createdAt: true }, }); return { content: [ { type: 'text', text: JSON.stringify({ count: users.length, users }, null, 2), }, ], }; } catch (error) { // Fehler sauber zurückgeben — nie werfen! return { content: [{ type: 'text', text: `Fehler: ${error.message}` }], isError: true, }; } } ); // Tool 2: Benutzer erstellen server.tool( 'create_user', 'Erstellt einen neuen Benutzer in der Datenbank', { name: z.string().min(2).max(100).describe('Vollständiger Name'), email: z.string().email().describe('Gültige E-Mail-Adresse'), role: z .enum(['user', 'admin', 'moderator']) .default('user') .describe('Benutzerrolle'), send_welcome_email: z .boolean() .default(true) .describe('Willkommens-E-Mail senden'), }, async ({ name, email, role, send_welcome_email }) => { try { const existing = await prisma.user.findUnique({ where: { email } }); if (existing) { return { content: [{ type: 'text', text: `Fehler: E-Mail ${email} bereits vergeben` }], isError: true, }; } const user = await prisma.user.create({ data: { name, email, role, isActive: true }, }); if (send_welcome_email) { await sendEmail({ to: email, template: 'welcome', data: { name } }); } return { content: [{ type: 'text', text: `Benutzer erstellt: ${user.id} (${user.name} <${user.email}>)`, }], }; } catch (error) { return { content: [{ type: 'text', text: `Datenbankfehler: ${error.message}` }], isError: true, }; } } ); }

Best Practices für Tool-Definitionen

✅ Do: Klare Beschreibungen schreiben

Claude entscheidet anhand der Beschreibungen, welches Tool für welche Aufgabe geeignet ist. Schlechte Beschreibungen führen zu falschem Tool-Einsatz. Jeder Parameter braucht ein .describe() mit konkreter Erklärung.

❌ Don't: Fehler werfen

Tools sollten niemals unkontrolliert Errors werfen. Nutze immer { content: [...], isError: true } als Rückgabewert. So kann Claude den Fehler lesen und sinnvoll reagieren, anstatt abzubrechen.

4. Ressourcen bereitstellen

Während Tools Aktionen ausführen, stellen Resources Daten bereit, die Claude in seinen Kontext laden kann. Das ist ideal für Datenbankschemas, Dokumentation oder Konfigurationen, die Claude kennen muss, um sinnvolle Tool-Aufrufe zu machen.

Statische und dynamische Resources

src/resources/database-resources.ts
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'; export function registerResources(server: McpServer) { // Statische Resource: Datenbankschema server.resource( 'db-schema', 'db://schema', 'Komplettes Datenbankschema aller Tabellen (Prisma)', async () => ({ contents: [ { uri: 'db://schema', mimeType: 'text/plain', text: await fs.readFile('./prisma/schema.prisma', 'utf-8'), }, ], }) ); // Dynamische Resource mit URI-Parametern server.resource( 'user-profile', new ResourceTemplate('db://users/{userId}', { list: undefined }), 'Benutzerprofil aus der Datenbank (dynamisch per userId)', async (uri, { userId }) => { const user = await prisma.user.findUnique({ where: { id: userId as string }, include: { orders: { take: 5, orderBy: { createdAt: 'desc' } }, subscriptions: true, }, }); if (!user) { return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify({ error: `User ${userId} not found` }), }], }; } return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(user, null, 2), }], }; } ); // Resource: Alle aktiven Konfigurationswerte server.resource( 'app-config', 'config://app', 'Aktuelle App-Konfiguration (ohne Secrets)', async () => ({ contents: [{ uri: 'config://app', mimeType: 'application/json', text: JSON.stringify({ environment: process.env.NODE_ENV, version: process.env.APP_VERSION, features: { betaFeatures: process.env.ENABLE_BETA === 'true', maintenanceMode: process.env.MAINTENANCE === 'true', }, // NIEMALS Secrets hier einfügen! }, null, 2), }], }) ); }

Prompts für wiederverwendbare Workflows

src/prompts/code-review.ts
export function registerPrompts(server: McpServer) { server.prompt( 'code-review', 'Führt ein strukturiertes Code-Review nach internen Standards durch', { language: z.enum(['typescript', 'python', 'go', 'rust']) .describe('Programmiersprache'), focus: z.enum(['security', 'performance', 'readability', 'all']) .default('all') .describe('Review-Schwerpunkt'), }, ({ language, focus }) => ({ messages: [ { role: 'user', content: { type: 'text', text: `Du bist ein erfahrener ${language}-Entwickler. Analysiere den folgenden Code mit Fokus auf: ${focus}. Prüfe dabei: ${focus === 'security' || focus === 'all' ? '- OWASP Top 10 Schwachstellen\n- Input-Validierung\n- Authentifizierung\n' : ''} ${focus === 'performance' || focus === 'all' ? '- N+1 Queries\n- Memory Leaks\n- Unnötige Re-Renders\n' : ''} ${focus === 'readability' || focus === 'all' ? '- Naming Conventions\n- Kommentare\n- Komplexität\n' : ''} Formatiere dein Review mit: 1. Zusammenfassung (2-3 Sätze) 2. Kritische Probleme (Blocker) 3. Verbesserungsvorschläge 4. Positives Feedback`, }, }, ], }) ); }

5. Claude Code Integration

Der fertige MCP-Server nützt nichts, solange er nicht in Claude Code eingebunden ist. Das geht über die Datei .claude/settings.json im Projektverzeichnis — oder global für alle Projekte.

Lokale Projekt-Konfiguration

.claude/settings.json
{ "mcpServers": { "mein-mcp-server": { "command": "node", "args": ["dist/index.js"], "cwd": "/absolute/pfad/zu/mein-mcp-server", "env": { "DATABASE_URL": "postgresql://user:pass@localhost:5432/mydb", "NODE_ENV": "development" } } } }

Globale Installation für alle Projekte

~/.claude.json
{ "mcpServers": { "mein-mcp-server": { "command": "node", "args": ["/home/user/mein-mcp-server/dist/index.js"], "env": { "DATABASE_URL": "${DATABASE_URL}" } }, "filesystem": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-filesystem", "/home/user/projekte" ] } } }

Via CLI hinzufügen

Terminal
# MCP-Server zu Claude Code hinzufügen (lokal, für das Projekt) claude mcp add mein-mcp-server -- node /pfad/zu/dist/index.js # Global hinzufügen (-s user) claude mcp add -s user mein-mcp-server -- node /pfad/zu/dist/index.js # Mit Umgebungsvariablen claude mcp add mein-mcp-server \ -e DATABASE_URL=postgresql://localhost/mydb \ -- node /pfad/zu/dist/index.js # Liste aller konfigurierten MCP-Server claude mcp list # Status prüfen claude mcp get mein-mcp-server

Test in Claude Code

Sobald Claude Code neu gestartet wird, stehen die Tools zur Verfügung. Du kannst direkt in natürlicher Sprache testen:

💬 Beispiel-Prompts zum Testen

  • “Suche alle aktiven Benutzer mit der E-Mail @example.com”
  • “Erstelle einen neuen Benutzer: Max Muster, max@example.com, Rolle: admin”
  • “Lies das Datenbankschema und erkläre die Tabellenstruktur”
  • “Führe ein Security-Review für diese TypeScript-Datei durch”

Wichtig: Nach jeder Änderung am MCP-Server-Code muss der Server neu kompiliert (npm run build) und Claude Code neu gestartet werden. Der Server-Prozess wird bei jedem Claude-Code-Start frisch gestartet.

6. Praktische Beispiele & Patterns

Hier sind vier produktionsreife MCP-Server-Patterns, die in realen Projekten bewährt sind. Jedes Lösungsmuster zeigt die Kernlogik — die Integration in die Server-Instanz funktioniert wie in den Abschnitten zuvor beschrieben.

Pattern 1: Database-MCP für Prisma

src/tools/prisma-tools.ts
import { PrismaClient } from '@prisma/client'; import { z } from 'zod'; const prisma = new PrismaClient(); // Pattern: Rohe SQL-Abfragen mit Sicherheitsprüfung server.tool( 'run_query', 'Führt eine SELECT-Abfrage aus (nur lesend, kein DML)', { sql: z.string().describe('SQL SELECT-Statement'), params: z.array(z.unknown()).default([]).describe('Abfrage-Parameter (Prepared Statement)'), }, async ({ sql, params }) => { // Sicherheitsprüfung: nur SELECT erlaubt const normalized = sql.trim().toUpperCase(); if (!normalized.startsWith('SELECT')) { return { content: [{ type: 'text', text: 'Fehler: Nur SELECT-Abfragen erlaubt' }], isError: true, }; } try { const result = await prisma.$queryRawUnsafe(sql, ...params); return { content: [{ type: 'text', text: JSON.stringify(result, null, 2), }], }; } catch (e) { return { content: [{ type: 'text', text: `SQL-Fehler: ${e.message}` }], isError: true, }; } } );

Pattern 2: GitHub-API-MCP

src/tools/github-tools.ts
import { Octokit } from '@octokit/rest'; const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, }); // Tool: Pull Requests auflisten server.tool( 'list_pull_requests', 'Listet offene Pull Requests für ein Repository auf', { owner: z.string().describe('GitHub-Organisation oder Benutzername'), repo: z.string().describe('Repository-Name'), state: z.enum(['open', 'closed', 'all']).default('open'), }, async ({ owner, repo, state }) => { const { data } = await octokit.pulls.list({ owner, repo, state, per_page: 20, }); const summary = data.map(pr => ({ number: pr.number, title: pr.title, author: pr.user?.login, created: pr.created_at, labels: pr.labels.map(l => l.name), })); return { content: [{ type: 'text', text: JSON.stringify({ total: summary.length, prs: summary }, null, 2), }], }; } ); // Tool: Issue erstellen server.tool( 'create_issue', 'Erstellt ein neues GitHub-Issue', { owner: z.string(), repo: z.string(), title: z.string().min(1).max(256), body: z.string().describe('Issue-Beschreibung in Markdown'), labels: z.array(z.string()).default([]), assignees: z.array(z.string()).default([]), }, async ({ owner, repo, title, body, labels, assignees }) => { const { data } = await octokit.issues.create({ owner, repo, title, body, labels, assignees, }); return { content: [{ type: 'text', text: `Issue #${data.number} erstellt: ${data.html_url}`, }], }; } );

Pattern 3: Internal-Tools-MCP (Monitoring & Deployment)

src/tools/ops-tools.ts
import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); // Tool: Service-Status prüfen server.tool( 'check_service_status', 'Prüft den Status eines systemd-Services', { service: z.string().regex(/^[a-z0-9_-]+$/).describe('Service-Name (nur a-z, 0-9, -, _)'), }, async ({ service }) => { try { // Input-Validierung bereits via Regex in Zod const { stdout } = await execAsync( `systemctl status ${service} --no-pager -l` ); return { content: [{ type: 'text', text: stdout }] }; } catch (e: any) { // Exit-Code 3 = Service stopped (kein echter Fehler) return { content: [{ type: 'text', text: e.stdout || e.message }], isError: e.code !== 3, }; } } ); // Tool: Deployment-Log lesen server.tool( 'get_deployment_log', 'Liest die letzten N Zeilen des Deployment-Logs', { lines: z.number().int().min(10).max(500).default(50), filter: z.string().optional().describe('Optionaler Grep-Filter'), }, async ({ lines, filter }) => { const logFile = '/var/log/deployment.log'; const cmd = filter ? `tail -n ${lines} ${logFile} | grep -i "${filter.replace(/"/g, '\\"')}"` : `tail -n ${lines} ${logFile}`; try { const { stdout } = await execAsync(cmd); return { content: [{ type: 'text', text: stdout || 'Keine Einträge gefunden' }] }; } catch (e: any) { return { content: [{ type: 'text', text: `Fehler: ${e.message}` }], isError: true, }; } } );

Pattern 4: Vollständiger Server mit allen Capabilities

src/index.ts (vollständig)
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { registerDatabaseTools } from './tools/database-tools.js'; import { registerGithubTools } from './tools/github-tools.js'; import { registerOpsTools } from './tools/ops-tools.js'; import { registerResources } from './resources/database-resources.js'; import { registerPrompts } from './prompts/code-review.js'; const server = new McpServer({ name: 'company-mcp-server', version: '2.0.0', }); // Alle Module registrieren registerDatabaseTools(server); registerGithubTools(server); registerOpsTools(server); registerResources(server); registerPrompts(server); async function main() { // Graceful shutdown process.on('SIGINT', async () => { await prisma.$disconnect(); process.exit(0); }); const transport = new StdioServerTransport(); await server.connect(transport); console.error(`[${new Date().toISOString()}] MCP Server v2.0.0 gestartet`); console.error(`Tools: DB + GitHub + Ops | Resources: Schema + User-Profile + Config`); } main().catch((err) => { console.error('Fatal:', err); process.exit(1); });

Deployment-Optionen im Überblick

1

Lokal (stdio) — Entwicklung

Direkt per node dist/index.js gestartet. Claude Code managed den Prozess automatisch. Kein Netzwerk nötig, maximale Performance.

2

npm-Package — Team-Distribution

MCP-Server als privates npm-Package veröffentlichen. Andere Entwickler nutzen dann npx @company/mcp-server als Command in ihrer Claude-Code-Konfiguration.

3

Docker + HTTP — Produktion

MCP-Server als HTTP-Service deployen. Mehrere Entwickler teilen sich eine Instanz. Ideal für Datenbankzugriffe mit Connection-Pooling.

4

Cloudflare Workers — Serverless

HTTP-MCP-Server als Cloudflare Worker deployen. Kein eigener Server nötig, globale Verteilung, kostenloser Tier für kleine Teams.

Praxis-Tipp: Starte immer lokal (stdio), bis du den Server produktiv nutzt. Die Umstellung auf HTTP ist später mit dem @modelcontextprotocol/sdk-HTTP-Adapter möglich, ohne die Tool-Definitionen zu ändern. Nur der Transport wechselt.

Fazit: MCP als Multiplikator für KI-Produktivität

MCP-Server sind kein akademisches Konzept — sie sind der praktische Weg, Claude von einem allgemeinen Assistenten zu einem Experten für dein spezifisches System zu machen. Ein gut entwickelter MCP-Server, der auf deine Datenbankstruktur, deine APIs und deine internen Workflows zugeschnitten ist, multipliziert die Produktivität von Entwicklern sprunghaft.

Die Einstiegskosten sind gering: Das @modelcontextprotocol/sdk ist ausgezeichnet dokumentiert, Zod macht Typ-Sicherheit einfach, und die Integration in Claude Code geht in wenigen Minuten. Die wichtigsten Lernpunkte aus diesem Guide:

KI-Agenten mit eigenen Tools ausstatten?

Starte mit SpockyMagicAI — entdecke, wie KI-Automatisierung dein Team mit mageschneiderten MCP-Servern, Claude Code und modernen Agentic-Workflows voranbringt.

Kostenlosen Trial starten →