Startseite / Artikel / MCP für KI-Agenten: Standardisierung der Tool-Integration in LangGraph

MCP für KI-Agenten: Standardisierung der Tool-Integration in LangGraph

Dieser Artikel erläutert, was MCP in agierenden KI-Systemen tatsächlich standardisiert, indem er ad hoc integrierte Tools mit MCP-basierten Lösungen innerhalb eines LangGraph-Orchestriers vergleicht.

4323 Wörter

Einführung & Zusammenfassung

Zum Ende von Teil 2 hatte das System einen wirklich koordinierten Zustand erreicht: eine Gruppe spezialisierter Agenten, von denen jeder für einen eng definierten Aufgabenbereich verantwortlich war und unter der Leitung eines Orchestriersystems, das entschied, was als Nächstes ausgeführt werden sollte, auf einem gemeinsamen Zustand arbeitete. In jedem Beispiel wurde jedoch davon ausgegangen, dass jeder Agent bereits Zugriff auf alles hatte, was er innerhalb dieses gemeinsamen Zustands benötigte, und dieses jederzeit lesen konnte, wenn es erforderlich war.

Diese Annahme gilt in der Praxis selten. Ein Agent benötigt in der Regel etwas, das außerhalb des Graphen liegt: eine Zeile aus einer Datenbank, eine Antwort von einer externen API, einen Abschnitt aus einer Wissensdatenbank oder eine andere Ressource, die sich jenseits der eigenen Grenzen des Systems befindet. Immer dann, wenn ein Agent auf diese Weise nach außen zugreifen muss, benötigt er seinen eigenen Weg dafür. Wenn jeder dieser Wege manuell erstellt wird, wiederholt man letztendlich die gleiche Art von Integrationsarbeit – nur in geringfügig abgewandelter Form – für jeden Agenten und jede externe Ressource.

Genau diese Wiederholung ist das Problem, dem dieser Artikel gewidmet ist, und er erklärt auch, warum MCP im letzten Jahr zu einem so häufig diskutierten Thema geworden ist. Bevor man entscheidet, ob es sich lohnt, MCP zu übernehmen, hilft es, genau herauszufinden, welches Problem es löst, und zu betrachten, was es bedeutete, ein Tool mit einem Agenten zu verbinden, bevor MCP überhaupt existierte.

Das Problem, das MCP lösen soll

Vor MCP bedeutete es, einem Agenten Zugang zu externen Ressourcen zu gewähren, dass man eine maßgeschneiderte Integration entwickeln musste, die genau auf diese Ressource zugeschnitten war und nach den damals sinnvollen Kriterien gestaltet wurde. Eine Ressource war möglicherweise über eine REST API erreichbar, eine andere über einen Datenbankklienten, wieder eine andere über ein SDK mit eigenen Regeln für Authentifizierung und Fehlerbehandlung. Jeder dieser Unterschiede musste direkt in den eigenen Code des Agents integriert werden.

Das ist ein guter Kompromiss, wenn ein einzelner Agent mit einem einzigen Tool kommuniziert. Sobald das System in irgendeiner Richtung wächst, wird das nicht mehr gut. Fügen Sie einen zweiten Agenten hinzu, der dieselben Ressourcen benötigt, dann müssen Sie entweder die Integration kopieren oder jemand zieht sie schließlich in ein gemeinsames Modul aus – meist erst, nachdem die Duplikation bereits eingetreten ist. Fügen Sie stattdessen ein zweites Tool hinzu, dann muss der Code des Agents gleichzeitig zwei völlig unterschiedliche Integrationsschemata beinhalten.

Diese Integrationen ähneln sich selten, weil nichts dazu verlangt. Ein Wrapper könnte fehlgeschlagene Aufrufe automatisch erneut versuchen; ein anderer könnte überhaupt nicht nachversuchen. Einige könnten Fehler als ausgeworfene Ausnahmen sichtbar machen; andere könnten sie in einem Statusfeld verbergen, das der Aufrufer sich merken muss, um es zu überprüfen. Es gibt keine gemeinsame Sprache dafür, was es eigentlich bedeutet, „einen Agenten mit einem Tool zu verbinden“ – jede Integration beantwortet diese Frage schließlich auf ihre eigene Weise.

