MCP w szczegółach: wiadomości JSON-RPC, serwery FastMCP i ślad ReAct
Zgodnie z protokołem Model Context Protocol, żądanie z narzędzi/list trafia do ostatecznej odpowiedzi za pośrednictwem serwerów FastMCP, agenta LangGraph ReAct oraz narzędzia śledzącego, które rejestruje każdy krok.
Model Context Protocol, czyli MCP, to otwarty standard stworzony przez Anthropic, który umożliwia modelom AI takim jak Claude dostęp do zewnętrznych narzędzi, źródeł danych i usług. Dobrym przykładem jest uniwersalna wtyczka: zamiast pisania specjalnej integracji dla każdego narzędzia, istnieje standardowy sposób na komunikację modelu z zewnętrznymi systemami.
Części składowe
Strukturę ten standard tworzą trzy elementy. Host MCP to aplikacja, której faktycznie używa użytkownik – czy to okno czatu, czy IDE – i zawiera ona klienta zarządzającego połączeniem. Protokół MCP stanowi standaryzowaną warstwę komunikacji pomiędzy hostem a serwerami. Każdy serwer MCP łączy się z konkretnym rodzajem zasobu, na przykład bazą danych, zewnętrznym API lub środowiskiem do wykonywania kodu, pełniąc rolę mostu do niego.
Co dzieje się podczas działania
Na wyższym poziomie żądanie przechodzi przez następujące etapy:
- Użytkownik wysyła zapytanie do hosta.
- Host pyta serwery MCP, jakie narzędzia one oferują.
- Serwery zwracają swoje listy narzędzi.
- Host wysyła zapytanie wraz z listą narzędzi do LLM.
- LLM wybiera jedno narzędzie i informuje o tym hosta.
- Host wywołuje odpowiedni serwer MCP, aby je uruchomić.
- Serwer wykonuje wybrane narzędzie i zwraca wynik.
- Host przekazuje ten wynik z powrotem do LLM.
- LLM pisze ostateczną odpowiedź, a host ją dostarcza.
Serwer udostępnia trzy rodzaje elementów. Narzędzia to funkcje, które model może wywołać, takie jak search_web, query_database lub send_email. Zasoby to dane dostępne do odczytu, które host może pobrać jako kontekst – np. pliki, rekordy bazy danych, wydarzenia kalendarzowe i dokumenty – które model może odczytać, ale nie wykonać. Wskazówki to z góry przygotowane szablony oferowane przez serwer. Na przykład serwer GitHub może dostarczyć szablon „podsumuj ten PR”, który host może pokazać użytkownikowi.
W gruncie rzeczy MCP to jedynie standaryzowany sposób, w jaki host może komunikować się z serwerem. Określa on format wiadomości oraz zasady konwersacji, i nic więcej. W szczególności definiuje cztery elementy.
- Kształt wiadomości. Wszystko odbywa się według standardu JSON-RPC 2.0. Prośba zawiera
id,methodorazparams, a odpowiedź powtarzaidwraz zresultluberror, dzięki czemu obie strony komunikują się w ten sam sposób. - Dostępne metody. Protokół określa ustaloną listę wywołań, które host może wykonać, na przykład
tools/listdo wyszukiwania,tools/calldo uruchomienia narzędzia,resources/readdo pobierania danych orazprompts/getdo pobierania szablonu. Serwery muszą je implementować. - Procedura nawiązywania połączenia. Podczas pierwszego połączenia obie strony wymieniają wiadomości
initialize, w których podają wersję protokołu oraz swoje możliwości, dzięki czemu każda strona wie, co obsługuje druga, zanim dojdzie do czegokolwiek innego.
Jedna wywołanie narzędzia składa się z czterech wiadomości. Najpierw host pyta, jakie narzędzia są dostępne:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
Serwer odpowiada opisami swoich narzędzi:
{
"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"]
}
}
]
}
}
Wtedy gospodarz wzywa jednego z nich:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "search_web",
"arguments": {
"query": "latest news on MCP protocol"
}
}
}
A serwer zwraca wynik:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Anthropic released MCP in Nov 2024 as an open standard..."
}
]
}
}
Najlepiej uczyć się poprzez tworzenie, dlatego dalsza część tego tekstu przedstawia prosty serwer MCP używany przez agenta.
Co robi FastMCP
FastMCP to framework w Pythonie do tworzenia serwerów MCP przy minimalnej ilości kodu szablonowego. Bez niego programista musiałby zaimplementować pętlę komunikacji JSON-RPC, serializować i deserializować każdą prośbę oraz odpowiedź, ręcznie tworzyć schematy JSON dla każdej dostępnej funkcji oraz obsługiwać komunikaty protokołu takie jak initialize, tools/list, tools/call, resources/list i prompts/list. Z jego pomocą wystarczy zwykła funkcja w Pythonie wraz z dekoratorem, a framework zajmuje się resztą:
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
Na podstawie adnotacji typów FastMCP samodzielnie buduje schemat JSON, a dokumentacja służy jednocześnie jako opis w języku potocznym, który model czyta podczas oceny, czy warto wywołać dany narzędzie. Wywołanie mcp.run() uruchamia pętlę, która blokuje się na stdin i stdout, używając formatu komunikacji MCP, i to właśnie do tej pętli podłącza się MultiServerMCPClient.
Trzy podstawowe elementy w serwerach przykładowych
Każdy serwer w projekcie to samodzielna aplikacja FastMCP, która udostępnia te trzy podstawowe rodzaje elementów.
Najpierw przychodzą narzędzia: funkcje, które model może wywoływać. Wystarczy ozdobić funkcję w Pythonie tagiem @mcp.tool(), a FastMCP przekształca te adnotacje oraz dokumentację w schemat JSON, który jest wysyłany do klienta podczas żądania tools/list. Model analizuje ten schemat, aby ustalić, kiedy należy użyć danego narzędzia oraz jakie argumenty podać.
# 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
Zasoby to dane tylko do odczytu znajdujące się pod adresem URI. Model nie wywołuje ich automatycznie, więc muszą być żądane wyraźnie. W tym projekcie służą jedynie do zilustrowania koncepcji, natomiast w zastosowaniach produkcyjnych mogą to być pliki konfiguracyjne, zrzuty stanu bazy danych lub dokumentacja referencyjna.
# 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",
)
URI math://formulas/{category} jest szablonem. Jego część {category} to parametr ścieżki, dzięki czemu math://formulas/geometry i math://formulas/algebra stanowią dwa odrębne zasoby serwowane przez tę samą funkcję.
Wskazówki to wielokrotnie używalne szablony instrukcji. Są to sformatowane wiadomości systemowe, których klient może żądać, a które umożliwiają zapakowanie specjalistycznej osobowości lub instrukcji zadania wewnątrz serwera, aby każdy połączony klient mógł je spójnie wykorzystywać.
# 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..."
)
Serwery łącznie udostępniają 19 narzędzi, jak pokazują następne sekcje.
Perspektywa klienta
Oto dokładnie to, co dzieje się po uruchomieniu python main.py z zapytaniem. Ścieżka żądania od początku do końca wygląda w ten sposób:
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
Krok 1: gdzie rozpoczyna się wykonywanie
main.py to pierwszy plik, który Python wykonywa. Ładuje zmienne środowiskowe, analizuje argumenty linii poleceń, wyświetla panel powitalny i przekazuje kontrolę do asynchronicznego wykonywacza.
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() tworzy nowy pętlę zdarzeń i blokuje się, dopóki _run() nie zostanie zakończony. Zapytanie użyte do demonstracji brzmiało:
Generate 8 random numbers between 5 and 50 using seed=42,
then calculate their statistics, and tell me if there are any outliers.
Uruchom z linii poleceń w następujący sposób:
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."
Paneel informacyjny statyczny jest następnie wydrukowywany za pomocą _print_header():
╭────────────────────────────── 🔌 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/ │
╰──────────────────────────────────────────────────────────────────────────────╯
Krok 2: tworzenie agenta
client/agent.py zawiera funkcję run_query(query) – asynchroniczną funkcję, która koordynuje całą akcję. Zaczyna od utworzenia trackera oraz pliku logów.
log_file = str(LOGS_DIR / f"run_{int(time.time())}.jsonl")
tracker = MCPTracker(log_file=log_file)
Zanim zostanie połączony jakikolwiek serwer, tworzony jest obiekt MCPTracker, a w katalogu logs/ przygotowywany jest plik JSONL z datą i godziną. Od tego momentu każde zdarzenie jest zapisywane tam w czasie rzeczywistym.
Następnie wyświetlana jest zapytanie użytkownika:
╭──── 💬 USER QUERY ────╮
│ Generate 8 random... │
╰────────────────────────╯
Następnie określone są serwery, z których mają powstać instancje:
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": { ... },
}
Każda pozycja opisuje jeden serwer MCP działający jako podproces. command to już uruchomiony interpreter (sys.executable), args określa nazwę skryptu serwera, a transport wskazuje, w jaki sposób komunikują się klient i serwer, przy czym stdio oznacza standardowe rury wejścia i wyjścia. Pole env ustawia środowisko podprocesu, a FASTMCP_LOG_LEVEL=ERROR tłumi komunikat startowy FastMCP.
Transport stdio jest najprostszym w MCP: klient zapisuje wiadomości JSON-RPC do stdin serwera i odczytuje odpowiedzi z jego stdout, więc nie otwiera się żaden port sieciowy.
Następnie klient łączy się z każdym serwerem i odkrywa dostępne narzędzia:
client = MultiServerMCPClient(_server_config())
tools = await client.get_tools()
MultiServerMCPClient z biblioteki langchain-mcp-adapters wykonywa dla każdej pozycji serwera następujące czynności jednocześnie. Tworzy proces potomny, wysyła przez stdio sygnał inicjalizacji MCP initialize, wysyła żądanie tools/list, otrzymuje listę schematów narzędzi i otacza każdy z nich obiektem LangChain Tool.
Gdy zakończy się wywołanie await client.get_tools(), rezultatem jest prosty lista 19 obiektów StructuredTool: siedem z serwera matematycznego, siedem z serwera tekstowego i pięć z serwera danych. Ta lista stanowi pełny zestaw działań dostępnych dla modelu.
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
Następnie tworzone są model językowy i agent:
model = ChatOpenAI(model=model_name, temperature=0)
agent = create_react_agent(model, tools)
create_react_agent z LangGraph tworzy skompilowany graf stanów o następującej strukturze:
START
│
▼
[call_model] ─── no tool call ──▶ END
│
tool call requested
│
▼
[call_tools]
│
▼
[call_model] (loop again with tool result in context)
To jest wzorzec ReAct – rozumowanie połączone z działaniem. Model rozważa następny krok, działa poprzez wywołanie narzędzia, obserwuje wynik i powtarza ten proces, aż uzyska wystarczająco dużo informacji, by odpowiedzieć.
W końcu wywoływany jest agent:
config = {"callbacks": [tracker]}
result = await agent.ainvoke({"messages": [("human", query)]}, config=config)
Wpis "callbacks": [tracker] łączy się z systemem powiadomień LangChain. Za każdym razem, gdy framework wykonuje jakąś istotną czynność, taką jak rozpoczęcie lub zakończenie wywołania modelu językowego lub narzędzia, wywołuje odpowiadającą metodę w obiekcie tracker. Dzięki temu tracker może obserwować wszystko, nie będąc częścią żadnej logiki.
Krok 3: obserwacja w czasie rzeczywistym
MCPTracker rozszerza klasę BaseCallbackHandler z LangChain. Jego metody nigdy nie są wywoływane bezpośrednio, ponieważ LangChain wykonuje je we właściwych momentach.
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
Gdy tylko aktywuje się on_tool_start, tracker określa, który serwer posiada narzędzie, i przypisuje mu odpowiedni kolor. Jedyne celem jest lepsza czytelność.
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",
}
Każde zdarzenie staje się jedną linią w pliku JSON w logs/run_<timestamp>.jsonl. Plik jest otwierany w trybie dodawania i natychmiast zapisywany, więc uruchomienie tail -f logs/run_*.jsonl w innym terminalu pokazuje surowe zdarzenia pojawiające się w miarę pracy agenta. Po zwróceniu się z agent.ainvoke() plik client/agent.py ręcznie wywołuje funkcję print_summary(), która podaje łączną liczbę wywołań LLM, wywołań narzędzi oraz czas trwania całego procesu.
Pierwszym elementem, który się pojawia, jest wywołanie LLM numer 1, zawierające jedną wiadomość:
── 🤖 LLM CALL #1 model=gpt-5-mini messages=1 ──╮
│ Role Content preview │
│ Human Generate 8 random numbers between 5 and 50… │
╰──────────────────────────────────────────────────────╯
Graf ReAct właśnie się uruchomił. Model otrzymuje komendę systemową wstrzykniętą przez LangGraph wraz z listą narzędzi i ich schematami, oraz jedną wiadomość od człowieka, stąd messages=1.
LLM responded ⏱ 5.35s → will call: generate_numbers
Tracker sprawdził surową odpowiedź modelu. Zamiast tekstu prozatorskiego, model odpowiedział strukturyzowanym żądaniem wywołania narzędzia:
{
"name": "generate_numbers",
"arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}
}
Wewnątrz response.generations[0][0].message.tool_calls tracker zlokalizował żądanie i wyświetlił jego nazwę. 5,35 sekundy to po prostu czas potrzebny na przejście tam i z powrotem do API OpenAI. LangGraph odczytuje tę decyzję i przechodzi do węzła call_tools, który generuje numer 1 dla wywołania narzędzia:
╭── 🔧 TOOL CALL #1 ──────────────────╮
│ Tool : generate_numbers │
│ Server: data_server │
│ Args : │
│ {'count': 8, 'min_val': 5, │
│ 'max_val': 50, 'seed': 42} │
╰───────────────────────────────────────╯
Z nazwy narzędzia generate_numbers tracker skorzystał z tabeli TOOL_SERVER_MAP, która odwołuje się do data_server, i pokolorował panel na żółto, ponieważ SERVER_COLORS["data_server"] ma wartość "yellow". W tym dokładnym momencie LangChain wysyła wiadomość JSON-RPC typu tools/call przez kanał stdio do procesu potomnego data_server:
{"method": "tools/call", "params": {"name": "generate_numbers",
"arguments": {"count": 8, "min_val": 5, "max_val": 50, "seed": 42}}}
Proces otrzymuje go, uruchamia funkcję w Pythonie i zapisuje wynik z powrotem do swojego stdout.
╭── ✓ TOOL RESULT ⏱ 0.63s ────────────────────────────╮
│ [33.77, 6.13, 17.38, 15.04, 38.14, 35.45, 45.15, 8.91] │
╰──────────────────────────────────────────────────────────╯
Wykonał ten dokładny kod:
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]
0,63 sekundy to czas od wysłania żądania do otrzymania odpowiedzi przez kanał komunikacji, włączając koszty związane z planowaniem procesów w Pythonie. LangGraph dodaje wynik do listy wiadomości agenta jako ToolMessage, dzięki czemu lista zawiera teraz trzy elementy: informację od człowieka, decyzję o wywołaniu narzędzia przez AI oraz sam wynik.
Następuje wywołanie LLM numer 2, które obejmuje trzy wiadomości:
╭── 🤖 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, ...] │
╰──────────────────────────────────────────────────────╯
Model jest ponownie wywoływany z całym dotychczasowym historią. Widzi oryginalne pytanie, własną wcześniejszą decyzję o wywołaniu funkcji generate_numbers oraz liczby, które wróciły. Jego rozumowanie przebiega mniej więcej w ten sposób: liczby są już dostępne, a w pytaniu poproszono również o statystyki, więc należy uruchomić funkcję calculate_statistics z ich użyciem. To generuje drugi wywołanie narzędzia oraz jego wynik:
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}
Tutaj model nic nie zapamiętał. Wybrał wartości słowo po słowie z wcześniejszego ToolMessage w swoim oknie kontekstowym i umieścił je w argumentach następnego narzędzia. W ten sposób wygląda łączenie narzędzi w pętli ReAct: to, co wypuszcza jedno narzędzie, staje się kontekstem, który kolejne wezwanie modelu może odczytać i ponownie wykorzystać.
W trzecim wezwaniu LLM znajduje się pięć wiadomości, a model wywołuje funkcję find_outliers:
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}
Funkcja find_outliers oblicza wartość z-score dla każdej wartości (z = (x − mean) / std_dev) i sprawdza, czy któraś przekracza próg 2. Żadna z nich tego nie robi, więc outliers to pusta lista.
Numer wezwania LLM 4 zawiera siedem wiadomości:
╭── 🤖 LLM CALL #4 messages=7 ──╮
│ Human / AI / Tool │
│ AI / Tool │
│ AI / Tool │
╰───────────────────────────────────╯
LLM responded ⏱ 30.25s
Brakujący sufiks „will call” wskazuje, że model wygenerował zwykły tekst, a nie żądanie użycia narzędzia, co zamyka pętlę ReAct. Rozmowa trwała 30 sekund, ponieważ kontekst był obszerny (siedem wiadomości, w tym surowe wyniki w formacie JSON) i model musiał stworzyć długą, szczegółową syntezę, ręcznie obliczając wartość z-score dla każdej wartości. LangGraph nie znajduje żadnych żądań użycia narzędzi w odpowiedzi i kieruje proces do END, który wyświetla ostateczną odpowiedź:
╭── ✅ 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) │
╰───────────────────────────────────────────────────╯
Następnie pojawia się podsumowanie biegu:
╭─────────────────────────────────────────────────────╮
│ LLM Calls 4 │
│ Tool Calls 3 │
│ Total Time 42.25s │
│ Log File logs/run_1776864980.jsonl │
╰─────────────────────────────────────────────────────╯
Pełna sekwencja wiadomości
Oto wszystkie elementy listy wiadomości agenta, gdy zwróciła się funkcja agent.ainvoke():
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
Każde wezwanie LLM otrzymuje wszystko, co zostało dotychczas zgromadzone, więc wartość messages w panelach śledzenia rośnie o 1, 3, 5, 7.
Log JSONL
Wszystkie powyższe zdarzenia były również zapisywane do logs/run_x.jsonl w momencie ich wystąpienia, przy czym każda linia zawiera jeden obiekt JSON:
{"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"}}
Dlaczego to ma znaczenie
Prawdziwa wartość MCP polega na możliwości dystrybucji. Przed pojawieniem się standardu każdy zespół, który chciał wykorzystać funkcje przekraczające możliwości modeli LLM, musiał sam stworzyć warstwę integracyjną, a cała ta praca nie była przenoszona na inne modele czy frameworki. Teraz serwer zostaje napisany raz i może obsługiwać dowolnego zgodnego z standardem klienta. Zespół zajmujący się bazami danych może opublikować serwer tylko do odczytu dla swojego schematu, zespół DevOps może udostępnić narzędzia do wdrażania, a platforma danych może oferować funkcje zapytań i analiz. Każdy agent może się do nich połączyć, a zespoły nie muszą się zgadzać co do szczegółów implementacji.
Pozycje pokrewne
- Odbudowa agenta w stylu Claude Code za pomocą LangChain Deep Agents — Jak działa mechanizm agenta programistycznego, od podstawowego pętla po narzędzia, planowanie, pamięć, podagenty oraz zatwierdzenia w środowisku izolowanym, odbudowywany krok po kroku przy użyciu deepagents.
- Te serwery MCP, które warto zainstalować najpierw, w kolejności priorytetowej dla programistów — Uporządkowana lista serwerów MCP przeznaczonych do asystentów programistycznych opartych na AI, od Context7 i GitHub po Postgres oraz mechanizmy pamięci, wraz z konfiguracjami instalacji i wskazówkami dotyczącymi uprawnień.