Was Sie in diesem Beitrag erwartet
In unserem Grundlagenartikel „MCP Server: Was ist das Model Context Protocol und was kann man damit machen?“ haben wir erklärt, was das Model Context Protocol ist und welche drei Kernbausteine es gibt: Tools, Resources und Prompts. In diesem Tutorial gehen wir einen Schritt weiter: Wir bauen einen vollständigen MCP Server mit Python – von der Projektstruktur über die Implementierung konkreter Tools bis zur Anbindung an einen Client.
Dieser Beitrag richtet sich an Entwickler und IT-Teams, die eigene MCP-Server für den Unternehmenseinsatz erstellen möchten. Die Beispiele sind praxisnah und可以直接 in Produktionsumgebungen übernommen werden.
Voraussetzungen
Bevor wir starten, stellen Sie sicher, dass folgende Voraussetzungen erfüllt sind:
- Python 3.10+ – MCP SDKsetzt Python 3.10 oder höher voraus
- pip oder uv – für die Paketverwaltung
- Node.js 18+ – erforderlich, wenn Sie MCP-Server auch in TypeScript/JavaScript bauen möchten
- Grundverständnis für asynchrone Programmierung (async/await)
- Ein MCP-Client – zum Testen, z. B. unser Artikel zum MCP-Client erklärt die verfügbaren Optionen
Das MCP Python SDK: setup.py oder UV – Projekt initialisieren
Anthropic bietet ein offizielles Python SDK für MCP-Server. Die Installation ist in wenigen Schritten erledigt:
1
2
3
4
5 # Mit pip
pip install mcp
# Oder mit uv (schneller)
uv pip install mcp
Erstellen Sie anschließend ein neues Projektverzeichnis:
1
2
3 mkdir mein-mcp-server
cd mein-mcp-server
uv init --app
Die Projektstruktur sollte am Ende so aussehen:
1
2
3
4
5
6
7
8 mein-mcp-server/
├── pyproject.toml
├── src/
│ └── mein_mcp_server/
│ ├── __init__.py
│ └── server.py # Hauptlogik des Servers
├── .mcp.json # Lokale Server-Konfiguration
└── README.md
Schritt 1: Die Server-Grundstruktur definieren
Ein MCP-Server dreht sich um drei Kernkonzepte. Diese drei Bausteine werden im SDK als dekorierte Funktionen registriert:
- Tools – Aktionen, die das LLM ausführen darf (z. B. Datenbankabfragen, API-Aufrufe, Dateioperationen)
- Resources – Datenquellen, die das LLM lesen, aber nicht verändern darf (z. B. Datenbankinhalte, Dateien, Konfigurationen)
- Prompts – Vordefinierte Eingabeaufforderungen, die das LLM gezielt anstoßen kann (z. B. „Analysiere diesen Monatsbericht“)
Beginnen wir mit der
1 | server.py |
1
2
3
4
5
6
7 # src/mein_mcp_server/server.py
from mcp.server import Server
from mcp.types import Tool, Resource
# Server-Instanz erstellen
# Der name muss mit dem Ordner-/Paketnamen übereinstimmen
server = Server("mein-mcp-server")
Schritt 2: Ein Tool implementieren
Tools sind die mächtigste Komponente eines MCP-Servers. Sie ermöglichen es einem LLM, reale Aktionen auszuführen – von der Abfrage einer Datenbank bis zum Versand einer E-Mail. Ein Tool wird als async-Funktion definiert und mit dem
1 | @server.list_tools() |
Hier ein vollständiges Beispiel für ein Tool, das Kundendaten aus einem CRM-System abruft:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95 # src/mein_mcp_server/server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
from typing import Any
import asyncpg # PostgreSQL-Adapter für Python
server = Server("mein-mcp-server")
# Datenbank-Verbindungspool (wird beim Start initialisiert)
db_pool: asyncpg.Pool | None = None
async def init_db():
"""Verbindungspool zur Datenbank aufbauen."""
global db_pool
db_pool = await asyncpg.create_pool(
host="localhost",
port=5432,
database="crm",
user="crm_user",
password="sicheres_passwort", # Aus .env laden!
min_size=5,
max_size=20
)
@server.list_tools()
async def list_tools() -> list[Tool]:
"""Alle verfügbaren Tools registrieren."""
return [
Tool(
name="kunden_abrufen",
description="Ruft Kundendaten anhand der Kundennummer ab. Gibt Name, E-Mail, Firmenname und letzte Bestellung zurück.",
inputSchema={
"type": "object",
"properties": {
"kundennummer": {
"type": "string",
"description": "Die eindeutige Kundennummer (z. B. KDN-12345)"
}
},
"required": ["kundennummer"]
}
),
Tool(
name="bestellung_erstellen",
description="Erstellt eine neue Bestellung für einen existierenden Kunden.",
inputSchema={
"type": "object",
"properties": {
"kundennummer": {"type": "string"},
"artikelnummern": {
"type": "array",
"items": {"type": "string"},
"description": "Liste der zu bestellenden Artikelnummern"
},
"lieferadresse": {"type": "string"}
},
"required": ["kundennummer", "artikelnummern"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]:
"""Ein spezifisches Tool ausführen."""
if name == "kunden_abrufen":
kundennummer = arguments["kundennummer"]
async with db_pool.acquire() as conn:
row = await conn.fetchrow(
"SELECT name, email, firma, letzte_bestellung "
"FROM kunden WHERE kundennummer = $1",
kundennummer
)
if not row:
return [TextContent(type="text", text=f"Kunde '{kundennummer}' nicht gefunden.")]
return [TextContent(
type="text",
text=f"Kunde: {row['name']}\nFirma: {row['firma']}\n"
f"E-Mail: {row['email']}\nLetzte Bestellung: {row['letzte_bestellung']}"
)]
elif name == "bestellung_erstellen":
kundennummer = arguments["kundennummer"]
artikelnummern = arguments["artikelnummern"]
lieferadresse = arguments.get("lieferadresse", "Standardadresse")
async with db_pool.acquire() as conn:
result = await conn.fetchrow(
"INSERT INTO bestellungen (kundennummer, artikelnummern, lieferadresse) "
"VALUES ($1, $2, $3) RETURNING bestellnummer",
kundennummer, artikelnummern, lieferadresse
)
return [TextContent(
type="text",
text=f"Bestellung erstellt: {result['bestellnummer']}"
)]
else:
raise ValueError(f"Unbekanntes Tool: {name}")
Wichtige Hinweise zum Tool-InputSchema:
- Das
folgt dem JSON Schema Standard. Das LLM liest dieses Schema und用它, um die Parameter korrekt zu generieren.1inputSchema
- Markieren Sie Pflichtfelder immer mit
– das LLM otherwise might leave them blank.1"required"
- Die
-Felder der Parameter sind entscheidend: Sie sind die einzige Information, die das LLM darüber bekommt, wie es die Parameter füllen soll. Formulieren Sie sie konkret und actionorientiert.1description
Schritt 3: Resources bereitstellen
Resources sind schreibgeschützte Datenquellen. Anders als Tools werden Resources nicht vom LLM „aufgerufen“, sondern dem Modell als Kontext bereitgestellt – ähnlich wie ein Dateisystem, das gemountet wird. Das LLM kann sie lesen, aber keine Seiteneffekte auslösen.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 from mcp.types import Resource
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri="crm://kunden/verzeichnis",
name="Kundenverzeichnis",
description="Vollständiges Verzeichnis aller Kunden mit Stammdaten",
mimeType="application/json"
),
Resource(
uri="crm://bestellungen/offen",
name="Offene Bestellungen",
description="Alle Bestellungen mit Status 'offen' oder 'in Bearbeitung'",
mimeType="application/json"
),
Resource(
uri="config://unternehmen",
name="Unternehmenskonfiguration",
description="Interne Konfigurationsparameter (Rabattstufen, Steuersätze, Versandregeln)",
mimeType="application/json"
)
]
@server.read_resource()
async def read_resource(uri: str) -> str:
"""Den Inhalt einer Resource auf Basis ihrer URI zurückgeben."""
if uri == "crm://kunden/verzeichnis":
async with db_pool.acquire() as conn:
rows = await conn.fetch("SELECT * FROM kunden")
return json.dumps([dict(r) for r in rows])
elif uri == "crm://bestellungen/offen":
async with db_pool.acquire() as conn:
rows = await conn.fetch(
"SELECT * FROM bestellungen WHERE status IN ('offen', 'in_bearbeitung')"
)
return json.dumps([dict(r) for r in rows])
elif uri == "config://unternehmen":
return json.dumps({
"steuersatz_standard": 19,
"steuersatz_ermäßigt": 7,
"versandkosten_gratis_ab": 100,
"rabatt_grosskunde_stufe1": 5,
"rabatt_grosskunde_stufe2": 10
})
else:
raise ValueError(f"Unknown resource URI: {uri}")
Resources ermöglichen es dem LLM, auf aktuelle Daten zuzugreifen, ohne dass es dafür eine explizite Aktion auslösen muss – der Kontext wird automatisch mitgeliefert, wenn das Modell die Resource-Menge abruft.
Schritt 4: Prompts definieren
Prompts sind vordefinierte Vorlagen, die einem LLM gezielt eine Aufgabe stellen. Sie sind nützlich, um wiederkehrende Arbeitsabläufe zu standardisieren.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 from mcp.types import Prompt, PromptMessage
@server.list_prompts()
async def list_prompts() -> list[Prompt]:
return [
Prompt(
name="monatsbericht_kunde",
description="Generiert einen Monatsbericht für einen bestimmten Kunden",
arguments=[
{"name": "kundennummer", "description": "Kundennummer des zu berichtenden Kunden", "required": True}
]
),
Prompt(
name="angebot_erstellen",
description="Erstellt ein Angebotsdokument basierend auf Artikelauswahl und Kundenrabatt",
arguments=[
{"name": "kundennummer", "description": "Kundennummer", "required": True},
{"name": "artikelnummern", "description": "Liste der anzubietenden Artikelnummern", "required": True}
]
)
]
@server.get_prompt()
async def get_prompt(name: str, arguments: dict[str, str]) -> str:
"""Gibt den vollständigen Prompt-Text zurück."""
if name == "monatsbericht_kunde":
return (
f"Erstelle einen Monatsbericht für den Kunden mit der Nummer "
f"{arguments['kundennummer']}. Berücksichtige dabei die Bestellhistorie, "
f"offene Posten und eventuelle Beschwerden. Fasse die wichtigsten "
f"Erkenntnisse zusammen und schlage nächste Schritte vor."
)
elif name == "angebot_erstellen":
return (
f"Erstelle ein professionelles Angebotsdokument für Kunde "
f"{arguments['kundennummer']} über folgende Artikel: "
f"{', '.join(arguments['artikelnummern'])}. "
f"Berücksichtige den passenden Rabatt aus der Unternehmenskonfiguration."
)
raise ValueError(f"Unknown prompt: {name}")
Schritt 5: Den Server startklar machen
Um den Server als eigenständigen Prozess zu starten, braucht es noch eine
1 | __main__.py |
1
2
3
4
5
6
7
8
9
10
11
12
13 # src/mein_mcp_server/__main__.py
import asyncio
from .server import server, init_db
async def main():
# Datenbank-Verbindung vor dem Serverstart aufbauen
await init_db()
# Server über stdio transportieren (Standard für MCP)
async with server.run_stdio():
await server.wait_for shutdown()
if __name__ == "__main__":
asyncio.run(main())
Der
1 | server.run_stdio() |
Starten lässt sich der Server dann so:
1
2
3
4
5 # Im Projektverzeichnis
uv run python -m mein_mcp_server
# Oder direkt
python -m src.mein_mcp_server
Schritt 6: MCP-Server mit Claude Desktop verbinden
Um den Server mit einem Client zu verbinden, braucht es eine Konfigurationsdatei. Für Claude Desktop (Anthropic) ist das die
1 | ~/.config/claude-desktop/claude_desktop_config.json |
1
2
3
4
5
6
7
8
9
10
11
12 {
"mcpServers": {
"mein-mcp-server": {
"command": "uv",
"args": ["run", "python", "-m", "mein_mcp_server"],
"env": {
"DB_PASSWORD": "sicheres_passwort"
},
"cwd": "/path/zum/projekt"
}
}
}
Für Cursor und VS Code gibt es vergleichbare Konfigurationsmöglichkeiten. Unser MCP-Client-Artikel erklärt die Details für jede Plattform.
Schritt 7: Lokale Konfiguration mit .mcp.json
Neben der Client-Konfiguration ist es sinnvoll, eine
1 | .mcp.json |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 {
"mcpServers": {
"mein-mcp-server": {
"command": "uv",
"args": ["run", "python", "-m", "mein_mcp_server"],
"env": {
"DATABASE_URL": "postgresql://crm_user:${DB_PASSWORD}@localhost:5432/crm"
},
"metadata": {
"description": "Unternehmens-CRM-Server für Kundendaten und Bestellungen",
"version": "1.0.0"
}
}
}
}
Achten Sie darauf, dass Sie Secrets niemals direkt in Konfigurationsdateien speichern. Nutzen Sie Umgebungsvariablen (wie im Beispiel
1 | ${DB_PASSWORD} |
1 | .env |
Schritt 8: Testing mit dem MCP Inspector
Bevor Sie den Server produktiv einsetzen, sollten Sie ihn mit dem offiziellen MCP Inspector testen. Damit können Sie jeden Tool-Aufruf, Resource-Zugriff und Prompt einzeln prüfen:
1
2
3
4
5 # MCP Inspector installieren und starten
npx @anthropic-ai/mcp-inspector
# Dann den Server starten (im separaten Terminal)
uv run python -m mein_mcp_server
Der Inspector öffnet eine Web-Oberfläche, über die Sie:
- Manuell Tool-Aufrufe mit Parametern testen
- Alle registrierten Tools und Resources auflisten lassen
- Die JSON-RPC-Kommunikation zwischen Client und Server einsehen
- Fehlermeldungen und Response-Zeiten analysieren
Best Practices für produktive MCP-Server
Ein MCP-Server im Unternehmensumfeld bringt zusätzliche Anforderungen mit. Unser MCP-Security-Artikel geht ausführlich auf Authentifizierung und Zugriffskontrolle ein. Hier die wichtigsten Grundregeln:
- Input-Validierung – Validieren Sie alle Parameter aus dem Tool-Aufruf, bevor Sie sie weiterverarbeiten. Das LLM kann fehlerhafte oder bösartige Parameter generieren.
- Timeouts setzen – Jeder Tool-Aufruf sollte ein vernünftiges Timeout haben, damit ein blockierter Aufruf nicht den gesamten Server lahmlegt.
- Secrets schützen – Zugangsdaten und API-Keys niemals in Code oder Konfigurationsdateien hardcodieren. Nutzen Sie Umgebungsvariablen oder einen Secrets Manager.
- Logging und Monitoring – Protokollieren Sie alle Tool-Aufrufe mit Timestamp, User-Agent und Ergebnis. Ein MCP-Gateway kann das zentralisieren, wenn Sie mehrere Server betreiben.
- Fehlerbehandlung – Fangen Sie Fehler auf Serverebene ab und geben Sie dem Client lesbare Fehlermeldungen zurück. Niemals Stacktraces exponieren.
- Versionierung – Nutzen Sie semantische Versionierung für Ihr Server-Backend, damit Clients wissen, welche Tools verfügbar sind.
Erweiterung: Mehrere Tools in einem Server bündeln
Es spricht nichts dagegen, einen einzelnen MCP-Server mit Dutzenden von Tools zu betreiben. Im Gegenteil: Ein gut strukturierter Server mit kohärenten Tools ist einfacher zu verwalten als viele kleine Server. Hier ein erweitertes Beispiel, das zeigt, wie Sie Module sauber aufteilen:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 # server.py – Modularer Aufbau
from mcp.server import Server
server = Server("unternehmens-server")
# Einzelne Module importieren und registrieren
# Jedes Modul bringt seine eigenen Tools, Resources und Prompts mit
from . import crm_tools, file_tools, mail_tools
# Tool-Registries zusammenführen
@server.list_tools()
async def list_tools():
tools = []
tools.extend(crm_tools.get_tools())
tools.extend(file_tools.get_tools())
tools.extend(mail_tools.get_tools())
return tools
Diese modulare Architektur ist besonders dann sinnvoll, wenn verschiedene Teams an unterschiedlichen Funktionsbereichen arbeiten, aber alle über dieselbe MCP-Schnittstelle erreichbar sein sollen.
Vergleich: MCP Server vs. klassisches Function Calling
Eine berechtigte Frage, die sich viele stellen: Warum überhaupt MCP, wenn ich bereits Function Calling nutzen kann? Die wesentlichen Unterschiede im Überblick:
| Kriterium | Function Calling (klassisch) | MCP Server |
|---|---|---|
| Standardisierung | Proprietär pro Anbieter (OpenAI, Anthropic, etc.) | Herstellerunabhängiger offener Standard |
| Tool-Registrierung | Bei jedem Modell-Aufruf neu übergeben | Permanent beim Server registriert |
| Zustandsverwaltung | Manuell vom Entwickler zu verwalten | Server-seitig über Resources möglich |
| Transport | HTTP/REST pro Anbieter | stdio, HTTP/SSE – herstellerunabhängig |
| Ökosystem | Geschlossene Anbieter-Tool-Stores | MCP Registry mit öffentlichen Servern |
| Unternehmenseinsatz | Individualentwicklung pro Integration | Wiederverwendbare, zentral verwaltete Server |
MCP ist damit die bessere Wahl, wenn Sie eine langfristige, herstellerunabhängige Strategie für KI-Tool-Integration verfolgen – besonders im Mittelstand.
Zusammenfassung und nächste Schritte
Sie haben in diesem Beitrag gelernt, wie Sie einen vollständigen MCP-Server mit Python bauen:
- ✅ Projektstruktur und Python SDK eingerichtet
- ✅ Eigene Tools mit InputSchema definiert und registriert
- ✅ Resources als schreibgeschützte Datenquellen bereitgestellt
- ✅ Prompts für standardisierte Arbeitsabläufe erstellt
- ✅ Server über stdio zum Laufen gebracht
- ✅ Verbindung mit Claude Desktop konfiguriert
- ✅ Testing mit dem MCP Inspector durchgeführt
- ✅ Best Practices für den Produktiveinsatz angewendet
Damit sind Sie startklar für den eigenen MCP-Server. Weiterführende Ressourcen:
- MCP Server: Was ist das Model Context Protocol? – Grundlagen und Architektur
- MCP-Client: So verbindet sich ein LLM mit MCP-Servern – Alle unterstützten Plattformen
- MCP Security: Authentifizierung und Zugriffskontrolle – Sicherheit im Unternehmenseinsatz
- MCP Registry: Öffentliche MCP-Server finden – Bestehende Server wiederverwenden
- MCP-Gateway: Was ist das und wozu braucht man es? – Zentrale Verwaltung mehrerer Server
Wenn Sie Fragen zur Implementierung haben oder Unterstützung beim Aufbau Ihrer MCP-Infrastruktur brauchen, sprechen Sie uns an. Wir helfen mittelständischen Unternehmen beim sicheren, produktiven Einsatz des Model Context Protocols.