Genau diese Lücke will MCP schließen. Es werden keine neuen Funktionen eingeführt, die Agenten zuvor gefehlt haben; vielmehr wird der Mechanismus zur Nutzung bereits vorhandener Funktionen standardisiert, sodass das Anbinden eines neuen Agenten an ein bestehendes Tool oder eines neuen Tools an einen bestehenden Agenten nicht mehr bedeutet, eine weitere maßgeschneiderte Integration von Grund auf zu entwickeln. Ob es in der Praxis diesem Versprechen gerecht wird, lässt sich viel leichter beurteilen, sobald man gesehen hat, wie der ad hoc Ansatz tatsächlich im Code aussieht – und genau dort setzt die weitere Diskussion an.

Vor MCP: Improvisierte Verbindungen von Tools

Betrachten wir eine ziemlich gewöhnliche Integration: einen Agenten, der ein externes System abfragen muss und durch irgendwelchen „Klebstoff-Code“ miteinander verbunden wird, der die Verbindung funktionsfähig macht.

import requests
class LookupToolClient:
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.api_key = api_key
    def lookup(self, query: str) -> dict:
        response = requests.get(
            f"{self.base_url}/search",
            params={"q": query},
            headers={"Authorization": f"Bearer {self.api_key}"},
        )
        if response.status_code != 200:
            return {"error": f"lookup failed: {response.status_code}"}
        return response.json()
def agent_node(state: GraphState) -> dict:
    client = LookupToolClient(base_url="https://internal-tool.example.com", api_key="...")
    result = client.lookup(state["extracted_fields"]["query"])
    return {"tool_result": result}

Allein betrachtet gibt es damit kein Problem. Es handelt sich um einen kompakten HTTP-Client, etwas Fehlerbehandlung sowie eine Funktion, die ihn von innerhalb eines Nodes aufruft. Die Probleme beginnen erst, wenn ein zweites Tool hinzukommt – diesmal nicht wieder eine REST-API, sondern ein Datenbank-Client mit einer völlig anderen Struktur:

import psycopg2
class RecordsClient:
    def __init__(self, connection_string: str):
        self.conn = psycopg2.connect(connection_string)
    def fetch_record(self, record_id: str) -> dict:
        with self.conn.cursor() as cur:
            cur.execute("SELECT * FROM records WHERE id = %s", (record_id,))
            row = cur.fetchone()
            if row is None:
                raise ValueError(f"no record found for {record_id}")
            return dict(zip([desc[0] for desc in cur.description], row))

Diese beiden Clients teilen weder eine gemeinsame Schnittstelle noch eine Namenskonvention, geschweige denn eine einheitliche Methode zur Signalisierung von Fehlern: Der eine gibt ein Fehlerdictionary zurück, der andere wirft direkt eine Ausnahme. Jeder Agent, der beide nutzen muss, muss sich diese Besonderheiten einzeln aneignen und jede für sich berücksichtigen. Wenn man das nun mit allen weiteren Tools verrechnet, die das System letztendlich benötigt – seinen eigenen Client, sein eigenes Authentifizierungsverfahren, seine eigenen Fehlermodi – dann verwandelt sich das, was ursprünglich aus ein paar kleinen Integrationen bestand, in eine echte Wartungslast, bei der es keine gemeinsame Struktur gibt, die alles miteinander verbindet.

Das ist die Grundlage, an der man sich hier festhalten sollte: Es handelt sich nicht um eine schlecht geschriebene Integration, sondern nur um eine typische, wie sie bei den meisten Tool-Integrationen entsteht, wenn nichts eine gemeinsame Struktur vorschreibt.

Was MCP tatsächlich standardisiert

Wenn man sich dieses ad hoc Beispiels weiterhin vor Augen hält, fällt es viel leichter, genau zu beschreiben, was MCP leistet – ohne auf die weiter gefassten, vageren Behauptungen zurückzugreifen, die oft über es aufgestellt werden.

In seinem Kern definiert MCP ein gemeinsames Protokoll zur Bereitstellung von Tools für einen Agenten, unabhängig davon, was das Tool intern tut oder in welcher Sprache bzw. mit welchem Framework es entwickelt wurde. Anstatt dass jedes Tool seinen eigenen, maßgeschneiderten Client mit eigenen Konventionen mitliefert, wird es über einen MCP-Server bereitgestellt, der seine Fähigkeiten in einem vorhersehbaren, standardisierten Format anzeigt: einen Namen, eine Beschreibung, ein Eingabeschema und ein Ausgabeschema. Jeder Agent, der das Protokoll versteht, kann dieses Tool entdecken und auf dieselbe Weise darauf zugreifen wie auf jedes andere Tool – egal ob es sich dabei um einen REST-Endpunkt, eine Datenbank oder etwas völlig anderes handelt.

