uptime · 1404 days · 23 posts published · last deploy 1 day, 10 hours ago build:passing rss
~ / linux / mcp-server-in-python-entwickeln-vom-leeren-repo.md
Linux · 03. Juni 2026 · ~18min · 23fa73b

MCP-Server in Python entwickeln: Vom leeren Repo zum ersten Tool

FastMCP, stdio & HTTP/SSE, Tool-Definitionen, Claude Code Integration

>
devmaker.net
author · 23fa73b · 2026-06-03
x
MCP Server Python Hero v3.jpg 1024×1024
MCP Server Python Hero v3
Bild generiert mit KI
Eigene MCP-Server entwickeln klingt komplex – ist es aber nicht. Mit FastMCP und Python schreibst du Tools in Minuten, die Claude direkt aufrufen kann. Dieser Artikel zeigt den Weg vom leeren Repo bis zum produktiven Remote-Server: stdio für Claude Code lokal, HTTP/SSE mit Auth für Homelab und Production. Basis ist der devmaker-MCP-Server, der diese Website steuert – praxiserprobt, kein Beispiel-Setup.

Warum ein eigener MCP-Server?

Fertige MCP-Server gibt es für Browser-Steuerung, Git, Datenbanken oder sogar Home Assistant. Wer aber eine eigene API, ein internes CMS oder ein Homelab-Setup hat, kommt schnell an ihre Grenzen: Die Tools passen nicht, die Auth fehlt, oder die Datenstruktur ist zu generisch.

Ein eigener MCP-Server löst das. Du definierst genau die Tools, die du brauchst – und Claude (oder jeder andere MCP-fähige Client) kann sie direkt aufrufen.

Dieser Artikel zeigt den Weg von Null bis zu einem funktionierenden Server in zwei Stufen:

  • Stufe 1: Lokaler Server mit stdio-Transport, ideal für Claude Code
  • Stufe 2: Remote-Server mit HTTP/SSE, für Production-Setups und mehrere Clients

Basis für die Beispiele ist der devmaker-MCP-Server, der diese Website steuert.

Setup

Du brauchst Python 3.10+ und fastmcp. FastMCP ist der De-facto-Standard für Python-MCP-Server – es abstrahiert das Low-Level-Protokoll und lässt dich direkt mit Tool-Definitionen starten.

pip install fastmcp
# oder mit uv (empfohlen)
uv add fastmcp

Für async-Tools mit HTTP-Calls:

uv add fastmcp httpx

Stufe 1: stdio-Transport (Claude Code lokal)

stdio ist der einfachste Einstieg: Claude Code startet den Server-Prozess direkt und kommuniziert über stdin/stdout – kein Port, keine Auth. Ideal für lokale Entwicklung und persönliche Tools.

Hello World

# server.py
from fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def hello(name: str) -> str:
    """Say hello to someone."""
    return f"Hello, {name}!"

if __name__ == "__main__":
    mcp.run()

Das war's. @mcp.tool() registriert die Funktion als MCP-Tool. Der Docstring ist entscheidend – das ist exakt das, was Claude liest, um zu verstehen wofür das Tool da ist.

Integration in Claude Code

In ~/.claude.json oder in der Projekt-Config .claude/settings.json:

{
  "mcpServers": {
    "my-server": {
      "command": "python",
      "args": ["/absoluter/pfad/zu/server.py"]
    }
  }
}

Mit uv:

{
  "mcpServers": {
    "my-server": {
      "command": "uv",
      "args": ["run", "/absoluter/pfad/zu/server.py"]
    }
  }
}

Nach /mcp in Claude Code sollte der Server erscheinen. Wenn nicht: claude --mcp-debug zeigt stderr des Server-Prozesses.

Echtes Tool mit Parameter-Validierung

FastMCP nutzt Pydantic automatisch für alle Parameter. Type Hints sind Pflicht – ohne sie hat Claude keinen Kontext für die Eingaben.

from fastmcp import FastMCP
from pydantic import Field
from typing import Optional
import httpx

mcp = FastMCP("devmaker")

@mcp.tool()
async def get_articles(
    limit: int = Field(default=10, ge=1, le=100, description="Anzahl Artikel"),
    topic: Optional[str] = Field(default=None, description="Topic-Slug, z.B. 'linux'")
) -> str:
    """Artikel aus dem CMS abrufen. Gibt Titel, ID und URL zurück."""
    params = {"limit": limit}
    if topic:
        params["topic"] = topic
    async with httpx.AsyncClient() as client:
        resp = await client.get("http://localhost:8000/api/articles/", params=params)
        resp.raise_for_status()
        data = resp.json()
    lines = [f"- [{a['title']}]({a['url']}) (ID {a['id']})" for a in data["results"]]
    return "\n".join(lines) or "Keine Artikel gefunden."

Wichtige Punkte:

  • async def sobald du externe Calls machst
  • Field-Constraints (ge, le) werden als JSON Schema an Claude übergeben
  • Rückgabe als lesbarer String, nicht als JSON-Dump

Fehlerbehandlung

@mcp.tool()
async def publish_article(page_id: int) -> str:
    """Artikel veröffentlichen. Gibt URL zurück oder Fehlermeldung."""
    try:
        async with httpx.AsyncClient() as client:
            resp = await client.post(f"http://localhost:8000/api/pages/{page_id}/publish/")
            resp.raise_for_status()
            return f"Veröffentlicht: {resp.json()['url']}"
    except httpx.HTTPStatusError as e:
        return f"Fehler {e.response.status_code}: {e.response.text}"

