Published on

MCP-Server intern betreiben: FastMCP mit Entra-ID-RBAC

Authors

Einen eigenen MCP-Server betreiben — mit Berechtigungen, die halten

TL;DR

Ein MCP-Server macht interne Systeme für KI-Agenten aufrufbar. Die Standardbeispiele laufen ohne Authentifizierung, weil sie für den Einzelplatz gedacht sind. Sobald der Server im Netz steht, brauchen Sie zwei Dinge: Tokenprüfung gegen Entra ID und eine Berechtigungsprüfung pro Werkzeug — nicht pro Server.


Wofür das Protokoll gut ist, und wofür nicht

Das Model Context Protocol standardisiert, wie ein Sprachmodell externe Werkzeuge aufruft. Statt für jede Anwendung eine eigene Werkzeugdefinition zu schreiben, betreiben Sie einen Server, der Werkzeuge anbietet, und jeder MCP-fähige Client kann sie nutzen.

Der praktische Nutzen im Mittelstand ist konkreter, als die Protokollbeschreibung vermuten lässt. Zwei Anwendungsfälle, die wir wiederholt sehen:

Ein Werkzeug bestand_abfragen, das gegen das ERP geht. Der Vertrieb fragt im Chat "Wie viele von Artikel 4711 sind in Werk 2 verfügbar?", der Agent ruft das Werkzeug auf und antwortet mit einer echten Zahl statt mit einer erfundenen.

Ein Werkzeug wartungshistorie, das die Instandhaltungsdatenbank abfragt. Der Techniker fragt nach den letzten Störungen an einer Maschine, bekommt die Liste und dazu eine Einordnung.

Wofür MCP nicht gut ist: Massendatenzugriff. Ein Werkzeug, das "alle Aufträge" zurückgibt, ist kein Werkzeug, sondern ein Datenleck mit JSON-Schnittstelle. Werkzeuge sollten eng geschnitten sein und Parameter erzwingen.

Der Server, der überall als Beispiel steht

So sieht der Einstieg in fast jeder Anleitung aus:

from fastmcp import FastMCP

mcp = FastMCP("ERP-Werkzeuge")

@mcp.tool
def bestand_abfragen(artikelnummer: str, werk: str) -> dict:
    """Gibt den verfügbaren Bestand eines Artikels in einem Werk zurück."""
    return erp.bestand(artikelnummer, werk)

if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)

Das funktioniert. Und es ist, sobald es auf 0.0.0.0 lauscht, jedem im Netz zugänglich, der die Adresse kennt. Keine Anmeldung, keine Protokollierung, wer was abgefragt hat.

Der Standardeinwand lautet: "Das läuft im internen Netz." Der hält seit ungefähr fünfzehn Jahren nicht mehr. Ein internes Netz mit Gastzugängen, Produktionsanlagen und Notebooks ist keine Vertrauensgrenze.

Schritt 1: Tokenprüfung gegen Entra ID

FastMCP bringt in neueren Versionen eine Authentifizierungsschicht mit, die JWT gegen einen OIDC-Anbieter prüft. Sie brauchen dafür eine App-Registrierung — im Zweifel dieselbe, die Sie ohnehin für SSO in Open WebUI und LiteLLM angelegt haben, ergänzt um eine eigene Anwendungs-ID-URI.

In der App-Registrierung unter Eine API verfügbar machen:

  • Anwendungs-ID-URI: api://ki-werkzeuge
  • Bereich hinzufügen: werkzeuge.aufrufen

Dann im Server:

from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier

TENANT = "<TENANT-ID>"

verifier = JWTVerifier(
    jwks_uri=f"https://login.microsoftonline.com/{TENANT}/discovery/v2.0/keys",
    issuer=f"https://login.microsoftonline.com/{TENANT}/v2.0",
    audience="api://ki-werkzeuge",
)

mcp = FastMCP("ERP-Werkzeuge", auth=verifier)

Ab hier weist der Server jede Anfrage ohne gültiges Token zurück. Das ist die halbe Miete — aber nur die halbe.

Schritt 2: Berechtigung pro Werkzeug, nicht pro Server

Der Fehler, den wir am häufigsten sehen: Der Server ist authentifiziert, und damit gilt die Sache als erledigt. Jeder angemeldete Mitarbeiter kann jedes Werkzeug aufrufen.