Diese Standardisierung umfasst genau drei Bereiche, und es ist sinnvoll, genau zu benennen, um welche drei es sich handelt, da man dazu neigt anzunehmen, der Anwendungsbereich von MCP sei breiter, als er tatsächlich ist.

  • Entdeckung: Ein Agent kann einen MCP-Server nach der Liste der Tools abfragen, die dieser zur Verfügung stellt, und erhält eine strukturierte Antwort, anstatt davon auszugehen, dass diese Informationen irgendwo fest codiert oder getrennt von der eigentlichen Implementierung dokumentiert sind.
  • Aufruf: Jeder Tool-Aufruf folgt demselben Muster, unabhängig davon, um welches Tool es sich handelt – eine Anfrage mit definiertem Aufbau und eine Antwort mit definiertem Aufbau – anstatt dass jeder Client seine eigene Methodensignatur sowie seinen eigenen Rückgabetyp definiert.
  • Fehlerbehandlung: Fehler werden in einem einheitlichen Format gemeldet, sodass ein Agent nicht nachverfolgen muss, ob ein bestimmtes Tool eine Ausnahme wirft, ein Fehlerfeld zurückgibt oder auf andere Weise versagt. Die Struktur bleibt bei jeder Meldung konstant.
  • MCP beseitigt weder die Notwendigkeit, die zugrundeliegende Logik eines Tools selbst zu schreiben, noch garantiert es, dass ein Tool korrekt funktioniert, nur weil es im Rahmen des Protokolls verpackt ist. Es standardisiert lediglich den Vertrag zwischen einem Agenten und einem Tool, nicht jedoch die Qualität oder Zuverlässigkeit dessen, was hinter diesem Vertrag steht – auf diesen Punkt weist der Artikel später direkt hin, nachdem die vorangegangenen Vergleiche den Unterschied klar gemacht haben.

    Anatomie eines MCP-Servers

    Angesichts der gerade beschriebenen Standardisierung lohnt es sich, genauer zu betrachten, was sie tatsächlich umsetzt. Strukturell ist ein MCP-Server nichts anderes als eine definierte Sammlung von Tools, wobei jedes Tool sein eigenes Schema besitzt und in einer Protokollschicht verpackt ist, die es einem Agenten ermöglicht, diese Tools auf konsistente Weise zu entdecken und aufzurufen.

    Zum Mindesten erfordert die Definition eines Tools auf einem MCP-Server die Angabe von drei Elementen: einen Namen, den der Agent zur Bezeichnung des Tools verwendet, ein Schema, das die erwarteten Eingaben beschreibt, sowie die Funktion, die ausgeführt wird, wenn das Tool tatsächlich aufgerufen wird.

    from mcp.server import Server
    from mcp.types import Tool
    server = Server("lookup-tools")
    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {
                        "query": {"type": "string"}
                    },
                    "required": ["query"],
                },
            )
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        raise ValueError(f"unknown tool: {name}")
    

    Zwei Aspekte heben sich hier ab, wenn man dies mit den zuvor besprochenen ad hoc-Klienten vergleicht. Erstens wird das Eingabeschema explizit zu Beginn deklariert, anstatt aus den Parametern abgeleitet zu werden, die eine Funktion zufällig entgegennimmt – dadurch können sowohl ein Agent als auch ein menschlicher Prüfer genau erkennen, was ein Tool erfordert, ohne in seine Implementierung eindringen zu müssen. Zweitens muss der Server nur zwei Eingangspunkte bereitstellen, list_tools und call_tool, unabhängig davon, wie viele Tools er enthält oder wie unterschiedlich sie im Hintergrund funktionieren. Ob das lookup-Tool mit einer REST-API kommuniziert, eine Datenbank abfragt oder etwas völlig anderes tut, bleibt vollständig hinter dieser beiden-Funktion-Oberfläche verborgen.

    Auf der Seite des Agents sieht die Verbindung zu diesem Server unabhängig davon identisch aus, welche Tools er bereitstellt:

    from mcp.client import ClientSession
    async def call_lookup_tool(query: str) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool("lookup", {"query": query})
            return result
    

    Vergleichen Sie dies mit den beiden ad hoc Clients von früher – einem, der auf requests basiert, und dem anderen auf psycopg2 –, wobei jeder seine eigenen Strukturen und Konventionen hat. Hier bleibt der Code des Agents unverändert, egal wie eine Tool intern funktioniert – er ruft session.call_tool mit einem Namen und einer Reihe von Argumenten auf und erhält jedes Mal in derselben Struktur ein Ergebnis zurück. Diese Einheitlichkeit ist der eigentliche Vorteil der Serverarchitektur, nicht die Logik des Tools selbst, die ohnehin von jemandem geschrieben werden muss.

    Die gleiche Integration über MCP umgestalten

    Der beste Weg, den praktischen Unterschied zu erkennen, besteht darin, genau dieselbe Integration, die bereits früher erstellt wurde – das Suchwerkzeug und den Datensatzklienten – zu nehmen und sie stattdessen mit MCP neu aufzubauen. Die Funktionalität bleibt unverändert, die zugrunde liegenden Systeme bleiben unverändert, nur die Schnittstelle ändert sich.

    Das Suchwerkzeug, das ursprünglich als eigenständiger Klient basierend auf requests entstanden ist, wird nun zu einer auf einem MCP-Server registrierten Tool-Deklaration:

    @server.list_tools()
    async def list_tools() -> list[Tool]:
        return [
            Tool(
                name="lookup",
                description="Search for a record by query string",
                inputSchema={
                    "type": "object",
                    "properties": {"query": {"type": "string"}},
                    "required": ["query"],
                },
            ),
            Tool(
                name="fetch_record",
                description="Fetch a record by ID",
                inputSchema={
                    "type": "object",
                    "properties": {"record_id": {"type": "string"}},
                    "required": ["record_id"],
                },
            ),
        ]
    @server.call_tool()
    async def call_tool(name: str, arguments: dict) -> dict:
        if name == "lookup":
            return perform_lookup(arguments["query"])
        elif name == "fetch_record":
            return fetch_record_from_db(arguments["record_id"])
        raise ValueError(f"unknown tool: {name}")
    

    Keine der internen Logiken wurde verändert – perform_lookup ruft weiterhin dieselbe externe API auf, und fetch_record_from_db führt weiterhin dieselbe Datenbankabfrage aus. Was sich ändert, ist, dass diese beiden Tools, obwohl sie auf völlig unabhängigen Systemen basieren, nun nebeneinander deklariert werden, ein identisches Schema teilen und über dieselben beiden Eingangspunkte zugänglich sind.

    Die größte sichtbare Veränderung findet im Knoten statt, der für den Aufruf dieser Funktionen zuständig ist, da er nicht länger Kenntnis von requests oder psycopg2 benötigt:

    async def agent_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["query"]}
            )
        return {"tool_result": result}
    

    Vergleichen Sie das mit der früheren Version, die eine spezifische HTTP-Bibliothek importierte, ein bestimmtes Fehlerformat parsen musste und lediglich zum Aufruf von fetch_record anstelle von lookup einen völlig separaten Codepfad erforderte. In dieser Version besteht der Aufruf eines zweiten Tools darin, einen String sowie ein Dictionary mit Argumenten auszutauschen, anstatt einen zweiten Client mit seinen eigenen Besonderheiten zu entwickeln.

    Es hat sich nichts daran geändert, was diese Tools tatsächlich leisten. Was sich geändert hat, ist, dass der Agent, der sie aufruft, ihre Implementierung überhaupt nicht mehr verstehen muss – nur ihren Namen sowie das deklarierte Schema. Das ist die zuvor in der Serie besprochene Standardisierung, jetzt etwas, auf das man im eigentlichen Code verweisen kann, anstatt es blind zu akzeptieren.

    Vergleich: Ad Hoc vs. MCP

    Sobald beide Ansätze vollständig implementiert sind, kann man aufhören, über theoretische Vorteile nachzudenken, und stattdessen direkt betrachten, was sich tatsächlich zwischen der Ad-Hoc-Version und der MCP-Version unterscheidet.

    • Anstrengung pro neues Tool: Bei dem ad hoc Ansatz brachte jedes zusätzliche Tool seinen eigenen Client, seine eigene Authentifizierungslogik, ihre eigene Fehlerstruktur sowie ihr eigenes Satz an Konventionen mit sich, die man erst lernen musste, bevor man es sicher verwenden konnte. Unter MCP bedeutet das Hinzufügen eines Tools das Hinzufügen einer weiteren Einträge in list_tools sowie eines weiteren Zweigs innerhalb von call_tool, wobei jeder nach dem identischen Muster des vorherigen Tools strukturiert ist.
    • Was der aufrufende Code verstehen muss: Der ad hoc Agent-Node musste eine bestimmte Client-Bibliothek importieren und das spezifische Fehlerformat dieser Bibliothek verarbeiten. Der MCP Agent-Node importiert nichts, was speziell für ein Tool bestimmt ist; er ruft einfach session.call_tool mit einem Namen und einem Dictionary an Argumenten auf, wobei dieser Aufruf unabhängig davon gleich funktioniert, ob das zugrunde liegende Tool ein REST-Endpunkt, eine Datenbank oder etwas anderes ist.
  • Wie wiederverwendbar ist die Einrichtung bei verschiedenen Agenten: Bei der ad hoc Gestaltung muss ein zweiter Agent, wenn er dieselbe Abfragemöglichkeit benötigt, entweder denselben Client direkt importieren und somit an diese spezifische Implementierung gebunden sein, oder jemand entwickelt schließlich einen gemeinsamen Wrapper, um Duplikate zu vermeiden. Mit einem MCP-Server kann ein zweiter Agent sich einfach mit diesem Server verbinden und das gleiche Werkzeugset erben, das auf dieselbe Weise auffindbar und aufrufbar ist – ohne zusätzliches Wissen, das über das hinausgeht, was bereits der erste Agent benötigte.
  • Wie viel Wartung erforderlich ist: Die Anpassung des Verhaltens der Abfragedatei, das Hinzufügen einer Wiederholungspolitik oder die Feinabstimmung eines Zeitlimits erforderte eine direkte Bearbeitung von LookupToolClient, wodurch jeder darauf angewiesene Agent die Änderung automatisch übernahm. MCP beseitigt diese Wartungslast nicht, konsolidiert sie jedoch: Das Verhalten jeder Datei liegt nun hinter denselben beiden Funktionen, list_tools und call_tool, anstatt über so viele unterschiedliche Client-Formen verteilt zu sein, wie es Dateien gibt.
  • Nichts davon vereinfacht die eigentliche Logik des Tools. perform_lookup und fetch_record_from_db müssen dennoch geschrieben und in jedem Fall weiterhin korrekt funktionieren. Was sich ändert, ist alles, was diese Logik umgibt: wie sie entdeckt wird, wie sie aufgerufen wird, wie Fehler sichtbar gemacht werden und wie viel von dieser Last der eigene Code des Agents tragen muss im Vergleich dazu, dass sie automatisch durch Befolgung eines gemeinsamen Protokolls abgewickelt wird.

    Integration eines MCP-Tools in das LangGraph-System (aus Teil 2)

    Bis jetzt konzentrierten sich alle Beispiele auf einen einzelnen Agenten, der ein einzelnes Tool isoliert aufruft. Es lohnt sich, diese Lücke zu schließen, indem ein auf MCP basierendes Tool wieder in das mehr-Agenten-Graphen eingefügt wird, das in Teil 2 erstellt wurde – schließlich ist dort der Schnittpunkt aller drei Artikel dieser Serie.

    Durch einen MCP-Server, der dieselbe Abfrage als Tool bereitstellt, ändern sich die Aufgaben des Nodes kaum. Er liest weiterhin aus dem Graphenzustand und schreibt seine Entscheidung wieder in diesen Zustand zurück; er erreicht lediglich das externe System, indem er session.call_tool aufruft, anstatt über einen speziellen Client zu gehen:

    async def rules_engine_node(state: GraphState) -> dict:
        async with ClientSession(server_params) as session:
            lookup_result = await session.call_tool(
                "lookup", {"query": state["extracted_fields"]["category"]}
            )
        decision = evaluate_rules(state["extracted_fields"], lookup_result)
        return {"decision": decision["decision"], "decision_reason": decision["reason"]}
    

    Die Position des Knotens innerhalb des Gesamtflusses wird durch nichts davon beeinflusst. Er bleibt weiterhin an derselben Stelle – flussabwärts von llm_interpretation und flussaufwärts vom bedingten Zweig, der bereits in Teil 2 vorhanden war. Er ist weiterhin durch den in der Diskussion zu den Richtlinien dieses Artikels definierten Berechtigungsrahmen eingeschränkt und wird weiterhin unter derselben Trace-ID erfasst, wie sie in dem Abschnitt zur Überwachbarkeit beschrieben wird. Die einzige wirkliche Veränderung besteht darin, wie der Knoten mit der Außenwelt kommuniziert: Anstatt sich auf einen speziell für diesen Knoten entwickelten Client zu verlassen, sendet er nun Aufrufe an ein Tool, das jeder andere Knoten – egal ob in diesem Graphen oder in einem zukünftigen – über denselben Pfad aufrufen kann.

    Genau hier treffen die drei Komponenten der Serie aufeinander. Ein Agent, der nur Entscheidungen trifft, die ihm ausdrücklich erlaubt sind, der innerhalb eines Graphen agiert, der ihn zusammen mit anderen Agenten koordiniert, und über dieselbe standardisierte Schnittstelle auf externe Systeme zugreift, die alle Agenten teilen. Keine der drei Komponenten – weder das Regelsystem noch der Graph noch MCP – benötigt ein detailliertes Verständnis der anderen beiden. Sie müssen lediglich denselben Ansatz befolgen, den diese Serie stets betont hat: enge Verantwortungsbereiche, ausdrückliche Vereinbarungen und keine Annahmen ohne Grund.

    Kosten & Latenz: Was Orchestrierung tatsächlich kostet

    Jede Schicht, die in dieser Reihe hinzugefügt wird, bringt einen gewissen Grad an Struktur mit sich – und diese Struktur kostet etwas. Anstatt die Kosten zu verbergen, hilft es, nachzuvollziehen, was tatsächlich mit der Antwortzeit geschieht, sobald eine Anfrage durch alle bisher beschriebenen Komponenten geht.

    Betrachten Sie eine einzelne Anfrage, die durch das in Teil 2 vorgestellte Graphen-System läuft, das nun um die oben beschriebene MCP-basierte Abfragemöglichkeit erweitert wurde. Der Ablauf verläuft in etwa wie folgt: Die Eingabeverarbeitung führt eine leichte Formatierung durch, ohne externe Aufrufe zu tätigen, wodurch höchstens einige Millisekunden vergehen. Der Schritt der Interpretation durch das große Sprachmodell ruft ein Modell auf, um aus der Rohanfrage strukturierte Felder zu extrahieren – dies ist in der Regel die aufwändigste Phase des gesamten Ablaufs und fügt je nach gewähltem Modell sowie Länge der Anfrage oft mehrere hundert Millisekunden hinzu. Der Aufruf des Regelwerks an das Abfrumentool über MCP beinhaltet eine Netzwerkübertragung hin und zurück zusätzlich zu der Zeit, die das Tool selbst zum Antworten benötigt; dies stellt eine erhebliche Kostenstelle dar, ist aber im Allgemeinen geringer als der Schritt mit dem großen Sprachmodell. Die Bewertung der Regeln selbst, da es sich dabei um deterministische Logik handelt, ist nahezu kostenlos. Die Routenverwaltung sowie der Schritt zur Abschlussbenachrichtigung fügen lediglich einen geringen Aufwand dazu hinzu.

    Wenn man diese Werte zusammenzählt, wird das Bild klar: Eine Anfrage, die nur einen Zugriff auf das Modell und einen Aufruf einer Tool benötigt, wird in ihrer Gesamtdauer fast ausschließlich von diesen beiden Operationen bestimmt, wobei die eigene Koordination des Graphen kaum eine Auswirkung hat. Die in Teil 2 eingeführten Knoten, Kanten und gemeinsamen Zustände bieten Struktur, ohne eigene signifikante Latenz zu verursachen – das Lesen und Schreiben eines gemeinsamen Zustandsobjekts ist kostengünstig. Was tatsächlich Zeit in Anspruch nimmt, ist der Zugriff auf ein Modell oder ein externes System.

    Das verändert die Sichtweise darauf, wie man an die Optimierung der Leistung herangehen sollte. Das Hinzufügen mehrer Knoten, weiterer Routing-Äste oder zusätzlicher Überprüfungen zu einem Graphen hat kaum Auswirkungen auf die Geschwindigkeit, da es sich dabei lediglich um Funktionsaufrufe und Abfragen in einem Dictionary handelt. Was ein System tatsächlich verlangsamt, sind alle Aufrufe von LLMs sowie alle Aufrufe externer Tools, die sich auf dem kritischen Pfad der Anfrage befinden. Ein System, das aus drei Agenten besteht, die nacheinander jeweils ein Modell aufrufen, wird deutlich langsamer laufen als eines, das das Modell nur einmal aufruft – unabhängig davon, wie sauber die zugrundeliegende Orchestrierung ist.

    Die praktische Erkenntnis ist einfach: Wenn die Latenz für einen Workflow von Bedeutung ist, sollte man nicht erst mit der Untersuchung der Form des Graphen beginnen. Zunächst sollten die Anfragen an das Modell sowie die Aufrufe externer Tools gezählt werden, durch die eine normale Anfrage gehen muss, bevor sie abgeschlossen ist. Dabei sollte geprüft werden, ob einige davon parallel statt nacheinander ausgeführt werden können oder ob sie für Anfragen, die sie nicht benötigen, ganz weggelassen werden können.

    Was MCP nicht löst

    Es ist genauso wichtig, offen über die Grenzen von MCP zu sprechen wie zuvor über seine Vorteile, da die meisten Beiträge zum Thema stark auf die Vorteile fokussiert sind und nur selten auf die Einschränkungen eingehen. Die drei dort besprochenen Bereiche – Entdeckung, Aufruf und Fehlerbehandlung – verdienen einen zweiten Blick aus der entgegengesetzten Perspektive.

    • Die Standardisierung der Auffindung und Aufrufmethode behebt kein schlecht entwickeltes Tool: MCP standardisiert lediglich die Art und Weise, wie ein Tool gefunden und aufgerufen wird, nicht das, was innerhalb dessen geschieht. Ein Tool mit langsamer, unzuverlässiger oder schlecht strukturierter Implementierung bleibt weiterhin langsam, unzuverlässig und schlecht strukturiert, selbst wenn es hinter einem MCP-Server steht. Das Protokoll verschiebt die Inkonsistenzen von dem aufrufenden Code in die eigene Implementierung des Tools, macht sie aber nicht verschwinden.
  • Eine einheitliche Fehlerform bedeutet nicht automatisch eine effektive Fehlerbehandlung: Fehlschläge treten weiterhin auf, Tools laufen weiterhin aus Zeitmangel ab, und externe Systeme gehen weiterhin down. MCP verleiht diesen Fehlern eine vorhersehbare, konsistente Struktur, wenn sie auftreten, doch der Aufrufcode bleibt weiterhin dafür verantwortlich zu entscheiden, was als Nächstes geschehen soll – ob wiederholt wird, auf ein Alternativverfahren umgestellt wird oder der Fehler nach oben weitergeleitet wird, genauso wie zuvor. Ein konsistentes Fehlerformat ist nicht gleichbedeutend damit, dass der Fehlschlag tatsächlich behoben wird.
  • Der Betrieb des Protokolls hat eigene Betriebskosten: Ein MCP-Server ist ein weiterer Prozess, den man starten, bereitstellen und in Ordnung halten muss. Für einen einzelnen Agenten, der ein einfaches Tool aufruft, stellt das eine echte Zusatzbelastung dar; der zuvor in dieser Serie beschriebene ad-hoc-Kundenansatz wäre schneller einzurichten und einfacher zu verstehen gewesen. Diese Zusatzbelastung wird durch die Jugend des Ökosystems noch verschärft: Werkzeuge, Debugging-Unterstützung sowie etablierte Konventionen hinken immer noch hinter etwas So Reifem wie einer einfachen REST-API zurück. In der Praxis bedeutet das, dass mehr Zeit mit dem Lesen des Quellcodes und der Spezifikation direkt aufgewendet werden muss, da weniger bewährte Muster zur Verfügung stehen, auf die man zurückgreifen kann.
  • Nichts davon spricht dagegen, MCP zu übernehmen. Es handelt sich dabei um eine Erinnerung daran, dass die Wahl eines Protokolls die ingenieurtechnische Arbeit, die dennoch dahinter stattfinden muss, nicht ersetzt. Die Veränderung, die MCP mit sich bringt, ist real – wie in den vorherigen Abschnitten gezeigt –, doch sie ist enger gefasst als der Begriff „Agenten, die mit Tools verbunden sind“ im Ganzen. Es lohnt sich daher, genau zu bestimmen, wo diese Grenze liegt, bevor man entscheidet, ob eine Übernahme sinnvoll ist – genau das wird im nächsten Abschnitt behandelt.

    Wann es sich lohnt, MCP zu übernehmen

    Angesichts der oben dargestellten Abwägungen gibt es hier keine pauschale Antwort – weder ein klares „Übernehme es immer“ noch „Mach dir keine Mühe“. Entscheidend ist vielmehr, vor der Einführung in einem bestimmten System eine Reihe konkreter Bedingungen zu prüfen.

    • Mehr als ein Agent wird dieselben Tools benötigen. Fast alle zuvor beschriebenen Vorteile ergeben sich aus der Wiederverwendung: Ein zweiter Agent kann an einen bereits vorhandenen Server angeschlossen werden, anstatt einen eigenen Client zu erstellen oder diesen später hinzuzufügen. Wenn es nur einen Agenten und ein Tool gibt, existiert dieser Vorteil noch nicht – es gibt nichts zu teilen – und der zuvor besprochene ad hoc-Ansatz bleibt in diesem spezifischen Fall die einfachere Wahl.
    • Die Anzahl der Tools wird voraussichtlich zunehmen. Eine standardisierte Schnittstelle wird wertvoller, je mehr Tools dahinter stehen. Bei zwei oder drei Tools lohnt es sich möglicherweise nicht, einen dedizierten Server zu betreiben. Sobald es um zehn oder zwanzig Tools geht – von denen jedes sonst seinen eigenen benutzerdefinierten Client bräuchte – wird die Wartungslast des ad hoc-Ansatzes zu einer echten Belastung.
  • Das System muss über seine erste Veröffentlichung hinaus funktionieren. Ein Vorteil von MCP besteht darin, dass das Hinzufügen eines neuen Agents oder einer neuen Tool später nicht dazu zwingt, alles, was bereits vorhanden ist, umzustrukturieren. Dieser Vorteil sammelt sich im Laufe der Lebensdauer des Systems an und wird leicht übersehen, wenn man nur die Kosten der initialen Einrichtung betrachtet.
  • Andererseits eignet sich dies nicht für einen einzelnen Agent, der mit einem stabilen, gut verstandenen Tool kommuniziert, das sich nicht ändern wird. In diesem Szenario ist es schneller, einen ad hoc Client zu entwickeln – es muss weniger Wartungsteile betreut werden – und die von MCP angebotene Koordination hat keinen weiteren Nutzer, der sie tatsächlich nutzen könnte.

    Der praktische Test, der hier angewandt werden kann, ähnelt dem, der zuvor für den Regelmotor selbst verwendet wurde: Löst das Hinzufügen dieser Komplexität ein Problem, mit dem dieses spezielle System in seiner aktuellen Phase tatsächlich konfrontiert ist, oder wird es einfach nur eingeführt, weil es die modische Lösung für ein Problem ist, das das System noch nicht hat.

    Fazit: Abschluss der Serie

    In drei Artikeln entstand schrittweise ein System, wobei jede Schicht auf der vorherigen aufbaute. Der erste Artikel entwickelte einen einzelnen Agenten, der durch die strenge Trennung von Interpretation und Entscheidungsfindung als zuverlässig genug angesehen wurde, um ihm tatsächliche Entscheidungen anzuvertrauen. Der zweite Artikel fügte diesem Agenten Partner hinzu, indem mehrere spezialisierte Agenten innerhalb eines Graphen zusammengeführt wurden, der einen gemeinsamen Zustand ermöglichte und durch explizite Berechtigungen sowie vollständige Nachverfolgbarkeit geschützt wurde, sodass jede Anfrage vom Eingang bis zum Ausgang nachverfolgt werden konnte. Dieser Artikel schloss die verbleibende Lücke – nämlich wie diese Agenten über den Graphen hinausgreifen können –, indem er diesen Zugriff in Situationen, in denen er sich tatsächlich lohnt, über MCP standardisierte, während gleichzeitig klar dargelegt wurde, wo das nicht der Fall ist.

    None of these three pieces is especially complex on its own. What actually makes the resulting system dependable is one habit, repeated at every layer without exception: responsibilities stay narrow, contracts stay explicit, and nothing is left to guesswork or allowed to happen quietly in the background. That habit is the real subject of this series, more than any particular tool—LangGraph and MCP just happened to be the frameworks used to put it into practice, but the underlying principles hold regardless of which tools you reach for.