docs/client/index.md
A Client is how a Python program talks to an MCP server.
It is one object with one lifecycle: construct it, enter async with, call methods. Every protocol verb (list the tools, call one, read a resource, render a prompt) is an async method on it that returns a typed result.
--8<-- "docs_src/client/tutorial001.py"
The server at the top is only there so you have something to connect to. The client is the five highlighted lines.
Client(mcp) is given the server object itself. That is the in-memory transport: no subprocess, no port, no HTTP. It is how every example in this chapter, and every test you write, connects.async with is the lifecycle. Entering it connects and negotiates; leaving it disconnects. There is no connect() / close() pair, and a Client cannot be reused after the block ends.ClientClient takes one positional argument and resolves the transport from its type:
MCPServer (or low-level Server) instance: connected in-process.Client("http://localhost:8000/mcp")): Streamable HTTP, the production path.async with ... as (read, write), such as stdio_client(...) wrapping a subprocess.Everything else on this page is identical across all three. Headers, subprocesses, timeouts, and the Transport protocol get their own chapter: Client transports.
Four read-only properties, populated the moment you enter the block:
client.server_info: the server's identity. server_info.name here is "Bookshop", server_info.version is whatever the server reports.client.server_capabilities: what the server can do (tools, resources, prompts, completions, ...). A capability the server doesn't have is None.client.protocol_version: the protocol version the two sides agreed on. Here it is "2026-07-28".client.instructions: the server's instructions= string, or None if it didn't set one.You never picked a protocol version. By default the Client probes the server and falls back to the classic handshake on older ones, so one client works against any era of server. When you need to control that, Protocol versions has the whole story.
!!! tip
client.session is the underlying ClientSession, the low-level escape hatch.
You won't need it for anything on this page.
--8<-- "docs_src/client/tutorial002.py"
list_tools() returns a ListToolsResult; the tools are in .tools. Each one is the complete definition a host would hand to a model:
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
and tool.input_schema is the JSON Schema the server derived from the function's type hints:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
That schema is everything a UI needs to render an argument form, and everything a model needs to produce valid arguments.
!!! tip
title is optional, so a UI showing tools to a human has to pick: the title if there is one,
the name if not. from mcp.shared.metadata_utils import get_display_name does exactly that,
for tools, resources, resource templates and prompts.
call_tool(name, arguments) runs the tool and gives you back a CallToolResult.
--8<-- "docs_src/client/tutorial003.py"
The server's lookup_book returns a Pydantic Book. Here is what the client sees:
result.content # [TextContent(type='text', text='{\n "title": "Dune",\n "author": "Frank Herbert",\n "year": 1965\n}')]
result.structured_content # {'title': 'Dune', 'author': 'Frank Herbert', 'year': 1965}
result.is_error # False
One return value, three things to read. Each has a different consumer.
content: what the model readscontent is a list of content blocks, and a content block is a union: TextContent, ImageContent, AudioContent, ResourceLink, or EmbeddedResource. A tool can return several, of different kinds.
That is why main narrows with isinstance(block, TextContent) before touching block.text. Notice there is no .text outside the isinstance: the type checker won't allow it, because ImageContent has .data, not .text. The union is honest about what a tool is allowed to send you; your code should be too.
structured_content: what your application readsstructured_content is the tool's return value as JSON, matching the tool's declared output_schema. No string parsing, no guessing.
When both are present they say the same thing twice on purpose: content is for a model, structured_content is for code. Where the structured half comes from, and how to control it, is the Structured Output chapter.
is_error: whether the tool failedA tool that raises does not raise in your client. It comes back as an ordinary result with is_error=True.
!!! check
Ask lookup_book for "Solaris" (a title that isn't in the catalog) and the function raises
ValueError. The call still returns normally:
```python
result.is_error # True
result.content # [TextContent(type='text', text="Error executing tool lookup_book: No book titled 'Solaris' in the catalog.")]
result.structured_content # None
```
The exception's message landed in `content`, where the **model** can read it and try again. That
is deliberate: a tool error is part of the conversation, not a crash. Always look at `is_error`
before you trust `structured_content`.
!!! warning
is_error=True covers more than your own raise. Ask for a tool the server doesn't even have
(call_tool("does_not_exist", {})) and nothing raises. You get the same shape back,
is_error=True with Unknown tool: does_not_exist in content. A Client method raises
MCPError only when the server answers with a JSON-RPC error instead of a result, and
Handling errors covers when a server produces which.
The resource verbs come in pairs: two ways to list, one way to read.
--8<-- "docs_src/client/tutorial004.py"
list_resources() returns the concrete resources, the ones with a fixed URI. Here: ['catalog://genres'].list_resource_templates() returns the parameterised ones. Here: ['catalog://genres/{genre}']. They are two different lists because a template isn't readable until you fill it in.read_resource(uri) takes a plain str URI and works on both: pass "catalog://genres/poetry" and the server matches it to the template.read_resource returns contents, a list of TextResourceContents or BlobResourceContents. Same idea as tool content: narrow with isinstance, then read .text (or .blob).
A client can also be told when a resource changes. On 2025-era connections that is subscribe_resource(uri) / unsubscribe_resource(uri) - a method pair MCPServer doesn't implement, so on the 2026-07-28 wire (where those verbs no longer exist) the request answers -32601, Method not found. The 2026 replacement is a subscriptions/listen stream, which MCPServer does serve - server_capabilities.resources.subscribe is True there, and the server side of the story is Subscriptions.
--8<-- "docs_src/client/tutorial005.py"
list_prompts() tells you what the server offers and what each prompt needs:
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
get_prompt(name, arguments) renders it. The arguments dict is str -> str: prompt arguments are always strings. The result is messages, a list of PromptMessage, each with a role and a content block:
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
A host hands those messages straight to the model. That is the whole feature.
A server with a completion handler can autocomplete prompt and resource-template arguments as the user types.
--8<-- "docs_src/client/tutorial006.py"
ref says which prompt or template you're filling in: a PromptReference or a ResourceTemplateReference.argument is {"name": ..., "value": ...}: the argument and what the user has typed so far.The answer is in result.completion.values. Type "p" and the server comes back with ['poetry']. The server side, and how a handler uses the other already-filled arguments to narrow its suggestions, is the Completions chapter.
Every list_* method takes a cursor= keyword and every result carries a next_cursor. When next_cursor is None, you have everything.
--8<-- "docs_src/client/tutorial007.py"
This loop is correct against every server. MCPServer returns everything in one page, so next_cursor is None and the loop runs once, which is why most code never writes it. Servers that genuinely page, and the rules cursors obey, are in Pagination.
Client(mcp) with no process and no port is already a test harness for your server.
There is one constructor flag built for that: Client(mcp, raise_exceptions=True). It only has an effect on in-memory connections, and Testing is the chapter that explains it and builds the whole pattern around it.
Client(x) connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.async with is the whole lifecycle. Inside it, server_info, server_capabilities, protocol_version and instructions are already populated.list_tools() gives you each tool's name, title, description and input_schema.call_tool() returns content for the model, structured_content for your code, and is_error. A raising tool is a result, not an exception.content is a union of block types; narrow with isinstance before reading.list_resources / list_resource_templates / read_resource, list_prompts / get_prompt, and complete round out the verbs.list_* takes cursor=; loop until next_cursor is None.Next: the things a server can ask the client for, and how you answer, in Client callbacks.