Le client
Traduction automatique
Cette page a été traduite automatiquement à partir de la documentation en anglais, et la page en anglais fait foi. Si quelque chose vous semble incorrect, la page Traductions explique comment le signaler.
Un Client est le moyen par lequel un programme Python dialogue avec un serveur MCP.
C’est un seul objet avec un seul cycle de vie : vous le construisez, vous entrez dans async with, vous appelez des méthodes. Chaque verbe du protocole (lister les outils, en appeler un, lire une ressource, rendre un prompt) est une méthode async de cet objet qui renvoie un résultat typé.
Votre premier client
Un client a besoin d’un serveur avec qui dialoguer. Ce serveur Bookshop est celui auquel se connecte chaque extrait de cette page. Enregistrez-le sous le nom server.py et laissez-le tourner en HTTP :
from pydantic import BaseModel
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
from mcp.types import Completion, CompletionArgument, CompletionContext, PromptReference, ResourceTemplateReference
mcp = MCPServer("Bookshop", instructions="Search the catalog before recommending a book.")
GENRES = ["fiction", "non-fiction", "poetry"]
class Book(BaseModel):
title: str
author: str
year: int
@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})."
@mcp.tool()
def lookup_book(title: str) -> Book:
"""Look up a book by its exact title."""
if title != "Dune":
raise ToolError(f"No book titled {title!r} in the catalog.")
return Book(title="Dune", author="Frank Herbert", year=1965)
@mcp.resource("catalog://genres")
def genres() -> list[str]:
"""The genres the catalog is organised by."""
return GENRES
@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}."
@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."
@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)])
uv run mcp run server.py --transport streamable-http
Cela le sert à l’adresse http://localhost:8000/mcp. Le client est un programme à part. Enregistrez-le sous le nom client.py et lancez python client.py dans un second terminal :
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
print(client.server_info)
print(client.server_capabilities)
print(client.protocol_version)
print(client.instructions)
if __name__ == "__main__":
anyio.run(main)
Client("http://localhost:8000/mcp")reçoit une URL, il se connecte donc en Streamable HTTP au serveur que vous venez de démarrer.async withest le cycle de vie. Y entrer connecte et négocie ; en sortir déconnecte. Il n’y a pas de paireconnect()/close(), et unClientne peut pas être réutilisé une fois le bloc terminé.- À l’intérieur du bloc, les informations de connexion sont déjà là, sous forme de simples propriétés.
Ce que vous pouvez passer à Client
Client prend un seul argument positionnel et déduit le transport de son type :
- Une chaîne d’URL (
Client("http://localhost:8000/mcp")) : Streamable HTTP, le transport derrière lequel vous déployez. - Un
StdioServerParameters: la commande à lancer comme sous-processus local, avec laquelle le client dialogue via son stdin et son stdout. - Un transport : tout ce sur quoi vous pouvez faire
async with ... as (read, write), commestreamable_http_client(url, http_client=...)autour de votre propre client HTTP. - Une instance de
MCPServer(ou duServerbas niveau) : connexion dans le processus, sans sous-processus ni port. Celle-ci sert aux tests, et Tests s’appuie dessus.
Tout le reste de cette page est identique pour les quatre. Les en-têtes, les sous-processus, les délais d’expiration et le protocole Transport ont leur propre page : Transports côté client.
Ce que porte un client connecté
Quatre propriétés en lecture seule, renseignées dès que vous entrez dans le bloc :
client.server_info: l’identité du serveur, ouNonepour un serveur de génération 2026 qui n’en déclare pas (les serveurs python-sdk le font par défaut). Ici,server_info.namevaut"Bookshop"etserver_info.versionest ce que le serveur déclare.client.server_capabilities: ce que le serveur sait faire (tools,resources,prompts,completions, ...). Une capacité que le serveur n’a pas vautNone.client.protocol_version: la version du protocole sur laquelle les deux côtés se sont mis d’accord. Ici, c’est"2026-07-28".client.instructions: la chaîneinstructions=du serveur, ouNones’il n’en a pas défini.
Vous n’avez jamais choisi de version du protocole. Par défaut, le Client sonde le serveur et se rabat sur la poignée de main (handshake) classique avec les plus anciens, si bien qu’un seul client fonctionne avec un serveur de n’importe quelle génération. Lorsque vous avez besoin de contrôler cela, tous les détails sont dans Versions du protocole.
Tip
client.session est la ClientSession sous-jacente, l’échappatoire bas niveau.
Vous n’en aurez besoin pour rien sur cette page.
Lister les outils
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_tools() renvoie un ListToolsResult ; les outils sont dans .tools. Chacun est la définition complète qu’un hôte transmettrait à un modèle. Voici le premier :
tool.name # 'search_books'
tool.title # 'Search the catalog'
tool.description # 'Search the catalog by title or author.'
et tool.input_schema est le JSON Schema que le serveur a dérivé des annotations de type de la fonction :
{
"type": "object",
"properties": {
"query": {"title": "Query", "type": "string"},
"limit": {"default": 10, "title": "Limit", "type": "integer"}
},
"required": ["query"],
"title": "search_booksArguments"
}
Ce schéma est tout ce dont une interface a besoin pour afficher un formulaire d’arguments, et tout ce dont un modèle a besoin pour produire des arguments valides.
Le second outil, lookup_book, a été enregistré sans title=, donc son tool.title vaut None.
Tip
title est facultatif, donc une interface qui présente des outils à un humain doit choisir : le title s’il existe,
le name sinon. from mcp.shared.metadata_utils import get_display_name fait exactement cela,
pour les outils, les ressources, les modèles de ressource et les prompts.
Appeler un outil
call_tool(name, arguments) exécute l’outil et vous renvoie un CallToolResult.
import anyio
from mcp import Client
from mcp.types import TextContent
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
Le lookup_book du serveur renvoie un Book Pydantic. Voici ce que voit le client :
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
Une valeur de retour, trois choses à lire. Chacune a un consommateur différent.
content : ce que lit le modèle
content est une list de blocs de contenu, et un bloc de contenu est une union : TextContent, ImageContent, AudioContent, ResourceLink ou EmbeddedResource. Un outil peut en renvoyer plusieurs, de natures différentes.
C’est pourquoi main restreint le type avec isinstance(block, TextContent) avant de toucher à block.text. Remarquez qu’il n’y a pas de .text en dehors du isinstance : le vérificateur de types ne le permettrait pas, car ImageContent a .data, pas .text. L’union est honnête sur ce qu’un outil a le droit de vous envoyer ; votre code devrait l’être aussi.
structured_content : ce que lit votre application
structured_content est la valeur de retour de l’outil au format JSON, conforme au output_schema déclaré par l’outil. Pas d’analyse de chaînes, pas de devinettes.
Quand les deux sont présents, ils disent volontairement deux fois la même chose : content est pour un modèle, structured_content pour du code. D’où vient la moitié structurée, et comment la contrôler, c’est le sujet de la page Sortie structurée.
is_error : si l’outil a échoué
Un outil qui lève une exception ne lève rien dans votre client. Il revient sous la forme d’un résultat ordinaire avec is_error=True.
Check
Demandez "Solaris" à lookup_book (un titre qui n’est pas au catalogue) et la fonction lève
ToolError. L’appel revient pourtant normalement :
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
Le message de la ToolError a atterri dans content, où le modèle peut le lire et réessayer. C’est
délibéré : une erreur d’outil fait partie de la conversation, ce n’est pas un plantage. (Si l’outil avait planté avec
une autre exception, content dirait seulement Error executing tool lookup_book.) Regardez toujours
is_error avant de faire confiance à structured_content.
Warning
is_error=True couvre plus que vos propres raise. Demandez un outil que le serveur n’a même pas
(call_tool("does_not_exist", {})) et rien n’est levé. Vous obtenez la même forme en retour :
is_error=True avec Unknown tool: does_not_exist dans content. Une méthode de Client ne lève
MCPError que lorsque le serveur répond par une erreur JSON-RPC au lieu d’un résultat, et
Gérer les erreurs explique quand un serveur produit l’une ou l’autre.
Ressources
Les verbes des ressources vont par paires : deux façons de lister, une façon de lire.
import anyio
from mcp import Client
from mcp.types import TextResourceContents
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_resources()renvoie les ressources concrètes, celles qui ont un URI fixe. Ici :['catalog://genres'].list_resource_templates()renvoie les ressources paramétrées. Ici :['catalog://genres/{genre}']. Ce sont deux listes distinctes parce qu’un modèle n’est pas lisible tant que vous ne l’avez pas rempli.read_resource(uri)prend un URI sous forme de simplestret fonctionne sur les deux : passez"catalog://genres/poetry"et le serveur le fait correspondre au modèle.
read_resource renvoie contents, une liste de TextResourceContents ou de BlobResourceContents. Même idée que pour le contenu des outils : restreignez le type avec isinstance, puis lisez .text (ou .blob).
Un client peut aussi être prévenu quand une ressource change. Sur les connexions de génération 2025, c’est subscribe_resource(uri) / unsubscribe_resource(uri) — une paire de méthodes que MCPServer n’implémente pas, si bien que sur la liaison en version 2026-07-28 (où ces verbes n’existent plus) la requête reçoit en réponse -32601, Method not found. Le remplaçant en version 2026 est un flux subscriptions/listen, que MCPServer sert bel et bien — server_capabilities.resources.subscribe y vaut True — et sa consommation avec client.listen(...) fait l’objet de la page Abonnements de cette section.
Prompts
import anyio
from mcp import Client
async def main() -> None:
async with Client("http://localhost:8000/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)
if __name__ == "__main__":
anyio.run(main)
list_prompts() vous dit ce que le serveur propose et ce dont chaque prompt a besoin :
prompt.name # 'recommend'
prompt.title # 'Recommend a book'
prompt.arguments # [PromptArgument(name='genre', required=True)]
get_prompt(name, arguments) le rend. Le dictionnaire d’arguments est str -> str : les arguments de prompt sont toujours des chaînes. Le résultat est messages, une liste de PromptMessage, chacun avec un role et un bloc content :
message.role # 'user'
message.content # TextContent(type='text', text='Recommend one poetry book from the catalog and say why.')
Un hôte transmet ces messages tels quels au modèle. C’est toute la fonctionnalité.
Complétions
Un serveur doté d’un gestionnaire (handler) de complétion peut compléter automatiquement les arguments des prompts et des modèles de ressource au fil de la saisie de l’utilisateur.
import anyio
from mcp import Client
from mcp.types import PromptReference
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
result = await client.complete(
ref=PromptReference(type="ref/prompt", name="recommend"),
argument={"name": "genre", "value": "p"},
)
print(result.completion.values)
if __name__ == "__main__":
anyio.run(main)
refindique quel prompt ou modèle vous remplissez : unPromptReferenceou unResourceTemplateReference.argumentvaut{"name": ..., "value": ...}: l’argument et ce que l’utilisateur a saisi jusqu’ici.
La réponse se trouve dans result.completion.values. Tapez "p" et le serveur revient avec ['poetry']. Le côté serveur, et la façon dont un gestionnaire utilise les autres arguments déjà remplis pour affiner ses suggestions, c’est la page Complétions.
Pagination
Chaque méthode list_* accepte un argument nommé cursor= et chaque résultat porte un next_cursor. Quand next_cursor vaut None, vous avez tout.
import anyio
from mcp import Client
from mcp.types import Tool
async def list_all_tools(client: Client) -> list[Tool]:
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:
return tools
cursor = page.next_cursor
async def main() -> None:
async with Client("http://localhost:8000/mcp") as client:
tools = await list_all_tools(client)
print([tool.name for tool in tools])
if __name__ == "__main__":
anyio.run(main)
La fonction list_all_tools est correcte face à n’importe quel serveur. MCPServer renvoie tout en une seule page, donc next_cursor vaut None et la boucle s’exécute une fois, ce qui explique que la plupart du code ne l’écrive jamais. Les serveurs qui paginent réellement, et les règles auxquelles obéissent les curseurs, sont dans Pagination.
Dans les tests
Chaque client.py de cette page a atteint server.py en HTTP. Dans un test, vous vous passez du réseau et donnez à Client l’objet serveur lui-même : from server import mcp, puis Client(mcp). Pas de processus, pas de port, et chaque méthode ci-dessus fonctionne de la même façon.
Il existe un drapeau du constructeur conçu pour cela : Client(mcp, raise_exceptions=True). Il n’a d’effet que sur les connexions dans le processus, et Tests est la page qui l’explique et construit tout le modèle autour de lui.
Récapitulatif
Client(x)se connecte en Streamable HTTP à une chaîne d’URL, lance un sous-processus pour unStdioServerParameters, entre directement dans un transport et, dans les tests, prend l’objet serveur lui-même.async withest tout le cycle de vie. À l’intérieur,server_capabilitiesetprotocol_versionsont déjà renseignés ;server_infoetinstructionsle sont aussi lorsque le serveur les fournit.list_tools()vous donne lename, letitle, ladescriptionet leinput_schemade chaque outil.call_tool()renvoiecontentpour le modèle,structured_contentpour votre code, etis_error. Un outil qui lève une exception est un résultat, pas une exception.contentest une union de types de blocs ; restreignez le type avecisinstanceavant de lire.list_resources/list_resource_templates/read_resource,list_prompts/get_promptetcompletecomplètent la liste des verbes.- Chaque
list_*acceptecursor=; bouclez jusqu’à ce quenext_cursorvailleNone.
Ce qu’un serveur peut demander au client, et la façon d’y répondre, c’est Fonctions de rappel du client.