MCP client
Client is the part of an application that speaks MCP to one server, and it can connect to a server object in memory as this course does.
Last updated: 29 Sep, 2026 · MCP 2.2
The last lesson listed a tool. Now call it. The same Client that lists tools also runs them, over the real protocol, whether the server is in memory, a subprocess or across a network.
The Client API
from mcp import Client
async with Client(mcp) as client: # a server object means in memory
await client.list_tools() # what the server offers
await client.call_tool(name, args) # run one toolConnecting in memory
Client(mcp) is given the server object, so it connects in memory: no process to start, no port. async with connects when the block starts and disconnects when it ends. Every client method is async, so it is awaited inside main, which asyncio.run starts.
async with Client(mcp) as client:
result = await client.call_tool("lookup_order", {"order_id": "A17"})View the code here
from mcp.server import MCPServer
mcp = MCPServer("Shop support")
ORDERS = {
"A17": {"item": "blue mug", "status": "shipped", "total": 12.50},
"B42": {"item": "desk lamp", "status": "processing", "total": 48.00},
}
@mcp.tool()
def lookup_order(order_id: str) -> str:
"""Look up an order by its id and say where it is."""
order = ORDERS[order_id]
return f"Order {order_id}: {order['item']}, {order['status']}."
Calling a tool end to end
The whole call in one file. The result comes back in three parts.
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
result = await client.call_tool("lookup_order", {"order_id": "A17"})
print(result.content[0].text)
print(result.structured_content)
print(result.is_error)
asyncio.run(main())Order A17: blue mug, shipped.
{'result': 'Order A17: blue mug, shipped.'}
FalseThe three parts of a result
contentis a list of blocks for the model to read; here, one text block.structured_contentis the same result as data for the application's code; a string return is wrapped as{"result": ...}.is_errorsays whether the call failed, and Tool errors covers when it isTrue.
Reading what the connection knows
When a client connects, the two sides agree on a protocol version, and the server declares its capabilities: the kinds of request it will answer.
import asyncio
from mcp import Client
from shop import mcp
async def main():
async with Client(mcp) as client:
print(client.server_info.name)
print(client.protocol_version)
print(client.server_capabilities.tools)
print(client.server_capabilities.completions)
asyncio.run(main())Shop support 2026-07-28 list_changed=True None
MCPServer always declares tools; Resources and Prompts arrive later in the course. Completions, argument autocomplete, needs a handler this server does not have, so it is None, and a client will not ask for it.
In memory vs a subprocess vs HTTP
| Connect with | What happens | Where it is covered |
|---|---|---|
Client(mcp) | In memory, no process, no port | this course throughout |
StdioServerParameters | The server runs as a subprocess | the stdio transport lesson |
| a URL | An HTTP connection to a remote server | the Streamable HTTP lesson |
When to use the in-memory client
- Trying a server while you build it, with no process to launch.
- Testing a server, where the in-memory client becomes a pytest fixture in part 6.
is_error before you read structured_content. A failed call still returns a result object, and reaching into its data without checking the flag hides the failure from your code.Related
- Previous: MCPServer quickstart
- Next: MCP Inspector with mcp dev
- Reference: MCP Python SDK client
- Call
lookup_orderforB42. - Print
resultitself, not its parts, to see every field. - Call a tool that does not exist,
"cancel_order", and printis_errorand the text.
This is what real progress feels like.