Das ist selten gewollt. Der Vertrieb soll Bestände sehen, aber keine Einkaufspreise. Die Instandhaltung soll Wartungshistorien abfragen, aber keine Personaldaten. Diese Trennung gehört pro Werkzeug abgebildet.

from fastmcp.server.dependencies import get_access_token
from fastmcp.exceptions import ToolError
from functools import wraps

def requires_group(*erlaubte: str):
    """Prüft die Gruppenmitgliedschaft aus dem Token vor dem Aufruf."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            token = get_access_token()
            gruppen = set(token.claims.get("groups", []))
            if not gruppen & set(erlaubte):
                # Bewusst unspezifisch — die Fehlermeldung darf nicht
                # verraten, welche Gruppe gereicht hätte
                raise ToolError("Keine Berechtigung für dieses Werkzeug.")
            return func(*args, **kwargs)
        return wrapper
    return decorator


@mcp.tool
@requires_group("GRP-Vertrieb", "GRP-Disposition")
def bestand_abfragen(artikelnummer: str, werk: str) -> dict:
    """Gibt den verfügbaren Bestand eines Artikels in einem Werk zurück."""
    return erp.bestand(artikelnummer, werk)


@mcp.tool
@requires_group("GRP-Einkauf")
def einkaufspreis(artikelnummer: str, lieferant: str) -> dict:
    """Gibt den aktuellen Einkaufspreis eines Artikels zurück."""
    return erp.preis(artikelnummer, lieferant)

Zwei Details, die in der Praxis zählen:

Die Gruppen kommen als Objekt-IDs im Token, nicht als Klarnamen — es sei denn, Sie konfigurieren in der Tokenkonfiguration ausdrücklich etwas anderes. Legen Sie eine Zuordnungstabelle an oder arbeiten Sie mit App-Rollen, die lesbare Werte tragen. App-Rollen sind für diesen Zweck der bessere Weg, weil sie unabhängig von der Verzeichnisstruktur sind.

Und: Wenn Ihr Tenant mehr als 200 Gruppen hat, liefert Entra keinen groups-Anspruch mehr, sondern einen Verweis auf den Graph. Der Server müsste dann für jede Anfrage einen zusätzlichen Aufruf machen. Das ist der Punkt, an dem App-Rollen alternativlos werden.

Schritt 3: protokollieren, was aufgerufen wurde

Ein MCP-Server ohne Aufrufprotokoll ist bei einem Sicherheitsvorfall wertlos. Sie brauchen für jeden Aufruf: Zeitstempel, Nutzer, Werkzeug, Parameter, Ergebnisgröße, Dauer.

import logging, time, json

log = logging.getLogger("mcp.audit")

@mcp.tool
@requires_group("GRP-Vertrieb", "GRP-Disposition")
def bestand_abfragen(artikelnummer: str, werk: str) -> dict:
    """Gibt den verfügbaren Bestand eines Artikels in einem Werk zurück."""
    start = time.monotonic()
    ergebnis = erp.bestand(artikelnummer, werk)
    log.info(json.dumps({
        "werkzeug": "bestand_abfragen",
        "nutzer": get_access_token().claims.get("preferred_username"),
        "parameter": {"artikelnummer": artikelnummer, "werk": werk},
        "dauer_ms": round((time.monotonic() - start) * 1000),
    }, ensure_ascii=False))
    return ergebnis

Schreiben Sie das strukturiert und nicht als Fließtext. Der Unterschied zeigt sich in dem Moment, in dem jemand fragt "Wer hat im letzten Quartal Einkaufspreise abgefragt?" — mit JSON-Zeilen ist das eine Abfrage, mit Freitext ein Nachmittag.

Unsere Empfehlung: Legen Sie das Protokoll in denselben Speicher wie Ihre übrigen Anwendungsprotokolle und definieren Sie eine Aufbewahrungsfrist. 90 Tage sind ein guter Ausgangswert. Und klären Sie mit dem Betriebsrat, was protokolliert wird, bevor Sie produktiv gehen — Aufrufprotokolle mit Nutzerkennung sind mitbestimmungsrelevant.

Was schiefgeht, wenn man es laufen lässt

Drei Muster aus Projekten, die wir übernommen haben:

Werkzeuge, die zu viel zurückgeben. Ein auftraege_suchen ohne Pflichtparameter liefert bei leerer Suchanfrage den gesamten Bestand. Das Modell schickt eine leere Anfrage öfter, als man denkt. Erzwingen Sie Parameter und begrenzen Sie das Ergebnis serverseitig.

Schreibende Werkzeuge ohne Bestätigung. Ein auftrag_anlegen ist verlockend und gefährlich. Wenn Sie schreibende Werkzeuge anbieten, lassen Sie den Agenten nur einen Vorschlag erzeugen, den ein Mensch freigibt. Ein Modell, das versehentlich 400 Bestellungen auslöst, ist kein theoretisches Szenario.

Beschreibungen, die das Modell in die Irre führen. Der Docstring ist die einzige Information, die das Modell über ein Werkzeug hat. "Gibt Bestand zurück" ist zu wenig. Schreiben Sie hinein, wann das Werkzeug nicht passt — das reduziert Fehlaufrufe messbar.

Sobald mehr als eine Handvoll Werkzeuge im Umlauf sind, brauchen Sie eine Übersicht darüber, welche existieren und wer sie verantwortet. Wie das aussieht, steht in der Governance für interne Agenten und MCP-Registry.

Häufig gestellte Fragen

Was ist ein MCP-Server im Unternehmenskontext?

Ein Dienst, der interne Funktionen — ERP-Abfragen, Datenbankzugriffe, Dokumentensuche — in einem standardisierten Format anbietet, das KI-Agenten aufrufen können. Der Vorteil gegenüber selbst gebauten Schnittstellen ist die Wiederverwendbarkeit: Ein einmal definiertes Werkzeug steht jedem MCP-fähigen Client zur Verfügung, ohne dass die Anbindung erneut geschrieben werden muss.

Braucht ein interner MCP-Server wirklich Authentifizierung?

Ja. Die Standardbeispiele laufen ohne, weil sie für den Einzelplatzbetrieb gedacht sind, bei dem Client und Server auf demselben Rechner laufen. Sobald der Server im Netz erreichbar ist, ist er für jeden erreichbar, der die Adresse kennt — inklusive Gastnetz und kompromittierter Endgeräte. Tokenprüfung gegen das vorhandene Verzeichnis ist der Mindeststandard.

Wie vergebe ich unterschiedliche Rechte für verschiedene Werkzeuge?

Über eine Prüfung im Werkzeug selbst, nicht über die Serverkonfiguration. Ein Dekorator, der vor dem eigentlichen Aufruf die Gruppen oder App-Rollen aus dem Token gegen eine Liste prüft, ist der praktikabelste Weg. Wichtig: Die Fehlermeldung bei fehlender Berechtigung darf nicht verraten, welche Gruppe gereicht hätte.

Kann ein KI-Agent über MCP Daten verändern?

Technisch ja, und genau deshalb sollten Sie schreibende Werkzeuge zurückhaltend anbieten. Unsere Empfehlung: Der Agent erzeugt einen Vorschlag, ein Mensch gibt ihn frei. Wo das nicht praktikabel ist, begrenzen Sie den Schaden — Mengenobergrenzen pro Aufruf, Tageskontingente, und eine Protokollierung, die eine Rücknahme ermöglicht.

Was muss protokolliert werden?

Zeitstempel, Nutzerkennung, aufgerufenes Werkzeug, übergebene Parameter und Dauer — strukturiert als JSON, nicht als Fließtext. Die Aufbewahrungsfrist sollte zu Ihren übrigen Anwendungsprotokollen passen, 90 Tage sind ein üblicher Ausgangswert. Da Aufrufprotokolle mit Nutzerkennung eine Leistungs- und Verhaltenskontrolle ermöglichen, klären Sie die Ausgestaltung vor dem Produktivbetrieb mit dem Betriebsrat.


Der nächste Schritt

Schreiben Sie auf, welche drei Fragen Ihre Mitarbeiter heute an ein Fachsystem stellen und dafür jemanden anrufen müssen. Das sind Ihre ersten drei Werkzeuge — eng geschnitten, lesend, mit Pflichtparametern. Alles Weitere folgt daraus. Bei der Absicherung schauen wir gern drauf.

📖 Verwandte Artikel

Weitere interessante Beiträge zu ähnlichen Themen