MCP im Detail: JSON-RPC-Nachrichten, FastMCP-Server und ein ReAct-Trace
Befolgen Sie eine Anfrage nach dem Model Context Protocol von Tools/List bis zur endgültigen Antwort, unter Verwendung von FastMCP-Servern, einem LangGraph ReAct-Agenten sowie einem Tracker, der jeden Schritt protokolliert.
Quellen: How MCP Works: A Deep Dive with Code
Das Model Context Protocol, kurz MCP, ist ein offener Standard von Anthropic, der KI-Modellen wie Claude ermöglicht, auf externe Tools, Datenquellen und Dienste zuzugreifen. Ein gutes Bild dafür ist ein universeller Stecker: Anstatt für jedes Tool eine eigene Integration zu schreiben, gibt es eine standardisierte Methode, damit ein Modell mit externen Systemen kommunizieren kann.
Die Bestandteile
Die Struktur setzt sich aus drei Komponenten zusammen. Der MCP-Host ist die Anwendung, die der Benutzer tatsächlich verwendet – sei es ein Chat-Fenster oder eine IDE – und enthält einen Client, der die Verbindung verwalten wird. Das MCP-Protokoll ist die standardisierte Kommunikationsschicht zwischen Host und Servern. Jeder MCP-Server ist mit einer bestimmten Art von Ressource verbunden, beispielsweise einer Datenbank, einer externen API oder einer Code-Ausführungsumgebung, und dient als Brücke zu dieser Ressource.
Was passiert bei der Ausführung
Auf höherer Ebene durchläuft eine Anfrage diese Schritte:
- Der Benutzer sendet eine Anfrage an den Host.
- Der Host fragt die MCP-Server, welche Tools sie anbieten.
- Die Server geben ihre Tool-Listen zurück.
- Der Host sendet die Anfrage zusammen mit der Tool-Liste an das LLM.
- Das LLM wählt ein Tool aus und teilt es dem Host mit.
- Der Host ruft den entsprechenden MCP-Server auf, um das Tool auszuführen.
- Der Server führt das Tool aus und gibt das Ergebnis zurück.
- Der Host leitet dieses Ergebnis an das LLM weiter.
- Das LLM formuliert die endgültige Antwort, die der Host dann übermittelt.
Ein Server stellt drei Arten von Ressourcen zur Verfügung. Tools sind Funktionen, die das Modell aufrufen kann, wie zum Beispiel search_web, query_database oder send_email. Ressourcen sind lesbare Daten, die der Host als Kontext heranziehen kann – wie Dateien, Datenbanken Einträge, Kalendereinträge und Dokumente –, die das Modell zwar lesen, aber nicht ausführen kann. Prompts sind vorausgeschriebene Vorlagen, die der Server anbietet. Ein GitHub-Server könnte beispielsweise eine „Diesen PR zusammenfassen“-Vorlage bereitstellen, die der Host dem Benutzer anzeigen kann.
Im Grunde ist MCP lediglich eine standardisierte Methode dafür, dass ein Host mit einem Server kommuniziert. Er legt das Nachrichtenformat sowie die Regeln der Konversation fest – und nichts weiter. Genauer gesagt definiert er vier Aspekte.
- Form der Nachrichten. Alles basiert auf JSON-RPC 2.0. Eine Anfrage enthält einen
id, einemethodsowieparams, und eine Antwort wiederholt denidzusammen mit entweder einemresultoder einemerror, sodass beide Seiten auf dieselbe Weise kommunizieren. - Verfügbare Methoden. Das Protokoll legt ein festes Vokabular an Aufrufen fest, die ein Host durchführen darf – beispielsweise
tools/listzur Erkundung,tools/callzum Ausführen eines Tools,resources/readzum Abrufen von Daten sowieprompts/getzum Herunterladen eines Templates. Server müssen diese Methoden implementieren. - Der Handshake. Bei der ersten Verbindung tauschen die beiden Seiten
initialize-Nachrichten aus, in denen sie ihre Protokollversion sowie ihre Fähigkeiten angeben, damit jede Seite vor weiteren Aktionen weiß, was die andere unterstützt.
Ein einziger Tool-Aufruf zerfällt in vier Nachrichten. Zuerst fragt der Host nach, welche Tools vorhanden sind:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Der Server antwortet mit Beschreibungen seiner Tools:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "search_web",
"description": "Search the web for a given query",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}
}
]
}
}
Dann ruft der Host einen von ihnen an:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_web",
"arguments": {
"query": "latest news on MCP protocol"
}
}
}
Und der Server gibt das Ergebnis zurück:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Anthropic released MCP in Nov 2024 as an open standard..."
}
]
}
}
Das Lernen durch Gestalten funktioniert am besten, daher wird im Folgenden ein kleiner MCP-Server vorgestellt, der von einem Agenten genutzt wird.
Was FastMCP leistet
FastMCP ist ein Python-Framework zum Erstellen von MCP-Servern mit minimalem Boilerplate. Ohne es müsste ein Entwickler den JSON-RPC-Message-Loop implementieren, jede Anfrage und Antwort serialisieren und deserialisieren, für jede bereitgestellte Funktion manuell JSON-Schemata schreiben sowie Protokollnachrichten wie initialize, tools/list, tools/call, resources/list und prompts/list verarbeiten. Mit FastMCP reicht eine gewöhnliche Python-Funktion zusammen mit einem Dekorator aus, und das Framework kümmert sich um den Rest:
from fastmcp import FastMCP
mcp = FastMCP("My Server") # creates the server
@mcp.tool() # registers the function as an MCP tool
def add(a: float, b: float) -> float:
"""Add two numbers.""" # docstring → tool description sent to the LLM
return a + b # type hints → JSON Schema sent to the LLM
mcp.run() # starts the stdio message loop
Durch die Typangaben erstellt FastMCP selbst ein JSON-Schema, und die Dokumentation dient gleichzeitig als Beschreibung in Alltagssprache, die das Modell liest, um zu entscheiden, ob ein Tool aufgerufen werden sollte. Die Aufrufung von mcp.run() startet einen Loop, der auf stdin und stdout wartet und dabei das MCP-Protokoll verwendet; an diesen Loop hängt sich genau MultiServerMCPClient an.
Drei Primitive in den Beispielservern
Jeder Server im Projekt ist eine eigenständige FastMCP-Anwendung, die die drei grundlegenden Primitivtypen bereitstellt.
Zuerst kommen die Tools: die Funktionen, die das Modell aufrufen darf. Es genügt, eine Python-Funktion mit @mcp.tool() zu versehen, und FastMCP wandelt die Anmerkungen sowie die Dokumentation in das JSON-Schema um, das während von tools/list an den Client gesendet wird. Das Modell studiert dieses Schema, um herauszufinden, wann der Tool angewendet werden soll und welche Argumente bereitgestellt werden müssen.
# servers/math_server.py
@mcp.tool()
def divide(a: float, b: float) -> float:
"""Divide a by b. Raises an error if b is zero."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Ressourcen sind nur zum Lesen zugängliche Daten, die sich an einer URI befinden. Das Modell ruft sie nicht automatisch auf, sodass sie explizit angefordert werden müssen. In diesem Projekt dienen sie lediglich dazu, die Idee zu veranschaulichen, während praktische Anwendungen Konfigurationsdateien, Datenbank-Snapshots oder Referenzdokumentation beinhalten können.
# servers/math_server.py
@mcp.resource("math://constants")
def get_math_constants() -> str:
"""Common mathematical constants."""
return f"π = {math.pi}\n e = {math.e}\n ..."
@mcp.resource("math://formulas/{category}")
def get_formulas(category: str) -> str:
"""Retrieve mathematical formulas by category (geometry | algebra | statistics)."""
catalog = {
"geometry": (
"Geometry Formulas:\n"
" Circle area: A = π × r²\n"
" Circle circumference: C = 2π × r\n"
" Rectangle area: A = length × width\n"
" Triangle area: A = (base × height) / 2\n"
" Sphere volume: V = (4/3) × π × r³\n"
),
"algebra": (
"Algebra Formulas:\n"
" Quadratic formula: x = (−b ± √(b²−4ac)) / 2a\n"
" Difference of squares: a²−b² = (a+b)(a−b)\n"
" Perfect square: (a+b)² = a²+2ab+b²\n"
" Sum of arithmetic seq: S = n(a₁+aₙ)/2\n"
),
"statistics": (
"Statistics Formulas:\n"
" Mean: μ = Σx / n\n"
" Variance: σ² = Σ(x−μ)² / n\n"
" Std Dev: σ = √(Σ(x−μ)² / n)\n"
" Z-score: z = (x−μ) / σ\n"
),
}
return catalog.get(
category,
f"Unknown category '{category}'. Available: geometry, algebra, statistics",
)
Die URI math://formulas/{category} ist ein Vorlageformat. Der Teil {category} stellt einen Pfadparameter dar, wodurch math://formulas/geometry und math://formulas/algebra zwei unterschiedliche Ressourcen sind, die von derselben Funktion bereitgestellt werden.
Prompte sind wiederverwendbare Anweisungsvorlagen. Ein Prompt ist eine parametrisierte Systemnachricht, die vom Client angefordert werden kann, und dient dazu, eine Expertenpersönlichkeit oder Aufgabenanweisungen im Server zu verpacken, damit jeder verbundene Client sie einheitlich nutzen kann.
# servers/math_server.py
@mcp.prompt()
def math_tutor(difficulty: str = "intermediate") -> str:
return (
f"You are an expert math tutor for {difficulty}-level students. "
"Break every problem into numbered steps..."
)
Die Server stellen zusammen 19 Tools zur Verfügung, wie die folgenden Abschnitte zeigen.
Die Sichtweise des Clients
Hier wird genau beschrieben, was nach dem Ausführen von python main.py mit einer Abfrage geschieht. Der End-to-End-Anfragenpfad sieht wie folgt aus:
You type a query
│
▼
main.py ← entry point, parses args, kicks off async loop
│
▼
client/agent.py ← spawns 3 MCP servers, builds the agent, invokes it
│ │
│ ▼
│ utils/tracker.py ← fires on every LLM call, tool call, and result
│
▼
LangGraph ReAct loop ← think → call tool → observe → repeat
│
▼
servers/{math,text,data}_server.py ← each runs as an isolated subprocess
Schritt 1: Wo die Ausführung beginnt
main.py ist das Erste, was Python ausführt. Es lädt Umgebungsvariablen, analysiert Kommandozeilenargumente, gibt ein Willkommenspanel aus und übergibt die Kontrolle an einen asynchronen Ausführer.
async def _run(queries: list) -> None:
from client.agent import run_query # imported here (late) to keep startup fast
for i, q in enumerate(queries):
await run_query(q)
def main() -> None:
...
asyncio.run(_run(queries))
asyncio.run() erstellt einen neuen Ereigniszyklus und blockiert, bis _run() abgeschlossen ist. Die für die Demonstration verwendete Abfrage lautete:
Generate 8 random numbers between 5 and 50 using seed=42,
then calculate their statistics, and tell me if there are any outliers.
Starten Sie es von der Kommandozeile wie folgt:
python3 main.py --query "Generate 8 random numbers between 5 and 50 using seed=42, \
then calculate their statistics, and tell me if there are any outliers."
Ein statisches Info-Panel wird anschließend durch _print_header() ausgegeben:
╭────────────────────────────── 🔌 MCP Example ───────────────────────────────╮
│ Multi-Server MCP Demo │
│ │
│ Three FastMCP servers, each exposing tools + resources + prompts: │
│ ● Math Server — add, subtract, multiply, divide, power, sqrt, │
│ percentage │
│ ● Text Server — count_words, word_frequency, reverse, transform, │
│ extract_emails │
│ ● Data Server — generate_numbers, calculate_statistics, find_outliers, │
│ normalise │
│ │
│ Stack : FastMCP · LangChain · LangGraph · OpenAI · Rich │
│ Track : live Rich panels + JSONL log files under logs/ │
╰──────────────────────────────────────────────────────────────────────────────╯
Schritt 2: Erstellung des Agents
client/agent.py enthält run_query(query), die asynchrone Funktion, die alles koordiniert. Zunächst wird ein Tracker sowie eine Protokolldatei erstellt.
log_file = str(LOGS_DIR / f"run_{int(time.time())}.jsonl")
tracker = MCPTracker(log_file=log_file)
Bevor ein Server verbunden wird, wird ein MCPTracker instanziert und eine mit einem Zeitstempel versehene JSONL-Datei unter logs/ vorbereitet. Von da an wird jedes Ereignis in Echtzeit dort geschrieben.
Anschließend wird die Anfrage des Benutzers ausgegeben:
╭──── 💬 USER QUERY ────╮
│ Generate 8 random... │
╰────────────────────────╯
Dann werden die zu generierenden Server definiert:
def _server_config() -> Dict[str, Any]:
silent_env = {**os.environ, "FASTMCP_LOG_LEVEL": "ERROR"}
return {
"math_server": {
"command": sys.executable,
"args": ["servers/math_server.py"],
"transport": "stdio",
"env": silent_env,
},
"text_server": { ... },
"data_server": { ... },
}
Jeder Eintrag beschreibt einen als Unterprozess laufenden MCP-Server. Der command gibt den bereits laufenden Interpreter an (sys.executable), args nennt das Server-Skript, und transport legt fest, wie Client und Server miteinander kommunizieren, wobei stdio für Standard-Eingabe- und -Ausgabepipe steht. Das Feld env legt die Umgebung des Unterprozesses fest, und FASTMCP_LOG_LEVEL=ERROR unterdrückt die Startanzeige von FastMCP.
Der stdio-Transport ist der einfachste in MCP: Der Client schreibt JSON-RPC-Nachrichten in den stdin des Servers und liest Antworten aus seinem stdout ab, wodurch keine Netzwerkporte geöffnet werden.
Anschließend verbindet sich der Client mit jedem Server und entdeckt die verfügbaren Tools:
client = MultiServerMCPClient(_server_config())
tools = await client.get_tools()
MultiServerMCPClient aus langchain-mcp-adapters führt für jede Server-Einträge gleichzeitig folgende Schritte aus. Es startet einen Kindprozess, sendet die MCP initialize-Anfrage über stdio, sendet eine tools/list-Anfrage, erhält die Liste der Tool-Schemata und umhüllt jedes Schema in ein LangChain Tool-Objekt.
Sobald await client.get_tools() abgeschlossen ist, ergibt sich eine einfache Liste mit 19 StructuredTool-Objekten: sieben vom Mathematik-Server, sieben vom Text-Server und fünf vom Daten-Server. Diese Liste enthält die vollständige Reihe von Aktionen, die dem Modell zur Verfügung stehen.
Connected to 3 servers (math_server, text_server, data_server) with 19 tools: add, subtract, multiply,
divide, power, square_root, calculate_percentage, count_words, word_frequency, reverse_text,
transform_case, find_and_replace, extract_emails, count_vowels_consonants, generate_numbers,
calculate_statistics, find_outliers, sort_values, normalize_values
Dann werden das Sprachmodell und der Agent erstellt:
model = ChatOpenAI(model=model_name, temperature=0)
agent = create_react_agent(model, tools)
LangGraphs create_react_agent erstellt ein kompiliertes Zustandsdiagramm mit dieser Struktur:
START
│
▼
[call_model] ─── no tool call ──▶ END
│
tool call requested
│
▼
[call_tools]
│
▼
[call_model] (loop again with tool result in context)
Das ist das ReAct-Muster, also Schlussfolgern plus Handeln. Das Modell überlegt sich den nächsten Schritt, handelt, indem es ein Tool aufruft, beobachtet das Ergebnis und wiederholt diesen Vorgang, bis es genügend Informationen hat, um zu antworten.
Schließlich wird der Agent aufgerufen:
config = {"callbacks": [tracker]}
result = await agent.ainvoke({"messages": [("human", query)]}, config=config)
Der Eintrag "callbacks": [tracker] wird in LangChain’s Callback-System integriert. Immer dann, wenn das Framework etwas Wichtiges ausführt – beispielsweise einen Aufruf eines LLMs oder einer Tool-Funktion startet oder beendet – ruft es die entsprechende Methode in tracker auf. So kann der Tracker alles mitverfolgen, ohne innerhalb der Logik selbst zu liegen.
Schritt 3: In Echtzeit beobachten
MCPTracker erweitert LangChain’s BaseCallbackHandler. Seine Methoden werden niemals direkt aufgerufen, da LangChain sie zu den richtigen Zeitpunkten aufruft.
agent.ainvoke() called
│
├─▶ on_chain_start() "▶ AGENT STARTED" panel
│
├─▶ on_chat_model_start() "🤖 LLM CALL #1" panel (timer starts)
├─▶ on_llm_end() "LLM responded ⏱ 1.23s → will call: add, multiply"
│
├─▶ on_tool_start() "🔧 TOOL CALL #1" panel (timer starts)
├─▶ on_tool_end() "✓ TOOL RESULT ⏱ 0.01s" panel
│
├─▶ on_tool_start() (second tool, if any)
├─▶ on_tool_end()
│
├─▶ on_chat_model_start() "🤖 LLM CALL #2" (LLM synthesises final answer)
├─▶ on_llm_end() no tools this time → loop ends
│
└─▶ agent.ainvoke() returns
Jedes Mal, wenn on_tool_start ausgelöst wird, ermittelt der Tracker, welcher Server das Tool besitzt, und weist eine entsprechende Farbe zu. Der einzige Zweck ist die Lesbarkeit.
TOOL_SERVER_MAP = {
"add": "math_server",
"count_words": "text_server",
"generate_numbers": "data_server",
...
}
SERVER_COLORS = {
"math_server": "cyan",
"text_server": "green",
"data_server": "yellow",
}
Jedes Ereignis wird zu einer Zeile im JSON-Format in logs/run_<timestamp>.jsonl. Die Datei wird im Append-Modus geöffnet und sofort geschrieben, sodass das Ausführen von tail -f logs/run_*.jsonl in einem anderen Terminal die Rohdaten der Ereignisse anzeigt, während der Agent arbeitet. Nachdem agent.ainvoke() zurückgegeben hat, ruft client/agent.py manuell print_summary() auf, welches die Gesamtanzahl der LLM-Aufrufe, der Tool-Aufrufe sowie die verstrichene Zeit für den gesamten Lauf ausgibt.
Das erste anzuzeigende Panel zeigt den LLM-Aufruf Nummer 1 mit einer einzigen Nachricht:
── 🤖 LLM CALL #1 model=gpt-5-mini messages=1 ──╮
│ Role Content preview │
│ Human Generate 8 random numbers between 5 and 50… │
╰──────────────────────────────────────────────────────╯
Das ReAct-Diagramm hat gerade begonnen. Das Modell erhält den Systemprompt, der von LangGraph zusammen mit der Tool-Liste und deren Schemata eingefügt wird, sowie die eine menschliche Nachricht – deshalb gilt messages=1.
LLM responded ⏱ 5.35s → will call: generate_numbers
Der Tracker überprüfte die rohe Modellantwort. Anstelle von Prosa antwortete das Modell mit einer strukturierten Anfrage, ein Tool aufzurufen:
{
"name": "generate_numbers",
"arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}
}
In response.generations[0][0].message.tool_calls entdeckte der Tracker die Anfrage und gab ihren Namen aus. Die 5,35 Sekunden entsprechen einfach der Hin- und Rückreise zur OpenAI API. LangGraph liest diese Entscheidung und wechselt zum call_tools-Node, wodurch die Tool-Call-Nummer 1 erzeugt wird:
╭── 🔧 TOOL CALL #1 ──────────────────╮
│ Tool : generate_numbers │
│ Server: data_server │
│ Args : │
│ {'count': 8, 'min_val': 5, │
│ 'max_val': 50, 'seed': 42} │
╰───────────────────────────────────────╯
Aus dem Toolnamen generate_numbers zog der Tracker TOOL_SERVER_MAP heran, der auf data_server verweist, und färbte das Panel gelb, weil SERVER_COLORS["data_server"] den Wert "yellow" hat. Genau in diesem Moment sendet LangChain eine JSON-RPC tools/call-Nachricht über den stdio-Pipe an den Kindprozess data_server:
{"method": "tools/call", "params": {"name": "generate_numbers",
"arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}}}
Der Prozess nimmt es entgegen, ruft die Python-Funktion auf und schreibt das Ergebnis zurück in seinen stdout.
╭── ✓ TOOL RESULT ⏱ 0.63s ────────────────────────────╮
│ [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91] │
╰──────────────────────────────────────────────────────────╯
Es hat diesen genauen Code ausgeführt:
random.seed(42)
return [round(random.uniform(5, 50), 2) for _ in range(8)]
# → [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
Die 0,63 Sekunden umfassen die Zeit vom Senden der Anfrage bis zum Erhalten der Antwort über das Kommunikationskanal, einschließlich der Overhead-Kosten durch die Planung des Python-Prozesses. LangGraph fügt das Ergebnis als ToolMessage der Nachrichtenliste des Agents hinzu, sodass die Liste nun drei Einträge enthält: Mensch, KI (die Entscheidung zur Tool-Aufrufung) und Tool (das Ergebnis).
Danach folgt der zweite LLM-Aufruf mit drei Nachrichten:
╭── 🤖 LLM CALL #2 model=gpt-5-mini messages=3 ──╮
│ Role Content preview │
│ Human Generate 8 random numbers… │
│ AI (the tool-call decision) │
│ Tool [33.77, 6.13, 17.38, ...] │
╰──────────────────────────────────────────────────────╯
Das Modell wird erneut mit der gesamten bisherigen Historie aufgerufen. Es sieht die ursprüngliche Frage, seine eigene frühere Entscheidung, generate_numbers aufzurufen, sowie die zurückgegebenen Zahlen. Sein Denkprozess verläuft ungefähr wie folgt: Da die Zahlen bereits vorliegen und die Frage auch nach Statistiken gefragt hat, sollte calculate_statistics mit ihnen ausgeführt werden. Dadurch entsteht die zweite Toolaufrufnummer sowie ihr Ergebnis:
Tool : calculate_statistics
Server: data_server
Args : {'numbers': [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]}
Result: {"count":8, "min":6.13, "max":45.15, "mean":24.9962,
"median":25.575, "std_dev":14.8181, "variance":219.5755,
"range":39.02, "q1":15.04, "q3":38.14}
Das Modell erinnerte sich hier an nichts. Es nahm die Werte Wort für Wort aus dem früheren ToolMessage in seinem Kontextfenster und fügte sie den Argumenten des nächsten Tools hinzu. So sieht die Kettung in einem ReAct-Loop aus: Was ein Tool ausgibt, wird zum Kontext, den der folgende Modellaufruf lesen und wiederverwenden kann.
In dem LLM-Aufruf Nummer 3 gibt es fünf Nachrichten, und das Modell ruft find_outliers auf:
Tool : find_outliers
Server: data_server
Args : {'numbers': [...], 'threshold': 2}
Result: {"outliers": [], "outlier_count": 0,
"total_checked": 8, "mean": 24.9962, "std_dev": 14.8181}
Die Funktion find_outliers berechnet den z-Score jedes Wertes (z = (x − mean) / std_dev) und prüft, ob einer die Schwelle von 2 überschreitet. Das ist nicht der Fall, daher ist outliers eine leere Liste.
Die LLM-Aufrufnummer 4 zeigt sieben Nachrichten:
╭── 🤖 LLM CALL #4 messages=7 ──╮
│ Human / AI / Tool │
│ AI / Tool │
│ AI / Tool │
╰───────────────────────────────────╯
LLM responded ⏱ 30.25s
Das fehlende „will call“-Suffix signalisiert, dass das Modell gewöhnlichen Text erzeugt hat und nicht eine Tool-Anfrage, wodurch der ReAct-Loop beendet wird. Der Aufruf dauerte 30 Sekunden, da der Kontext umfangreich war (sieben Nachrichten inklusive roher JSON-Ergebnisse) und das Modell eine lange, ausführliche Synthese erstellte, bei der es für jeden Wert manuell einen z-Score berechnete. LangGraph findet in der Antwort keine Tool-Aufrufe und leitet zum END-Punkt weiter, der die endgültige Antwort ausgibt:
╭── ✅ FINAL ANSWER ───────────────────────────────╮
│ Random numbers: [33.77, 6.13, 17.38, ...] │
│ Mean: 24.9962 / Median: 25.575 / Std Dev: 14.8181│
│ Outliers: none (all |z| < 2) │
╰───────────────────────────────────────────────────╯
Dann erscheint die Laufzusammenfassung:
╭─────────────────────────────────────────────────────╮
│ LLM Calls 4 │
│ Tool Calls 3 │
│ Total Time 42.25s │
│ Log File logs/run_1776864980.jsonl │
╰─────────────────────────────────────────────────────╯
Die vollständige Nachrichtenfolge
Das sind alle Einträge in der Nachrichtenliste des Agents, als agent.ainvoke() zurückgegeben wurde:
1 HumanMessage "Generate 8 random numbers…"
2 AIMessage [tool_call: generate_numbers({count:8, min:5, max:50, seed:42})]
3 ToolMessage [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91]
4 AIMessage [tool_call: calculate_statistics({numbers:[...]})]
5 ToolMessage {count:8, mean:24.9962, std_dev:14.8181, ...}
6 AIMessage [tool_call: find_outliers({numbers:[...], threshold:2})]
7 ToolMessage {outliers:[], outlier_count:0, ...}
8 AIMessage "Here are the results. Random numbers: …" ← final answer
Jeder Aufruf des LLM erhält alles, was bisher gesammelt wurde, wodurch die Zahl messages in den Überwachungspanels um 1, 3, 5, 7 ansteigt.
Das JSONL-Log
Sämtliche oben genannten Ereignisse wurden ebenfalls im Laufe ihres Auftretens in logs/run_x.jsonl protokolliert, wobei pro Zeile ein JSON-Objekt enthalten ist:
{"timestamp": "...", "event": "agent_start", "data": {"runnable": "LangGraph", "run_id": "..."}}
{"timestamp": "...", "event": "llm_call", "data": {"call_number": 1, "model": "gpt-5-mini", "messages": 1}}
{"timestamp": "...", "event": "llm_end", "data": {"elapsed": "5.35s", "tool_calls_requested": ["generate_numbers"]}}
{"timestamp": "...", "event": "tool_start", "data": {"call_number": 1, "tool": "generate_numbers", "server": "data_server", "args": "..."}}
{"timestamp": "...", "event": "tool_end", "data": {"elapsed": "0.63s", "output_preview": "[33.77, 6.13, ...]"}}
{"timestamp": "...", "event": "llm_call", "data": {"call_number": 2, "model": "gpt-5-mini", "messages": 3}}
...
{"timestamp": "...", "event": "run_summary", "data": {"llm_calls": 4, "tool_calls": 3, "total_elapsed": "42.25s"}}
Warum das wichtig ist
Der eigentliche Wert von MCP liegt in der Verteilung. Vor der Einführung eines Standards musste jedes Team, das einer KI über ihre eigenen Fähigkeiten hinausgehende Aufgaben übertragen wollte, seine eigene Integrationsschicht entwickeln – und all diese Anstrengungen waren nicht auf andere Modelle oder Frameworks übertragbar. Heute wird ein Server nur einmal geschrieben und dient jedem kompatiblen Client. Ein Datenbankteam kann einen nur zum Lesen zugänglichen Server für sein Schema veröffentlichen, ein DevOps-Team kann Bereitstellungswerkzeuge bereitstellen und eine Datenplattform kann Abfragen sowie Analysefunktionen anbieten. Jeder Agent kann sich mit all diesen Systemen verbinden, und die Teams müssen sich niemals auf Implementierungsdetails einigen.
Weitere Literatur
- Ein Claude-Code-ähnlicher Agent mit LangChain Deep Agents neu aufbauen — Wie das Steuerungssystem eines Programmier-Agenten funktioniert, vom Kernloop über Tools, Planung, Speicher, Subagenten bis hin zu sandbox-basierten Genehmigungen, Schritt für Schritt mit DeepAgents neu aufgebaut.
- Zehn MCP-Server, die Entwicklern in Prioritätsreihenfolge zuerst installiert werden sollten — Eine nach Priorität geordnete Auswahlliste von MCP-Servern für KI-Programmier-Assistenten, von Context7 und GitHub über Postgres bis hin zu Speichermöglichkeiten, inklusive Installationskonfigurationen und Hinweisen zu Berechtigungen.