MCP dla agentów sztucznej inteligencji: standaryzacja integracji narzędzi w LangGraph
Ten artykuł wyjaśnia, co dokładnie standaryzuje MCP w systemach sztucznej inteligencji opartych na agentach, porównując integracje narzędzi ad hoc z tymi opartymi na MCP w ramach orkiestratora LangGraph.
Wprowadzenie i podsumowanie
Pod koniec Części 2 system osiągnął rzeczywiście skoordynowany stan: zbiór specjalistycznych agentów, z których każdy był odpowiedzialny za wąski zakres zadań, działających w ramach wspólnego stanu pod kierownictwem orkiestratora, który decydował, co powinno zostać uruchomione dalej. W każdym przykładzie zakładano jednak, że każdy agent ma już dostęp do wszystkiego, czego potrzebuje w tym wspólnym stanie, i może to odczytać w dowolnym momencie, gdy jest to konieczne.
To założenie rzadko sprawdza się w środowisku produkcyjnym. Agent zazwyczaj potrzebuje czegoś, co znajduje się poza grafem: wiersza z bazy danych, odpowiedzi z zewnętrznego API, fragmentu tekstowego pobranego z bazy wiedzy lub innego zasobu poza granicami samego systemu. Ilekroć agent musi w ten sposób dotrzeć poza te granice, potrzebuje własnej ścieżki do tego celu, a jeśli każda z tych ścieżek jest budowana ręcznie, kończy się to powtarzaniem tego samego rodzaju pracy integracyjnej, lekko zmodyfikowanej, dla każdego agenta i każdego zewnętrznego zasobu.
To powtarzanie jest dokładnie problemem, którym zajmuje się ten materiał, i wyjaśnia, dlaczego MCP stało się tak częstym tematem w ciągu ostatniego roku. Zanim zdecydujemy, czy jego przyjęcie jest opłacalne, pomocne jest dokładne określenie problemu, który rozwiązuje, oraz przyjrzenie się temu, co polegało na połączeniu narzędzia z agentem przed pojawieniem się MCP.
Problem, który MCP ma rozwiązać
Zanim pojawił się MCP, udzielenie agentowi dostępu do zasobu zewnętrznego oznaczało ręczne tworzenie specjalistycznej integracji dostosowanej do danego zasobu, kształtowanej w sposób, który wydawał się w danym momencie sensowny. Jeden zasób mógł być dostępny przez REST API, inny przez klienta bazy danych, a jeszcze inny przez SDK, które miało własne zasady autoryzacji i obsługi błędów. Każda z tych różnic musiała być bezpośrednio włączona do kodu samego agenta.
To dobry kompromis, gdy jeden agent rozmawia z jednym narzędziem. Staje się on niewystarczający, gdy tylko system rozwija się w którymkolwiek kierunku. Dodaj drugiego agenta, który potrzebuje tych samych zasobów – wtedy albo kopiujesz integrację, albo ktoś ostatecznie przenosi ją do wspólnego modułu, zazwyczaj dopiero po tym, jak duplikacja już się pojawiła. Zamiast tego dodaj drugie narzędzie, a wtedy kod agenta musi jednocześnie zawierać dwa zupełnie różne wzory integracji.
Takie integracje rzadko przypominają się nawzajem, ponieważ nic tego nie wymaga. Jeden wrapper może automatycznie próbować ponownie wykonać nieudane żądania; inny może w ogóle ich nie próbować. Jeden może pokazywać błędy jako rzucane wyjątki; inny może je ukrywać w polu statusu, o którym użytkownik musi pamiętać, by je sprawdzić. Nie istnieje wspólny język opisujący to, co tak naprawdę oznacza „połączenie agenta z narzędziem” — każda integracja odpowiada na to pytanie na własnych warunkach.
To właśnie ta luka ma zostać zamknięta przez MCP. Nie wprowadza on nowych funkcjonalności, których wcześniej brakowało agentom; raczej standaryzuje mechanizm dostępu do funkcji, które już istniały, tak aby podłączenie nowego agenta do istniejącego narzędzia lub nowego narzędzia do istniejącego agenta nie oznaczało już konieczności pisania kolejnej, specjalnie dostosowanej integracji od zera. Ocena, czy faktycznie spełnia to swoje obietnice w praktyce, staje się znacznie łatwiejsza po zobaczeniu, jak wygląda podejście ad hoc w kodzie — właśnie tam kontynuuje się dyskusja.
Przed MCP: Improwizowane łączenie narzędzi
Rozważmy dość zwyczajną integrację: agenta, który musi wysyłać zapytania do zewnętrznego systemu, połączonego za pomocą kodu łączącego, który umożliwia komunikację.
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}
Sam w sobie nie ma w tym nic złego. To kompaktowy klient HTTP, proste zarządzanie błędami oraz funkcja, która go wywołuje z wnętrza węzła. Problemy pojawiają się, gdy do gry wchodzi drugi narzędzie – tym razem nie kolejna REST API, lecz klient bazy danych o zupełnie innej strukturze:
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))
Te dwa klienty nie mają wspólnej interfejsu, żadnej konwencji nazewniczej, a nawet spójnego sposobu sygnalizowania błędów: jeden zwraca słownik błędów, drugi po prostu rzuca wyjątek. Każdy agent, który musi korzystać z obu, musi samodzielnie poznać te osobliwości i radzić sobie z każdą z nich na własne sposoby. Gdy do tego dodamy wszystkie kolejne narzędzia, których system w końcu potrzebuje – swój własny klient, własny schemat autoryzacji, własny sposób radzenia sobie z błędami – to to, co zaczęło się jako kilka małych integracji, zamienia się w prawdziwy ciężar konserwacyjny, pozbawiony wspólnej struktury, która mogłaby je połączyć.
To jest podstawa, do której należy się tu przytrzymać: nie chodzi o źle napisaną integrację, lecz o typową, taką, jaka powstaje w większości integracji narzędzi, gdy nic nie wymusza na nich wspólnego kształtu.
Czym właściwie standaryzuje MCP
Mając wciąż na myśli ten przykład ad hoc, o wiele łatwiej jest dokładnie opisać, co robi MCP, bez uciekania się do szerszych i bardziej niejasnych stwierdzeń często na jego temat.
W swojej istocie MCP definiuje wspólny protokół służący do udostępniania narzędzi agentowi, niezależnie od tego, co narzędzie robi wewnętrznie lub w jakim języku lub frameworku zostało stworzone. Zamiast każde narzędzie dostarczać własnego, specjalnie zaprojektowanego klienta z własnymi konwencjami, jest ono udostępniane przez serwer MCP, który reklamuje swoje możliwości w przewidywalnym, standardowym formacie: nazwa, opis, schemat wejścia i schemat wyjścia. Każdy agent, który rozumie ten protokół, może odkryć to narzędzie i użyć do niego tych samych metod co w przypadku dowolnego innego narzędzia, niezależnie od tego, czy pod spodem znajduje się punkt końcowy REST, baza danych czy coś zupełnie innego.
To ujednolicenie obejmuje dokładnie trzy obszary, i warto precyzyjnie określić, które to są, ponieważ łatwo jest założyć, że zakres MCP jest szerszy, niż w rzeczywistości.
- Odkrywanie: Agent może zapytać serwer MCP o listę narzędzi, które są dostępne, i otrzymać ustrukturyzowaną odpowiedź, zamiast polegać na tym, że te informacje są wgrane bezpośrednio lub udokumentowane oddzielnie od faktycznej implementacji.
- Wezwanie: Każde wezwanie narzędzia follows ten sam wzorzec, niezależnie od tego, jakie jest to narzędzie – prośba o określonej strukturze, odpowiedź o określonej strukturze – zamiast tego, by każdy klient definiował swój własny sygnaturę metody i swój własny typ zwracany.
MCP nie eliminuje konieczności pisania logiki leżącej u podstaw narzędzia, ani nie gwarantuje poprawnego działania narzędzia tylko dlatego, że jest opakowane w ten protokół. Standardyzuje on umowę pomiędzy agentem a narzędziem, a nie jakość lub niezawodność tego, co znajduje się za tą umową — na ten punkt artykuł wraca bezpośrednio później, gdy porównania wcześniej przedstawione uwyraźniły tę różnicę.
Anatomia serwera MCP
Biorąc pod uwagę właśnie opisaną standaryzację, warto przyjrzeć się temu, co ją faktycznie wdraża. Strukturalnie serwer MCP to niewiele więcej niż zdefiniowany zestaw narzędzi, z których każde posiada własny schemat, zapakowane w warstwę protokołu umożliwiającą agentowi ich odkrywanie i wywoływanie w spójny sposób.
Jako minimum, aby zdefiniować narzędzie w serwerze MCP, konieczne jest określenie trzech elementów: nazwy, której agent używa do odniesienia się do niego, schematu opisującego oczekiwane dane wejściowe oraz funkcji, która jest wykonywana podczas faktycznego wywołania narzędzia.
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}")
Gdy porównamy to z wcześniej omawianymi klientami ad hoc, wyróżniają się tu dwa istotne aspekty. Po pierwsze, schemat danych wejściowych jest wyraźnie deklarowany na początku, zamiast być wnioskowany z parametrów, które funkcja akurat przyjmuje — dzięki temu zarówno agent, jak i ludzki recenzent mogą dokładnie zobaczyć, czego wymaga narzędzie, bez konieczności zagłębiania się w jego implementację. Po drugie, serwer musi udostępnić tylko dwa punkty wejścia: list_tools i call_tool, niezależnie od tego, ile narzędzi zawiera lub jak różnie zachowują się wewnątrz. To, czy narzędzie lookup komunikuje się z API REST, wysyła zapytania do bazy danych, czy robi coś zupełnie innego, pozostaje w pełni ukryte za tą samą dwufunkcyjną interfejsem.
Z perspektywy agenta połączenie z tym serwerem wygląda identycznie, niezależnie od tego, jakie narzędzia on udostępnia:
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
Porównaj to z dwoma wcześniejszymi klientami ad hoc – jednym opartym na requests, drugim na psycopg2 – z których każdy miał własną strukturę i konwencje. W tym przypadku kod agenta pozostaje niezmienny, bez względu na to, co narzędzie robi wewnętrznie – wywołuje session.call_tool z nazwą i zestawem argumentów, otrzymując wynik w tej samej strukturze za każdym razem. To właśnie ta jednolitość stanowi prawdziwą korzyść architektury serwerowej, a nie sama logika narzędzia, którą i tak ktoś musi napisać.
Przebudowa tej samej integracji za pomocą MCP
Najlepszym sposobem na zobaczenie praktycznej różnicy jest wzięcie dokładnie tej samej integracji stworzonej wcześniej – narzędzia wyszukiwania i klienta do przeglądania rekordów – oraz jej odbudowa z użyciem MCP. Funkcjonalność pozostaje identyczna, systemy podstawowe również są takie same, zmienia się jedynie interfejs.
Narzędzie wyszukiwania, które początkowo było samodzielnym klientem opartym na requests, teraz staje się deklaracją narzędzia zarejestrowaną na serwerze MCP:
@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}")
Żadna z logik wewnętrznych nie została zmieniona – perform_lookup nadal korzysta z tego samego zewnętrznego API, a fetch_record_from_db nadal wykonywa tę samą zapytanie do bazy danych. Różnica polega na tym, że te dwa narzędzia, mimo iż opierają się na zupełnie niepowiązanych systemach, są teraz deklarowane obok siebie, dzielą identyczny schemat i są dostępne przez te same dwa punkty wejścia.
Największa widoczna zmiana ma miejsce w węźle odpowiedzialnym za ich wywoływanie, ponieważ nie musi on już uwzględniać requests ani psycopg2:
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}
To stoi w sprzeczności z wcześniejszą wersją, która importowała określoną bibliotekę HTTP, parsowała specyficzny format błędów i wymagała zupełnie odrębnego ścieżki kodu tylko po to, by wywołać fetch_record zamiast lookup. W tej wersji wywołanie drugiego narzędzia polega jedynie na zamianie ciągu znaków i słownika argumentów, a nie na tworzeniu drugiego klienta z jego własnymi specyfikami.
Nic się nie zmieniło w tym, co faktycznie osiągają te narzędzia. Zmieniło się to, że agent używający ich nie musi już w ogóle rozumieć ich implementacji, wystarczy mu nazwa oraz deklarowane schemat. To właśnie standaryzacja omawiana wcześniej w serii – teraz coś, na co można się powołać w rzeczywistym kodzie, zamiast polegać wyłącznie na zaufaniu.
Porównanie: Ad Hoc vs. MCP
Gdy obie metody zostaną w pełni opracowane, można przestać rozważać teoretyczne zalety i zamiast tego bezpośrednio przyjrzeć się temu, co faktycznie różni wersję ad hoc od wersji MCP.
- Zysiłek wymagany przy dodawaniu nowego narzędzia: W podejściu ad hoc każde dodatkowe narzędzie wymagało własnego klienta, własnej logiki autoryzacji, własnego formatu błędów oraz zestawu konwencji, których trzeba było się nauczyć, zanim można je było bezpiecznie używać. W ramach MCP dodanie narzędzia oznacza dodanie kolejnego wpisu do
list_toolsoraz kolejnej gałęzi wewnątrzcall_tool, przy czym każda z nich follows identyczny wzorzec narzędzia, które ją poprzedza. - To, co kod wywołujący musi rozumieć: Węzeł agenta w podejściu ad hoc musiał importować określoną bibliotekę klienta i obsługiwać specyficzny format błędów tej biblioteki. Węzeł agenta MCP nie importuje niczego specyficznego dla danego narzędzia – po prostu wywołuje
session.call_toolz nazwą i słownikiem argumentów, a to wywołanie zachowuje się tak samo, niezależnie od tego, czy podstawowe narzędzie to punkt końcowy REST, baza danych czy cokolwiek innego.
LookupToolClient, a każdy agent, który od niego zależał, automatycznie przyjmował te zmiany. MCP nie eliminuje tego obciążenia konserwacyjnego, ale je uporządkowuje: zachowanie każdego narzędzia znajduje się teraz za tymi samymi dwiema funkcjami, list_tools i call_tool, zamiast być rozproszone w różnych formach klientów odpowiadających poszczególnym narzędziom.Nic z tego nie upraszcza rzeczywistej logiki narzędzia. Funkcje perform_lookup i fetch_record_from_db nadal muszą zostać napisane i funkcjonować poprawnie bez względu na wszystko inne. Zmienia się natomiast wszystko, co otacza tę logikę: sposób jej odkrywania, sposób wywoływania, sposób wykrywania błędów oraz to, jak dużym obciążeniem musi się zmagać sam kod agenta w porównaniu z sytuacją, gdy jest ono automatycznie obsługiwane poprzez przestrzeganie wspólnego protokołu.
Integracja narzędzia MCP z systemem LangGraph (część 2)
Pamiętajmy o grafie z Części 2: kroku pobierania danych, interpretacji przez LLM, silniku reguł przedstawionym w Części 1 oraz routingu warunkowym, który kieruje przepływ albo do węzła powiadamiania, albo na sprawdzenie przez człowieka. Wyobraźmy sobie, że silnik reguł musi teraz sprawdzić dane w jakimś zewnętrznym systemie, zanim będzie mógł podjąć decyzję – taka zależność oznaczałaby konieczność stworzenia dedykowanego klienta ad hoc, podobnego do tego, który został wcześniej opisany w tym artykule i podłączonego bezpośrednio do danego węzła.
Gdy serwer MCP udostępnia tę samą funkcję wyszukiwania jako narzędzie, obowiązki węzła prawie się nie zmieniają. Nadal odczytuje dane z stanu grafu i nadal zapisuje swoją decyzję z powrotem do tego stanu, tylko aby uzyskać dostęp do zewnętrznego systemu, korzysta z metody session.call_tool, zamiast używać dedykowanego klienta:
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"]}
Pozycja węzła w całym przepływie nie ulega żadnym zmianom pod wpływem tego wszystkiego. Nadal znajduje się w tym samym miejscu, poniżej llm_interpretation i powyżej gałęzi warunkowej, która istniała już w Części 2; nadal jest ograniczona zakresem uprawnień określonym w omówieniu zasad tego artykułu, a także nadal jest rejestrowana pod tym samym identyfikatorem śledzenia opisanym w jego rozdziale dotyczącym obserwowalności. Jedyną rzeczywistą zmianą jest sposób, w jaki węzeł komunikuje się ze światem zewnętrznym: zamiast polegać na kliencie stworzonym specjalnie dla tego węzła, teraz wysyła żądanie do narzędzia, które każdy inny węzeł, zarówno w tym grafie, jak i w przyszłym, może wywołać tą samą ścieżką.
To właśnie tutaj łączą się trzy elementy tej serii. Agent, który podejmuje decyzje wyłącznie wtedy, gdy ma na to wyraźne uprawnienia, działający w ramach grafu, który koordynuje jego działania z innymi agentami, oraz możliwość łączenia się z systemami zewnętrznymi za pośrednictwem tej samej standaryzowanej interfejsu, którą używają wszyscy agenci. Żaden z tych trzech elementów – silnik reguł, graf czy MCP – nie musi mieć szczegółowej wiedzy o pozostałych dwóch. Wystarczy, że będą przestrzegać tej samej zasady, na której kładła nacisk ta seria: wąskie zakresy odpowiedzialności, wyraźne umowy i brak przypuszczeń.
Koszt i opóźnienie: ile naprawdę kosztuje orkiestracja
Każda warstwa dodawana w ramach tej serii wprowadza pewien stopień struktury, a żadna z tych struktur nie jest darmowa. Zamiast ukrywać te koszty, pomocne jest zbadanie, co faktycznie dzieje się z czasem reakcji, gdy żądanie przechodzi przez wszystkie elementy opisane dotychczas.
Rozważmy pojedynczą prośbę przetwarzaną w grafie przedstawionym w Części 2, teraz rozszerzonym o wyszukiwanie oparte na MCP omówione powyżej. Ścieżka przetwarzania wygląda mniej więcej w ten sposób: moduł intake wykonuje lżekie formatowanie bez żadnych wywołań zewnętrznych, więc zajmuje co najwyżej kilka milisekund. Krok interpretacji przez LLM wywołuje model w celu wyodrębnienia ustrukturyzowanych pól z surowej prośby, a to zazwyczaj stanowi największy koszt w całej ścieżce, dodając często kilkaset milisekund w zależności od wybranego modelu i długości promptu. Wywołanie narzędzia wyszukiwania przez silnik reguł za pomocą MCP powoduje dodatkową podróż sieciową oprócz czasu potrzebnego samemu narzędziu na odpowiedź – jest to znaczący koszt, ale zazwyczaj mniejszy niż krok z udziałem LLM. Ocena samych reguł, ponieważ polega jedynie na logice deterministycznej, jest praktycznie bezkosztowa. Routing oraz krok wysyłania powiadomienia końcowego dodają jedynie niewielką ilość czasu do tego wszystkiego.
Jeśli dodamy te wartości, obraz staje się jasny: żądanie, które wymaga tylko jednej wizyty u modelu oraz jednego wywołania narzędzia, ma swoją całkowitą czasochłonność określaną niemal wyłącznie przez te dwie operacje, przy czym sama koordynacja w grafie prawie nie ma wpływu. Węzły, krawędzie oraz wspólny stan przedstawione w Części 2 zapewniają strukturę bez wprowadzania istotnego opóźnienia – odczytanie i zapisanie obiektu stanu wspólnego jest kosztowne w niskim stopniu. To, co faktycznie zajmuje czas, to kontakt z modelem lub zewnętrznym systemem.
To zmienia sposób, w jaki powinno się patrzeć na optymalizację wydajności. Dodawanie większej liczby węzłów, gałęzi routingu lub dodatkowych sprawdzeń do grafu prawie w ogóle nie wpływa na szybkość, ponieważ są to jedynie wywołania funkcji i wyszukiwania w słowniku. To, co faktycznie spowalnia system, to każde wywołanie LLM oraz każde wywołanie narzędzia zewnętrznego, które znajduje się na krytycznej ścieżce żądania. System zbudowany wokół trzech agentów, z których każdy wywołuje model kolejno po sobie, będzie działał znacznie wolniej niż taki, który wywołuje model tylko raz, bez względu na to, jak sprawna jest otaczająca go orkiestracja.
Praktyczna zasada jest prosta: gdy opóźnienie ma znaczenie w procesie pracy, nie zaczynaj od analizy kształtu grafu. Najpierw policz liczby wywołań modelu oraz połączeń z zewnętrznymi narzędziami, przez które musi przejść standardowa prośba, zanim zostanie zakończona, i zastanów się, czy niektóre z nich można wykonywać równolegle zamiast kolejno, lub całkowicie pominąć w przypadku próśb, które ich nie wymagają.
Czego MCP nie rozwiązuje
Warto być równie szczerym co w przypadku wcześniej opisanych zalet MCP, jeśli chodzi o jego ograniczenia, ponieważ większość materiałów na ten temat skupia się głównie na zaletach i rzadko porusza ograniczenia. Te same trzy obszary omówione wcześniej – odkrywanie, wywoływanie i obsługa błędów – zasługują na ponowną analizę z odwrotnej perspektywy.
- Standaryzacja procesu znajdowania i wywoływania narzędzi nie naprawia źle zaprojektowanego narzędzia: standard MCP określa, w jaki sposób narzędzie może być znalezione i wywołane, a nie to, co dzieje się w jego wnętrzu. Narzędzie o wolnej, niestabilnej lub źle zorganizowanej implementacji pozostaje wolne, niestabilne i źle zorganizowane, nawet gdy znajduje się za serwerem MCP. Protokół przenosi tę niespójność z kodu wywołującego do samej implementacji narzędzia, ale nie sprawia, że znika ona.
Nic z tego nie przemawia przeciwko przyjęciu MCP. To przypomnienie, że wybór protokołu nie zastępuje pracy inżynieryjnej, która nadal musi zostać wykonana w jego tle. Zmiana wprowadzona przez MCP jest rzeczywista, jak pokazano we wcześniejszych sekcjach, ale jest węższa niż pojęcie „agenty łączące się z narzędziami” jako całość, i warto być precyzyjnym co do tego, gdzie dokładnie leży ta granica, zanim zdecydujemy, czy przyjęcie MCP ma sens – o tym właśnie traktuje następna sekcja.
Kiedy warto przyjąć MCP
Biorąc pod uwagę przedstawione powyżej kompromisy, nie ma tu jednoznacznej odpowiedzi – ani prostego „zawsze je przyjmuj”, ani „w ogóle się tym nie przejmuj”. Ważne jest natomiast sprawdzenie kilku konkretnych warunków przed zastosowaniem go w danym systemie.
- Więcej niż jeden agent będzie potrzebował tych samych narzędzi. Prawie cała korzyść opisana wcześniej wynika z ponownego wykorzystania: drugi agent może połączyć się z już istniejącym serwerem, zamiast kopiować klienta lub tworzyć go później. Gdy istnieje tylko jeden agent i jedno narzędzie, takiej korzyści po prostu jeszcze nie ma – nie ma nic do udostępnienia – więc wcześniej omówione podejście ad hoc pozostaje prostszym wyborem w tym konkretnym przypadku.
- Oczekuje się wzrost liczby narzędzi. Standaryzowana interfejs staje się coraz cenniejsza, gdy za nią znajduje się coraz więcej narzędzi. Przy dwóch lub trzech narzędziach uruchamianie dedykowanego serwera może nie być warte dodatkowych kosztów. Gdy mamy do czynienia z dziesięcioma lub dwudziestoma narzędziami – z których każde w przeciwnym razie wymagałoby własnego, dostosowanego klienta – obciążenie związane z konserwacją w ramach podejścia ad hoc zaczyna stanowić poważny problem.
Z drugiej strony, taki model nie nadaje się w przypadku pojedynczego agenta komunikującego się z jednym stabilnym, dobrze znanym narzędziem, które nie zmieni się. W takiej sytuacji szybciej jest stworzyć specjalistyczny klient, pozostaje mniej elementów wymagających konserwacji, a koordynacja zapewniana przez MCP nie ma drugiego użytkownika, który mógłby ją faktycznie wykorzystać.
Test praktyczny, który należy tu zastosować, jest podobny do tego użytego wcześniej dla samego silnika reguł: czy dodanie tej złożoności rozwiązuje problem, z którym faktycznie boryka się ten konkretny system na obecnym etapie, czy też jest przyjmowany jedynie dlatego, że stanowi modną odpowiedź na problem, którego system jeszcze nie ma.
Wniosek: Zakończenie serii
W ciągu trzech artykułów stopniowo rozwijano jeden system, przy czym każda warstwa opierała się na poprzedniej. Pierwszy rozdział stworzył pojedynczego agenta na tyle godnego zaufania, by móc mu powierzyć rzeczywistą decyzję, poprzez ścisłe oddzielenie interpretacji od samego procesu podejmowania decyzji. Drugi rozdział dodał temu agencie towarzystwo, łącząc kilku specjalistycznych agentów w grafie, który umożliwiał wymianę informacji pomiędzy nimi, chroniony za pomocą wyraźnych uprawnień oraz umożliwiający śledzenie procesu od początku do końca, dzięki czemu każde żądanie można było śledzić od chwili jego wpłynięcia do momentu wyjścia. Ten artykuł zamknął pozostałą lukę – jak te agenci mogą działać poza grafem – poprzez standaryzację dostępu za pomocą MCP w sytuacjach, gdy jest to rzeczywiście opłacalne, jednocześnie jasno wskazując, gdzie to nie ma sensu.
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.