Объект Client
Машинный перевод
Эта страница переведена с английской документации автоматически, и основной версией остаётся английская страница. Если что-то читается неправильно, на странице Переводы объясняется, как об этом сообщить.
Через Client программа на Python общается с MCP-сервером.
Это один объект с одним жизненным циклом: создать его, войти в async with, вызывать методы. Каждая операция протокола (получить список инструментов, вызвать один из них, прочитать ресурс, отрендерить промпт) — это async-метод этого объекта, возвращающий типизированный результат.
Первый клиент
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")
@mcp.tool()
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
async def main() -> None:
async with Client(mcp) as client:
print(client.server_info)
print(client.server_capabilities)
print(client.protocol_version)
print(client.instructions)
Сервер в начале файла нужен лишь для того, чтобы было к чему подключаться. Клиент — это пять выделенных строк.
Client(mcp)получает сам объект сервера. Это транспорт в памяти: ни подпроцесса, ни порта, ни HTTP. Именно так подключается каждый пример на этой странице и каждый тест, который вы напишете.async with— это жизненный цикл. Вход в блок подключает и согласовывает возможности; выход — отключает. Парыconnect()/close()нет, иClientнельзя использовать повторно после завершения блока.- Внутри блока сведения о подключении уже доступны как обычные свойства.
Что можно передать в Client
Client принимает один позиционный аргумент и определяет транспорт по его типу:
- Экземпляр
MCPServer(или низкоуровневогоServer): подключение внутри процесса. - Строка с URL (
Client("http://localhost:8000/mcp")): Streamable HTTP, основной вариант для реального развёртывания. - Транспорт: всё, что можно использовать как
async with ... as (read, write), напримерstdio_client(...), оборачивающий подпроцесс.
Всё остальное на этой странице одинаково для всех трёх вариантов. Заголовкам, подпроцессам, тайм-аутам и протоколу Transport посвящена отдельная страница: Транспорты клиента.
Что есть у подключённого клиента
Четыре свойства только для чтения, заполняемые в момент входа в блок:
client.server_info: сведения о сервере илиNoneдля сервера поколения 2026, который их не сообщает (серверы на python-sdk по умолчанию сообщают). Здесьserver_info.name—"Bookshop", аserver_info.version— то, что сообщает сервер.client.server_capabilities: что умеет сервер (tools,resources,prompts,completions, ...). Возможность, которой у сервера нет, равнаNone.client.protocol_version: версия протокола, о которой договорились стороны. Здесь это"2026-07-28".client.instructions: строкаinstructions=сервера илиNone, если сервер её не задал.
Версию протокола вы нигде не выбирали. По умолчанию Client зондирует сервер и на старых серверах переходит к классическому рукопожатию, так что один клиент работает с сервером любого поколения. Если этим нужно управлять, подробнее — на странице Версии протокола.
Tip
client.session — это лежащий в основе ClientSession, низкоуровневый запасной выход.
Ни для чего на этой странице он не понадобится.
Получение списка инструментов
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.tool(title="Search the catalog")
def search_books(query: str, limit: int = 10) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r} (showing up to {limit})."
async def main() -> None:
async with Client(mcp) as client:
result = await client.list_tools()
for tool in result.tools:
print(tool.name)
print(tool.title)
print(tool.description)
print(tool.input_schema)
list_tools() возвращает ListToolsResult; инструменты лежат в .tools. Каждый из них — полное определение, которое хост передал бы модели:
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
а tool.input_schema — это JSON Schema, которую сервер вывел из аннотаций типов функции:
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
Этой схемы достаточно и интерфейсу, чтобы отрисовать форму аргументов, и модели, чтобы сформировать корректные аргументы.
Tip
title необязателен, поэтому интерфейсу, показывающему инструменты человеку, приходится выбирать: title, если он есть,
иначе name. from mcp.shared.metadata_utils import get_display_name делает именно это —
для инструментов, ресурсов, шаблонов ресурсов и промптов.
Вызов инструмента
call_tool(name, arguments) запускает инструмент и возвращает CallToolResult.
from pydantic import BaseModel
from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextContent
mcp = MCPServer("Bookshop")
class Book(BaseModel):
title: str
author: str
year: int
@mcp.tool()
def lookup_book(title: str) -> Book:
"""Look up a book by its exact title."""
if title != "Dune":
raise ValueError(f"No book titled {title!r} in the catalog.")
return Book(title="Dune", author="Frank Herbert", year=1965)
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool("lookup_book", {"title": "Dune"})
for block in result.content:
if isinstance(block, TextContent):
print(block.text)
print(result.structured_content)
print(result.is_error)
Серверный lookup_book возвращает Pydantic-модель Book. Вот что видит клиент:
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
Одно возвращаемое значение, три поля для чтения. У каждого свой потребитель.
content: что читает модель
content — это list блоков содержимого, а блок содержимого — объединение типов: TextContent, ImageContent, AudioContent, ResourceLink или EmbeddedResource. Инструмент может вернуть несколько блоков, причём разных видов.
Поэтому main сужает тип с помощью isinstance(block, TextContent), прежде чем обращаться к block.text. Обратите внимание: вне isinstance обращения к .text нет — проверка типов этого не допустит, потому что у ImageContent есть .data, а не .text. Объединение честно описывает, что инструмент вправе прислать; код должен быть столь же честен.
structured_content: что читает приложение
structured_content — это возвращаемое значение инструмента в виде JSON, соответствующего объявленной output_schema инструмента. Никакого разбора строк, никаких догадок.
Когда есть и то и другое, они намеренно говорят одно и то же дважды: content — для модели, structured_content — для кода. Откуда берётся структурированная часть и как ею управлять — на странице Структурированный вывод.
is_error: завершился ли инструмент ошибкой
Инструмент, выбросивший исключение, не выбрасывает его в клиенте. Он возвращается обычным результатом с is_error=True.
Check
Запросите у lookup_book "Solaris" (название, которого нет в каталоге), и функция выбросит
ValueError. Вызов всё равно завершится нормально:
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
Сообщение исключения попало в content, где модель может его прочитать и попробовать снова. Так
и задумано: ошибка инструмента — часть диалога, а не крах. Всегда проверяйте is_error,
прежде чем доверять structured_content.
Warning
is_error=True покрывает не только ваш собственный raise. Запросите инструмент, которого у сервера вообще нет
(call_tool("does_not_exist", {})), — и исключения не будет. Вернётся та же структура:
is_error=True с Unknown tool: does_not_exist в content. Метод Client выбрасывает
MCPError только тогда, когда сервер отвечает ошибкой JSON-RPC вместо результата, а
когда сервер выдаёт одно, а когда другое, описано на странице Обработка ошибок.
Ресурсы
Операции с ресурсами идут парами: два способа получить список и один способ прочитать.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import TextResourceContents
mcp = MCPServer("Bookshop")
@mcp.resource("catalog://genres")
def genres() -> list[str]:
"""The genres the catalog is organised by."""
return ["fiction", "non-fiction", "poetry"]
@mcp.resource("catalog://genres/{genre}")
def books_in_genre(genre: str) -> str:
"""Every title we stock in one genre."""
return f"3 books filed under {genre}."
async def main() -> None:
async with Client(mcp) as client:
listed = await client.list_resources()
print([resource.uri for resource in listed.resources])
templates = await client.list_resource_templates()
print([template.uri_template for template in templates.resource_templates])
result = await client.read_resource("catalog://genres/poetry")
for contents in result.contents:
if isinstance(contents, TextResourceContents):
print(contents.text)
list_resources()возвращает конкретные ресурсы — те, у которых фиксированный URI. Здесь:['catalog://genres'].list_resource_templates()возвращает параметризованные. Здесь:['catalog://genres/{genre}']. Это два разных списка, потому что шаблон нельзя прочитать, пока его не заполнить.read_resource(uri)принимает URI обычной строкойstrи работает с обоими: передайте"catalog://genres/poetry", и сервер сопоставит его с шаблоном.
read_resource возвращает contents — список TextResourceContents или BlobResourceContents. Та же идея, что и с содержимым инструмента: сузьте тип через isinstance, затем читайте .text (или .blob).
Клиент может также узнавать об изменениях ресурса. На подключениях поколения 2025 это subscribe_resource(uri) / unsubscribe_resource(uri) — пара методов, которую MCPServer не реализует, поэтому в протоколе 2026-07-28 (где этих операций больше нет) запрос получает в ответ -32601, Method not found. Замена в поколении 2026 — поток subscriptions/listen, который MCPServer как раз обслуживает (server_capabilities.resources.subscribe там равно True), а как читать его через client.listen(...) — на странице Подписки этого раздела.
Промпты
from mcp import Client
from mcp.server import MCPServer
mcp = MCPServer("Bookshop")
@mcp.prompt(title="Recommend a book")
def recommend(genre: str) -> str:
"""Ask for a recommendation in a genre."""
return f"Recommend one {genre} book from the catalog and say why."
async def main() -> None:
async with Client(mcp) as client:
listed = await client.list_prompts()
print(listed.prompts)
result = await client.get_prompt("recommend", {"genre": "poetry"})
for message in result.messages:
print(message.role, message.content)
list_prompts() сообщает, что предлагает сервер и что нужно каждому промпту:
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
get_prompt(name, arguments) рендерит его. Словарь аргументов имеет вид str -> str: аргументы промпта всегда строки. Результат — messages, список PromptMessage, у каждого есть role и блок content:
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
Хост передаёт эти сообщения прямо модели. Вот и вся возможность.
Автодополнение
Сервер с обработчиком автодополнения может дополнять аргументы промптов и шаблонов ресурсов по мере того, как пользователь их вводит.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
mcp = MCPServer("Bookshop")
GENRES = ["fiction", "non-fiction", "poetry"]
@mcp.prompt()
def recommend(genre: str) -> str:
"""Ask for a recommendation in a genre."""
return f"Recommend one {genre} book from the catalog and say why."
@mcp.completion()
async def complete_genre(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> Completion | None:
return Completion(values=[genre for genre in GENRES if genre.startswith(argument.value)])
async def main() -> None:
async with Client(mcp) as client:
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="recommend"),
argument={"name": "genre", "value": "p"},
)
print(result.completion.values)
refуказывает, какой промпт или шаблон заполняется:PromptReferenceилиResourceTemplateReference.argument— это{"name": ..., "value": ...}: аргумент и то, что пользователь уже успел набрать.
Ответ лежит в result.completion.values. Наберите "p" — и сервер вернёт ['poetry']. Серверная сторона и то, как обработчик использует другие, уже заполненные аргументы, чтобы сузить подсказки, — на странице Автодополнение.
Пагинация
Каждый метод list_* принимает именованный аргумент cursor=, а каждый результат содержит next_cursor. Когда next_cursor равен None, у вас есть всё.
from mcp import Client
from mcp.server import MCPServer
from mcp.types import Tool
mcp = MCPServer("Bookshop")
@mcp.tool()
def search_books(query: str) -> str:
"""Search the catalog by title or author."""
return f"Found 3 books matching {query!r}."
@mcp.tool()
def reserve_book(title: str) -> str:
"""Put a book on hold."""
return f"Reserved {title!r}."
async def main() -> None:
async with Client(mcp) as client:
tools: list[Tool] = []
cursor: str | None = None
while True:
page = await client.list_tools(cursor=cursor)
tools.extend(page.tools)
if page.next_cursor is None:
break
cursor = page.next_cursor
print([tool.name for tool in tools])
Этот цикл корректен для любого сервера. MCPServer возвращает всё одной страницей, так что next_cursor равен None и цикл выполняется один раз — поэтому в большинстве программ его и не пишут. О серверах, которые действительно разбивают выдачу на страницы, и о правилах, которым подчиняются курсоры, — на странице Пагинация.
В тестах
Client(mcp) без процесса и без порта — уже готовая тестовая обвязка для сервера.
Для этого есть один специальный флаг конструктора: Client(mcp, raise_exceptions=True). Он действует только на подключениях в памяти, а объясняет его и строит вокруг него весь подход страница Тестирование.
Итоги
Client(x)подключается в памяти к объекту сервера, по Streamable HTTP — к строке с URL, а ко всему остальному — через транспорт.async with— это весь жизненный цикл. Внутри негоserver_capabilitiesиprotocol_versionуже заполнены;server_infoиinstructions— тоже, если сервер их предоставляет.list_tools()даётname,title,descriptionиinput_schemaкаждого инструмента.call_tool()возвращаетcontentдля модели,structured_contentдля кода иis_error. Инструмент, выбросивший исключение, — это результат, а не исключение.content— объединение типов блоков; сужайте тип черезisinstanceперед чтением.list_resources/list_resource_templates/read_resource,list_prompts/get_promptиcompleteзамыкают набор операций.- Каждый
list_*принимаетcursor=; крутите цикл, покаnext_cursorне станетNone.
То, о чём сервер может попросить клиент, и как на это отвечать, — на странице Колбэки клиента.