FlowKI Club
← Alle Artikel
tools18. April 20265 min Lesezeit

Dein erstes MCP-Server in 60 Minuten — Schritt für Schritt ohne Framework-Frust

MCP-Server klingen technisch. Sind sie nicht. In einer Stunde hast du einen funktionierenden Server der Claude Code um deine eigenen Tools erweitert. Hier der Rezept-Artikel — keine Theorie, nur Code.

Quelle: Model Context Protocol Spec

Was du am Ende hast

Nach 60 Minuten hast du einen Python-MCP-Server der eine echte Funktion hat: Er liest SQLite-Datenbanken und erlaubt Claude Code darüber Queries zu machen. Du startest claude in einem Projekt, und Claude hat plötzlich ein neues Tool namens query_sqlite zur Verfügung.

Das ist simpel, aber es ist der Baustein. Wenn du das hier verstehst, kannst du jeden internen API-Zugriff, jede Datenbank, jeden Custom-Workflow für dich als Tool bereitstellen.

Keine Theorie. Code und Schritte.

Voraussetzungen (5 Minuten)

Python 3.10 oder neuer. Ein Terminal. Claude Code installiert und funktionierend. Wenn du das noch nicht hast, schau in meinem Claude-Code-Einsteiger-Artikel — dort steht das Setup.

Projekt-Ordner aufsetzen:

mkdir mcp-sqlite-tutorial
cd mcp-sqlite-tutorial
python3 -m venv venv
source venv/bin/activate
pip install mcp

Das war der gesamte Setup. Das MCP-Python-SDK ist schlank, keine weiteren Dependencies.

Schritt 1 — Der Minimal-Server (15 Minuten)

Wir fangen mit dem einfachsten denkbaren MCP-Server an. Der tut noch nichts Sinnvolles aber zeigt die Struktur.

server.py anlegen:

import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

app = Server("sqlite-query-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="hello",
            description="Sagt Hallo, zum Testen",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "Dein Name"}
                },
                "required": ["name"],
            },
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "hello":
        return [TextContent(type="text", text=f"Hallo, {arguments['name']}!")]
    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Das sind die beiden MCP-Primitives: list_tools beschreibt was dein Server kann, call_tool führt es aus. Der Rest ist Boilerplate.

Test: python server.py. Das Terminal scheint zu hängen. Das ist richtig — der Server wartet auf JSON-RPC-Nachrichten auf stdin. Mit Ctrl+C wieder raus.

Schritt 2 — An Claude Code anbinden (10 Minuten)

Claude Code muss wissen dass dein Server existiert. Das geht über eine Config-Datei.

Datei mcp_config.json im Projekt-Root:

{
  "mcpServers": {
    "sqlite-query": {
      "command": "python",
      "args": ["/absoluter/pfad/zu/deinem/server.py"],
      "env": {}
    }
  }
}

Den absoluten Pfad zu deiner server.py einsetzen. Claude Code starten mit dieser Config:

claude --mcp-config mcp_config.json

Im Claude Code wird angezeigt dass der MCP-Server sqlite-query geladen wurde. Du kannst das testen: "Benutze das hello-Tool und sag Hallo zu Dennis."

Claude ruft dein Tool auf, bekommt die Antwort zurück, zeigt sie dir. Das ist der vollständige Round-Trip.

Schritt 3 — Echte Funktionalität — SQLite-Queries (25 Minuten)

Jetzt machen wir den Server nützlich. Wir erweitern ihn um ein Tool das SQLite-Datenbanken abfragt.

Den bestehenden server.py erweitern:

import asyncio
import sqlite3
import json
from pathlib import Path
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

app = Server("sqlite-query-server")

@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="list_databases",
            description="Listet alle SQLite-Dateien im aktuellen Ordner",
            inputSchema={"type": "object", "properties": {}, "required": []},
        ),
        Tool(
            name="describe_schema",
            description="Zeigt Tabellen und Spalten einer SQLite-DB",
            inputSchema={
                "type": "object",
                "properties": {"db_path": {"type": "string"}},
                "required": ["db_path"],
            },
        ),
        Tool(
            name="query_sqlite",
            description="Führt eine SELECT-Query auf einer SQLite-DB aus. NUR SELECT erlaubt.",
            inputSchema={
                "type": "object",
                "properties": {
                    "db_path": {"type": "string"},
                    "query": {"type": "string"},
                },
                "required": ["db_path", "query"],
            },
        ),
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "list_databases":
        dbs = [str(p) for p in Path(".").glob("**/*.db") if p.is_file()]
        return [TextContent(type="text", text=json.dumps(dbs, indent=2))]

    elif name == "describe_schema":
        db_path = arguments["db_path"]
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
        tables = [row[0] for row in cursor.fetchall()]
        schema = {}
        for table in tables:
            cursor.execute(f"PRAGMA table_info({table})")
            schema[table] = [
                {"name": col[1], "type": col[2], "nullable": not col[3]}
                for col in cursor.fetchall()
            ]
        conn.close()
        return [TextContent(type="text", text=json.dumps(schema, indent=2))]

    elif name == "query_sqlite":
        db_path = arguments["db_path"]
        query = arguments["query"].strip()
        if not query.lower().startswith("select"):
            return [TextContent(
                type="text",
                text="FEHLER: Nur SELECT-Queries erlaubt."
            )]
        conn = sqlite3.connect(db_path)
        cursor = conn.cursor()
        try:
            cursor.execute(query)
            rows = cursor.fetchmany(100)  # max 100 rows pro Response
            cols = [d[0] for d in cursor.description]
            result = [dict(zip(cols, row)) for row in rows]
            return [TextContent(type="text", text=json.dumps(result, indent=2))]
        except Exception as e:
            return [TextContent(type="text", text=f"FEHLER: {str(e)}")]
        finally:
            conn.close()

    raise ValueError(f"Unknown tool: {name}")