Nicht mit Exceptions werfen – Claude bekommt dann eine kryptische MCP-Fehlermeldung. Lieber sprechende Strings zurückgeben.

Stufe 2: HTTP/SSE-Transport (Remote, Production)

stdio funktioniert nur lokal. Sobald der Server auf einem anderen Host läuft – oder mehrere Clients gleichzeitig zugreifen sollen – brauchst du HTTP mit Server-Sent Events (SSE).

Wann HTTP/SSE?

  • Der Server läuft auf einem Homelab-Host, nicht auf dem Laptop
  • Claude API (nicht Claude Code) soll den Server nutzen
  • Mehrere Nutzer oder Clients gleichzeitig
  • Auth und Rate-Limiting nötig

Server auf HTTP/SSE umstellen

Der Tool-Code bleibt identisch. Nur der Start ändert sich:

# server.py
from fastmcp import FastMCP
import uvicorn

mcp = FastMCP("devmaker", stateless_http=True)

# ... Tool-Definitionen wie bisher ...

if __name__ == "__main__":
    uvicorn.run(mcp.http_app(), host="0.0.0.0", port=8080)

stateless_http=True ist für die meisten Use Cases richtig – kein Session-State, jeder Request ist unabhängig.

Docker Compose

services:
  mcp-server:
    build: .
    ports:
      - "8080:8080"
    environment:
      - API_BASE_URL=${API_BASE_URL}
      - API_TOKEN=${API_TOKEN}
    restart: unless-stopped
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY server.py .
CMD ["python", "server.py"]

Claude Code mit Remote-Server

{
  "mcpServers": {
    "devmaker-remote": {
      "type": "http",
      "url": "http://mein-server.intern:8080/mcp"
    }
  }
}

Mit HTTPS und Auth-Token:

{
  "mcpServers": {
    "devmaker-remote": {
      "type": "http",
      "url": "https://mcp.devmaker.net/mcp",
      "headers": {
        "Authorization": "Bearer ${MCP_TOKEN}"
      }
    }
  }
}

Ressourcen (Resources)

Neben Tools gibt es Resources – schreibgeschützte Datenquellen, die Claude bei Bedarf lesen kann, ohne dass du explizit ein Tool aufrufst.

import json

@mcp.resource("config://server-info")
def server_info() -> str:
    """Gibt Server-Konfiguration und verfügbare Topics zurück."""
    return json.dumps({
        "version": "1.0",
        "topics": ["linux", "homelab", "home-automation", "elektronik", "3d-druck"],
        "max_articles_per_request": 50
    }, ensure_ascii=False)

Auth: Bearer Token

from fastmcp import FastMCP
from fastmcp.server.auth import BearerAuthProvider
import os

mcp = FastMCP("devmaker", stateless_http=True)

# FastMCP >= 2.x: auth via server-Parameter
mcp_with_auth = FastMCP(
    "devmaker",
    stateless_http=True,
    auth=BearerAuthProvider(token=os.environ["MCP_TOKEN"])
)

Kein Token, kein Zugriff. Reicht für Homelab-Setups. Die genaue API variiert je nach FastMCP-Version – fastmcp --version und Changelog prüfen.

Lessons Learned

Docstrings sind dein API-Vertrag. Claude liest den Docstring und entscheidet, ob und wie es das Tool aufruft. Schlecht beschriebene Tools werden ignoriert oder falsch genutzt.

stdio zuerst, HTTP wenn nötig. Der Schritt von stdio zu HTTP klingt klein, ist aber operativ aufwändiger (Docker, Reverse Proxy, Auth). stdio reicht für 90% der persönlichen Setups.

Rückgabe als Text, nicht JSON. Claude kann JSON parsen, aber ein lesbarer String ist direkter.

Ein Server, zwei Transporte. Du kannst denselben FastMCP-Server als stdio und als HTTP betreiben. Nur der Einstiegspunkt ändert sich:

if __name__ == "__main__":
    import sys
    if "--http" in sys.argv:
        import uvicorn
        uvicorn.run(mcp.http_app(), host="0.0.0.0", port=8080)
    else:
        mcp.run()  # stdio

Tool-Granularität: Lieber mehrere kleine Tools als ein Mega-Tool mit 15 Parametern. Claude wählt gezielter, wenn Tools eng gefasst sind.

Fazit

Ein eigener MCP-Server in Python ist kein Hexenwerk. FastMCP nimmt den Protokoll-Overhead weg – du schreibst Python-Funktionen, dekorierst sie mit @mcp.tool(), und Claude kann sie sofort nutzen.

Der devmaker-MCP-Server, der diese Website steuert, ist nach diesem Prinzip aufgebaut: ~40 Tools, stdio lokal für Entwicklung, HTTP für den Produktiv-Server. Die Gesamtkomplexität ist überschaubar – der größte Aufwand liegt im Schreiben guter Docstrings und klarer Tool-Grenzen.

// responses (0)
> echo "your thoughts" >> mcp-server-in-python-entwickeln-vom-leeren-repo.responses

Schreibe einen Kommentar

Wird für die Bestätigung benötigt