MCP od zera, część 3: Połączenie klienta z twoim serwerem
Stwórz klienta MCP stdio, który uruchamia hr_server.py, wywołuje funkcję search_employee i zwraca wyniki — bez konieczności ręcznego uruchamiania serwera.
W poprzedniej części pozostawiono serwer MCP z jedną funkcją: umożliwienie wyszukiwania w dziedzinie HR.
HR MCP Server
↓
search_employee
Proces ten rozpoczął się od:
python hr_server.py
Pozostało jedno pytanie: kto faktycznie komunikuje się z serwerem? Ta rola przypada klientowi.
Client
↓
MCP
↓
HR MCP Server
Rozpatrujmy te pojęcia w prosty sposób. Serwer oferuje możliwości, a klient się do niego łączy i je wykorzystuje.
Stwórzmy naszego klienta
Oprócz pliku hr_server.py dodajmy kolejny moduł:
hr_client.py
Struktura projektu wygląda teraz tak:
mcp-hr
│
├── hr_server.py
│
└── hr_client.py
Ponieważ serwer został już napisany, uwaga skupia się na klientie.
Połączmy się z naszym serwerem
Wplątajmy poniższy kod do pliku hr_client.py:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server = StdioServerParameters(
command="python",
args=["hr_server.py"]
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
asyncio.run(main())
Pierwszy rzut oka może sprawiać wrażenie, że treść jest gęsta. Decydującym fragmentem jest:
server = StdioServerParameters(
command="python",
args=["hr_server.py"]
)
Ty parametry informują klienta o uruchomieniu i połączeniu się z hr_server.py. Klient uruchamia proces serwera i otwiera sesję stdio z nim.
A teraz wezwijmy nasze narzędzie
Część 2 opisała narzędzie o nazwie:
search_employee
Klient może wyszukiwać Johna w ten sposób:
await session.initialize()
result = await session.call_tool(
"search_employee",
{"name": "John"}
)
print(result.content[0].text)
Mówiąc prościej, klient prosi sesję o uruchomienie funkcji search_employee z imieniem John. Pełny skrypt klienta jest następujący:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
server = StdioServerParameters(
command="python3",
args=["hr_server.py"]
)
async with stdio_client(server) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(
"search_employee",
{"name": "John"}
)
print(result.content[0].text)
asyncio.run(main())
To plik stanowi kompletną stronę klienta w tym demo.
Załóżmy, że to uruchomimy
Z terminala należy wykonać:
python hr_client.py
Ciekawy szczegół: hr_server.py nie wymaga osobistego uruchomienia ręcznie. Klient uruchamia je poprzez:
command="python",
args=["hr_server.py"]
a następnie łączy się z biegnącym procesem.
Co się dzieje?
Klient wysyła żądanie narzędzia w takiej formie:
Tool:
search_employee
Name:
John
Serwer otrzymuje to i wykonuje:
search_employee("John")
Rozwiązuje to w następujący sposób:
John → Finance
i zwraca ten wynik do wywołującego. W całości ścieżka wygląda tak:
hr_client.py
│
│ search_employee("John")
↓
hr_server.py
│
↓
Search employee list
│
↓
John → Finance
│
↓
hr_client.py
Oba strony protokołu przeprowadzają teraz rzeczywistą podróż w obie strony.
Z powrotem do naszego przykładu z USB
Połączenie klawiatury z komputerem stanowi przydatne porównanie.
Keyboard
↓
USB
↓
Computer
Oba końce zgadzają się co do wspólnego protokołu kablowego. Tutaj klient używa MCP, a serwer również MCP. To wspólne umówienie umożliwia im współpracę.
Ale czy wybrał on odpowiedni narzędzie?
Jedna z ograniczeń łatwo umknąć uwadze. Rozważmy ponownie miejsce wywołania:
session.call_tool(
"search_employee",
{"name": "John"}
)
Kto wybrał search_employee? To autor aplikacji, poprzez umieszczenie nazwy narzędzia w kodzie źródłowym. Klient nigdy nie czytał pytania w języku naturalnym, takiego jak „Czy John pracuje w dziale finansów?”, i nie decydował na podstawie tego, które funkcje uruchomić. Kolejnym brakującym elementem jest automatyczny wybór narzędzia.
Co dalej?
Załóżmy, że serwer posiada kilka dodatkowych narzędzi:
search_employee
create_employee
get_leave_balance
list_departments
Gdy użytkownik pyta, do którego działu należy John, coś musi określić, że właściwą funkcją jest:
search_employee
a nie jakieś inne narzędzie z tej samej grupy. Ta decyzja zależy od nazw narzędzi, ich opisów oraz dostępnego katalogu. W następnej części omówimy, w jaki sposób aplikacja wybiera spośród narzędzi MCP, gdy jest ich wiele dostępnych.
Aż do tego momentu istotna lekcja płynąca z tego kroku jest czysto mechaniczna: klient stdio może uruchomić serwer, zainicjować sesję, wywołać określone narzędzie z argumentami oraz wydrukować strukturyzowaną odpowiedź — bez konieczności ręcznego uruchamiania oddzielnego procesu serwera.