async def main():
    async with stdio_server() as (read, write):
        await app.run(read, write, app.create_initialization_options())

if __name__ == "__main__":
    asyncio.run(main())

Drei Tools: Datenbanken im Ordner finden, Schema einer DB anzeigen, Query ausführen. Die Query ist beschränkt auf SELECT — kein DELETE, kein DROP. Das ist ein erster Sicherheits-Filter. In Produktion müsstest du mehr machen.

Test-DB erzeugen um es zu probieren:

python -c "
import sqlite3
conn = sqlite3.connect('test.db')
conn.execute('CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
conn.execute('INSERT INTO users (name, email) VALUES (?, ?)', ('Dennis', 'dennis@example.com'))
conn.execute('INSERT INTO users (name, email) VALUES (?, ?)', ('Anna', 'anna@example.com'))
conn.commit()
conn.close()
"

Claude Code mit der MCP-Config neu starten:

claude --mcp-config mcp_config.json

Und dann: "Welche SQLite-Datenbanken liegen hier im Ordner?"

Claude ruft list_databases auf, findet test.db, zeigt die Liste. Danach: "Zeig mir das Schema von test.db." Claude nutzt describe_schema. Dann: "Wie heißt der User mit ID 2?" Claude baut eine SELECT-Query, ruft query_sqlite auf, zeigt das Ergebnis.

Das ist die komplette Kette. Von deinem Python-Code bis zu Claude-generiertem SQL.

Schritt 4 — Stolperfallen die mir begegnet sind (5 Minuten)

Drei Sachen wo ich hängen geblieben bin, damit du Zeit sparst:

Erste: MCP-Logs gehen auf stderr. Wenn dein Server crasht, siehst du in Claude Code nur "Server nicht antwortend". Füge am Anfang deines Servers hinzu:

import logging
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    stream=sys.stderr,  # wichtig — nicht stdout
)

Claude Code zeigt stderr-Logs in seinem Debug-Output an.

Zweite: Absolute Pfade im Config-File. Relative Pfade funktionieren nicht. Klingt dumm, ist aber oft der Grund warum "es geht auf einmal nicht."

Dritte: Der Server muss ein Prozess sein. Wenn du ihn zur Entwicklung neu startest, musst du Claude Code neu starten damit die Verbindung frisch ist. Mache ich inzwischen automatisch.

Was du jetzt damit machen kannst

Der SQLite-Server ist ein Beispiel. Die echten Anwendungen ergeben sich aus deinem Kontext:

  • Postgres-Query-Tool für euer internes Backend. Claude kann direkt gegen Prod-Reports querien (read-only, mit sauberer Rechtekontrolle).
  • Jira-Ticket-Tool das Tickets liest und zusammenfasst. Spart dir tägliche Status-Updates.
  • Internal-API-Wrapper der Claude Zugriff auf deinen CRM-Endpoint gibt.
  • Repo-Analyse-Tool das git-Statistiken berechnet und sie Claude verfügbar macht.

Die Architektur ist immer gleich: list_tools beschreibt, call_tool führt aus. Was drin passiert ist dein Code.

Sicherheit — bevor du das in Produktion nutzt

Drei Regeln die ich einhalte wenn ich MCP-Server in produktiven Setups einsetze:

  • Read-only by default. Schreibende Operationen erfordern einen expliziten Boolean-Parameter im Input und einen Confirmation-Schritt.
  • Queries allowlistet. Statt "jede SQL-Query" nur ausgewählte Query-Types zulassen. "SELECT COUNT" ja, beliebiges JOIN nein.
  • Logging aller Aufrufe. Welches Tool wann mit welchen Argumenten aufgerufen wurde. Für Audit und Debug.

Der Tutorial-Server hier ist bewusst simpel. In einem echten Setup würden noch zwei-drei Layer Sicherheit dazu kommen.

Weiterlesen

Für das Konzept dahinter siehe MCP-Server auf Deutsch. Für die Tool-Auswahl siehe Claude Code vs Cursor vs Codex Benchmark.

Eigenen MCP-Server gebaut, Stolperfallen getroffen, Success-Stories? Im Discord Zone "Coding & Projekte" — dort sammeln wir Patterns und teilen Server die für andere nützlich sein könnten.

Wie wir diesen Artikel geprüft haben

Tests am
2026-04-16, Tutorial zweimal von Null durchgespielt
Hardware
MacBook Pro M3 Max, Python 3.12
Software
mcp Python SDK 1.0.4, Claude Code 2.4.1, Python 3.12
KI-Einsatz
Claude Code hat Debug geholfen. Der Tutorial-Code ist selbst geschrieben.
Weiterlesen

Aus dem Magazin