This article is published in English.
MCP From Scratch Part 3: Connect a Client to Your Server
Build an MCP stdio client that launches hr_server.py, calls search_employee, and returns results—without starting the server by hand.
The previous installment left an MCP server in place with a single responsibility: expose an HR search capability.
HR MCP Server
↓
search_employee
That process started with:
python hr_server.py
One question remained open: who actually talks to the server? That role belongs to the client.
Client
↓
MCP
↓
HR MCP Server
Treat the terms simply. The server offers capabilities. The client connects and uses them.
Let’s Create Our Client
Beside hr_server.py, add another module:
hr_client.py
The project layout becomes:
mcp-hr
│
├── hr_server.py
│
└── hr_client.py
With the server already written, attention stays on the client.
Connect to Our Server
Place the following into 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())
The listing looks dense at first. The decisive fragment is:
server = StdioServerParameters(
command="python",
args=["hr_server.py"]
)
Those parameters tell the client to launch and attach to hr_server.py. The client starts the server process and opens a stdio session against it.
Now Let’s Call Our Tool
Part 2 defined a tool named:
search_employee
The client can search for John like this:
await session.initialize()
result = await session.call_tool(
"search_employee",
{"name": "John"}
)
print(result.content[0].text)
In plain language, the client asks the session to run search_employee with the name John. The full client script is:
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())
That file is the complete client side of the demo.
Let’s Run It
From a terminal, execute:
python hr_client.py
A useful detail: hr_server.py does not need a separate manual start. The client launches it through:
command="python",
args=["hr_server.py"]
and then attaches to the running process.
What Happens?
The client sends a tool request shaped like:
Tool:
search_employee
Name:
John
The server receives it and executes:
search_employee("John")
It resolves:
John → Finance
and returns that result to the caller. End to end, the path looks like:
hr_client.py
│
│ search_employee("John")
↓
hr_server.py
│
↓
Search employee list
│
↓
John → Finance
│
↓
hr_client.py
Both sides of the protocol are now exercising a real round trip.
Back to Our USB Example
Connecting a keyboard to a computer is a useful comparison.
Keyboard
↓
USB
↓
Computer
Both ends agree on a shared cable protocol. Here, the client speaks MCP and the server speaks MCP. That shared contract is what lets them cooperate.
But Did It Choose the Tool?
One limitation is easy to miss. Consider the call site again:
session.call_tool(
"search_employee",
{"name": "John"}
)
Who selected search_employee? The application author did, by hard-coding the tool name. The client never read a natural-language question such as “Does John work in Finance?” and decided which capability to invoke. The next missing piece is automatic tool selection.
What’s Next?
Imagine the server grows several tools:
search_employee
create_employee
get_leave_balance
list_departments
When a user asks which department John belongs to, something must decide that the right capability is:
search_employee
rather than a sibling tool. That decision depends on tool names, descriptions, and the advertised catalog. The following installment covers how an application chooses among MCP tools once many are available.
Until then, the durable lesson from this step is mechanical: a stdio client can start the server, initialize a session, call a named tool with arguments, and print the structured reply—without any separate server process started by hand.