docs: publish translated docs in twelve languages and the tool that maintains them (#3280)

This commit is contained in:
Max 2026-08-14 17:07:34 +01:00 • committed by GitHub
commit 52ad0a8876
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 89737 additions and 54 deletions

307
i18n/fr/glossary.json Normal file
View file

@ -0,0 +1,307 @@
{
"keep": [
"MCP",
"Model Context Protocol",
"MCPServer",
"FastMCP",
"ClientSession",
"Context",
"ctx",
"stdio",
"Streamable HTTP",
"SSE",
"JSON-RPC",
"JSON",
"OAuth",
"PKCE",
"JWT",
"CIMD",
"HTTP",
"HTTPS",
"TLS",
"CORS",
"URI",
"URL",
"ASGI",
"WebSocket",
"API",
"SDK",
"CLI",
"IDE",
"LLM",
"SEP",
"RFC",
"Python",
"TypeScript",
"Node.js",
"PyPI",
"Pydantic",
"Starlette",
"FastAPI",
"uvicorn",
"httpx",
"anyio",
"asyncio",
"trio",
"pytest",
"OpenTelemetry",
"Inspector",
"Claude",
"GitHub",
"VS Code",
"Windows",
"macOS",
"Linux",
"llms.txt",
"2026-07-28",
"2025-11-25",
"2025-06-18",
"2025-03-26"
],
"terms": [
{
"source": "tool",
"target": "outil",
"note": "MCP protocol noun (a server exposes tools), masculine: un outil / les outils, l’appel d’outil. First mention on a page may read \"outil (tool)\". Wire identifiers such as `tools/call` and the `@mcp.tool()` decorator are code and stay untouched; the Inspector's **Tools** tab is a UI label and stays English. Provisional pending native review."
},
{
"source": "resource",
"target": "ressource",
"note": "MCP protocol noun (data a server exposes for reading) and the general noun alike, feminine, French spelling with double s: la ressource / les ressources. `resources/read` and `@mcp.resource()` are code; the Inspector's **Resources** tab is a UI label and stays English."
},
{
"source": "prompt",
"target": "prompt",
"note": "The MCP feature (a reusable prompt template a server exposes) and the general AI sense; kept in English as French AI writing does. Masculine: le prompt / les prompts. Not invite, which is the command-line prompt (invite de commandes) and the wrong sense. `prompts/get` and `@mcp.prompt()` are code. Provisional pending native review."
},
{
"source": "sampling",
"target": "échantillonnage",
"note": "The (deprecated) client feature that lets a server borrow the client's model for a completion. Masculine: l’échantillonnage. First mention on a page reads \"échantillonnage (sampling)\" so the reader can map it to `sampling/createMessage`, which is code. Provisional pending native review; keeping the English word is the open alternative."
},
{
"source": "roots",
"target": "racines",
"note": "The (deprecated) client feature listing the workspace directories a client exposes. Feminine plural: les racines (\"the client's roots\" → les racines du client, i.e. ses répertoires racine). First mention on a page reads \"racines (roots)\" so the reader can map it to `roots/list`, which is code; a `Root` object in code font stays Latin. Provisional pending native review; keeping roots in English is the open alternative."
},
{
"source": "elicitation",
"target": "élicitation",
"note": "The MCP feature where the server asks the user a question through the client mid-request. The French noun exists (élicitation des exigences) and is exact; feminine: l’élicitation. First mention on a page reads \"élicitation (elicitation)\". `elicitation/create`, `ctx.elicit()` and the `Elicit` class are code. Provisional pending native review."
},
{
"source": "capability",
"target": "capacité",
"note": "What client and server declare during initialization (\"capability negotiation\" → la négociation des capacités): la capacité / les capacités. Not fonctionnalité, which is a feature and a different thing, and not aptitude. The `capabilities` field and keys such as `sampling.tools` stay Latin. Provisional pending native review."
},
{
"source": "transport",
"target": "transport",
"note": "The connection layer, masculine: le transport / les transports. The individual transports — stdio, Streamable HTTP, SSE — are names on the keep list and stay in English (le transport stdio, le transport Streamable HTTP)."
},
{
"source": "session",
"target": "session",
"note": "An MCP session (the negotiated connection state), feminine: la session / les sessions, l’identifiant de session. The `session` object, `ClientSession` and `ServerSession` are code and stay untouched."
},
{
"source": "handler",
"target": "gestionnaire",
"note": "The tool, resource or prompt function you register (nav section \"Inside your handler\" → Dans votre gestionnaire): le gestionnaire / les gestionnaires. First mention on a page may read \"gestionnaire (handler)\". Provisional pending native review; keeping le handler is the open alternative, but never alternate the two on one page."
},
{
"source": "dependency",
"target": "dépendance",
"note": "Both package dependencies and the SDK's parameter-injection feature — parameters declared with `Resolve(...)` (\"dependency injection\" → l’injection de dépendances; the \"Dependencies\" page → Dépendances). Feminine: la dépendance. The `Resolve` marker class stays Latin."
},
{
"source": "resolver",
"target": "résolveur",
"note": "The plain function attached to a parameter with `Resolve(...)` that computes or asks for its value before the tool runs: le résolveur / les résolveurs (the word French already uses for a DNS resolver). The `Resolve` class stays Latin. Provisional pending native review."
},
{
"source": "client",
"target": "client",
"note": "An MCP client, and the client side of a connection: le client / les clients. The `Client` class and the `mcp.client` module are code and stay untouched."
},
{
"source": "server",
"target": "serveur",
"note": "An MCP server (the program you build): le serveur / les serveurs, un serveur MCP. The `MCPServer`, `Server` and `ServerSession` classes are code and stay untouched."
},
{
"source": "host",
"target": "hôte",
"note": "The MCP host — the application the user talks to, which embeds the client and drives the model (Claude Desktop, an IDE) — and also a network host: l’hôte (masculine) / les hôtes in both senses. Hébergeur is a hosting provider and a different thing. Provisional pending native review."
},
{
"source": "context",
"target": "contexte",
"note": "The generic lower-case word (\"provide context to LLMs\" → fournir du contexte aux LLM): le contexte. The capitalised `Context` is the SDK object injected as `ctx`; it is on the keep list and stays Latin in prose (\"The Context\" → L’objet Context, la classe `Context`)."
},
{
"source": "request",
"target": "requête",
"note": "A JSON-RPC or HTTP request (\"the initialize request\" → la requête initialize, \"request body\" → le corps de la requête): la requête / les requêtes. Demande stays the everyday word for a request a person makes; never alternate the two for protocol messages. `Request` types in code font stay Latin."
},
{
"source": "response",
"target": "réponse",
"note": "A JSON-RPC or HTTP response, and equally the answer a person, model or tool gives: la réponse / les réponses. `Response` types in code font stay Latin."
},
{
"source": "notification",
"target": "notification",
"note": "A JSON-RPC notification (a message that expects no response) and change notifications alike: la notification / les notifications, \"change notification\" → notification de changement. Method strings such as `notifications/tools/list_changed` are code."
},
{
"source": "callback",
"target": "fonction de rappel",
"note": "Client callbacks such as `sampling_callback` and `elicitation_callback` (the \"Callbacks\" page → Fonctions de rappel): la fonction de rappel / les fonctions de rappel; first mention on a page may read \"fonction de rappel (callback)\". An OAuth redirect callback is l’URL de rappel / le rappel OAuth. Parameter names stay Latin. Provisional pending native review; le callback is the open alternative."
},
{
"source": "decorator",
"target": "décorateur",
"note": "The Python decorators the SDK is built on: le décorateur / les décorateurs. `@mcp.tool()` and its siblings are code and stay untouched."
},
{
"source": "type hint",
"target": "annotation de type",
"note": "Python type hints (\"from your type hints\" → à partir de vos annotations de type): l’annotation de type / les annotations de type — the plural goes on annotation, type stays singular. Translated on every page rather than left as type hints in the prose; `type hints` inside code font is code."
},
{
"source": "round trip",
"target": "aller-retour",
"note": "One request/response exchange: un aller-retour / des allers-retours (\"zero negotiation round trips\" → aucun aller-retour de négociation). Provisional pending native review."
},
{
"source": "multi-round-trip",
"target": "à plusieurs allers-retours",
"note": "The 2026-07-28 request pattern (\"Multi-round-trip requests\" → Requêtes à plusieurs allers-retours). Provisional pending native review: gloss the English on first use per page — requêtes à plusieurs allers-retours (multi-round-trip). The abbreviation MRTR stays Latin."
},
{
"source": "lifespan",
"target": "cycle de vie",
"note": "The server's startup/shutdown scope (the \"Lifespan\" page, as in the ASGI lifespan): le cycle de vie (du serveur). First mention on a page reads \"cycle de vie (lifespan)\" so the reader can connect it to the `lifespan=` parameter, which is code. Durée de vie is the neighbouring \"lifetime\" (\"for the lifetime of the app\" → pendant toute la durée de vie de l’application) and is not banned; espérance de vie is always wrong. Provisional pending native review.",
"avoid": ["espérance de vie"]
},
{
"source": "back-channel",
"target": "canal de retour",
"note": "This documentation's term for the server calling back into the client during a request, which exists only on legacy connections: le canal de retour. First mention on a page reads \"canal de retour (back-channel)\" so the reader can connect it to the `NoBackChannelError` exception, which is code. Provisional pending native review."
},
{
"source": "deprecated",
"target": "obsolète",
"note": "Advisory status: still works, scheduled for removal later — obsolète, as French Python and web documentation render \"deprecated\" (\"Deprecated features\" → Fonctionnalités obsolètes); \"deprecation\" → l’obsolescence (avertissement d’obsolescence, tied to the `MCPDeprecationWarning` class, which stays Latin); \"removed\" is supprimé, a different state. Provisional pending native review; the anglicism déprécié is widespread and is the open alternative, but never alternate the two."
},
{
"source": "legacy",
"target": "historique",
"note": "\"A legacy connection / client / session\" = one negotiated at spec version 2025-11-25 or earlier → une connexion historique, un client historique, une session historique (\"Serving legacy clients\" → Prendre en charge les clients historiques). Keep it distinct from obsolète, which renders \"deprecated\", and from ancien, which before the noun means \"former\". Provisional pending native review; hérité and d’ancienne génération are the open alternatives."
},
{
"source": "era",
"target": "génération",
"note": "\"Protocol era\" (\"a 2025-era client\", \"whatever era the client speaks\", \"any era of server\") → la génération du protocole, un client de génération 2025, un serveur de n’importe quelle génération. Provisional pending native review; not ère or époque, which read grandiose."
},
{
"source": "wire",
"target": "liaison",
"note": "The corpus's light metaphor for the byte stream between client and server: \"on the wire\" → sur la liaison, \"stdout is the wire\" → stdout est la liaison elle-même, \"invisible on the wire\" → invisible sur la liaison, \"the JSON on the wire\" → le JSON qui circule sur la liaison. Not the literal fil or câble. Provisional pending native review."
},
{
"source": "handshake",
"target": "poignée de main",
"note": "The initialization handshake (\"the classic handshake\" → la poignée de main classique), the term French protocol documentation uses for TCP and TLS alike: la poignée de main. First mention on a page may read \"poignée de main (handshake)\". Provisional pending native review; keeping le handshake is the open alternative."
},
{
"source": "escape hatch",
"target": "échappatoire",
"note": "The API-design metaphor for the lower-level mechanism you drop to when the convenience layer is in the way (`client.session`, `add_request_handler()`, the low-level `Server`): une échappatoire / des échappatoires. Pinned so every page uses one rendering; not trappe de secours or porte dérobée (a backdoor). Provisional pending native review."
},
{
"source": "library",
"target": "bibliothèque",
"note": "A software library: la bibliothèque / les bibliothèques. The false friend librairie means a bookshop and is never right here.",
"avoid": ["librairie"]
},
{
"source": "framework",
"target": "framework",
"note": "Kept in English as French developers say it: le framework / les frameworks (\"the framework supplies it\" → le framework le fournit). The purist coinage cadriciel is never used.",
"avoid": ["cadriciel"]
},
{
"source": "middleware",
"target": "middleware",
"note": "Kept in English as French developers say it, and matching `server.middleware`: le middleware / les middlewares (the \"Middleware\" page → Middleware). The purist coinage intergiciel is never used.",
"avoid": ["intergiciel"]
},
{
"source": "token",
"target": "jeton",
"note": "OAuth and bearer tokens, as French security documentation writes them: le jeton / les jetons, jeton d’accès (access token), jeton d’actualisation (refresh token), jeton porteur (bearer token). Provisional pending native review: for LLM tokens (a model's context or output length) le token is what French AI writing uses and is acceptable in that sense only."
},
{
"source": "logging",
"target": "journalisation",
"note": "The activity and the page title (\"Logging\" → Journalisation); \"a log message\" → un message de journal, \"the server's log\" → le journal du serveur, \"to log\" → journaliser or consigner, never logger / loguer. The `logging` module and `ctx.log()` are code. Provisional pending native review; les logs is common in speech and is the open alternative for the noun."
},
{
"source": "subscription",
"target": "abonnement",
"note": "Resource and list-change subscriptions (the two \"Subscriptions\" pages → Abonnements): l’abonnement / les abonnements, \"subscribe\" → s’abonner, \"subscriber\" → l’abonné. `subscriptions/listen` and `resources/subscribe` are code."
},
{
"source": "completion",
"target": "complétion",
"note": "Two senses, one word. The MCP feature that autocompletes prompt and resource-template arguments (the \"Completions\" page, `completion/complete`) → la complétion / les complétions (des arguments). The text a model produces in the sampling pages (\"an LLM completion\") may also read la complétion, or more plainly la réponse du modèle. Provisional pending native review."
},
{
"source": "structured output",
"target": "sortie structurée",
"note": "The tools feature and its page title (\"Structured Output\" → Sortie structurée): la sortie structurée. `structured_output` and `outputSchema` are code."
},
{
"source": "troubleshooting",
"target": "dépannage",
"note": "The page title and the activity: le dépannage. Not résolution des problèmes on some pages and dépannage on others. Provisional pending native review."
},
{
"source": "authorization",
"target": "autorisation",
"note": "The OAuth sense and the page title (\"Authorization\" → Autorisation); \"authentication\" is l’authentification — keep the two apart as the English does. The `Authorization` header is code."
},
{
"source": "return value",
"target": "valeur de retour",
"note": "A function's return value: la valeur de retour; \"returns X\" → renvoie X, never retourne X. The `return` keyword and annotations are code."
},
{
"source": "default value",
"target": "valeur par défaut",
"note": "A parameter's default: la valeur par défaut; \"by default\" → par défaut; \"required\" (of a parameter) → obligatoire, not requis."
},
{
"source": "Get started",
"target": "Prise en main",
"note": "The nav section that opens the guide, and the title of its index page. \"First steps\" is a separate page inside that section (Premiers pas), so the two need distinct renderings or the sidebar shows the same title twice — never Premiers pas for this one. Provisional pending native review; Démarrer is the open alternative."
},
{
"source": "First steps",
"target": "Premiers pas",
"note": "The tutorial page inside the \"Get started\" section; never reuse this rendering for the section itself (see that entry). Provisional pending native review."
},
{
"source": "Recap",
"target": "Récapitulatif",
"note": "Recurring section heading that closes most pages; one rendering everywhere, not En résumé on some pages and Récapitulatif on others. Provisional pending native review."
},
{
"source": "Try it",
"target": "Essayer",
"note": "Recurring section heading above a runnable example; one rendering everywhere (the infinitive, per the heading rule), not Essayez on some pages and À vous de jouer on others. Provisional pending native review."
}
]
}

172
i18n/fr/instructions.md Normal file
View file

@ -0,0 +1,172 @@
# French (fr) — translation instructions
Target language: French as written in France (français, fr-FR conventions),
directory and URL code `fr`, page language tag `fr`. This file is sent verbatim
with every translation request for this language, on top of the shared rules
in `../general-prompt.md`. The termbase in `glossary.json` is sent alongside it
and wins any terminology conflict with this file.
## 1. Register
Address the reader as **vous**, always — verb forms, votre / vos and object
pronouns to match. French developer documentation does not use tu.
- Never tu / toi / ton, never a mix. A page that drifts between vous and tu, or
between direct instructions and an impersonal administrative voice, is wrong
even when each sentence is acceptable on its own.
- Steps are imperatives in the second person plural: "Install the SDK, then
run the server" → Installez le SDK, puis lancez le serveur — not Veuillez
installer … before every step, not the infinitive Installer le SDK in
running prose. Obligations take the present: vous devez, not vous devrez,
unless the English is explicitly about the future.
- Headings, table headers, tab labels and admonition titles are infinitives or
noun phrases, never conjugated imperatives: "Declare a tool" → Déclarer un
outil, "Handling errors" → Gérer les erreurs, "Running your server" →
Exécuter votre serveur, "The Context" → L’objet Context. A question heading
may stay a question (Où placer ce code ?). No full stop after a heading.
- Requirement strength stays exact: must → devez / il faut, should → devriez /
il est recommandé de, may / can → pouvez, must not → ne devez pas.
- The authorial "we" is nous (Nous recommandons). The impersonal on is fine
for a genuinely general statement (on obtient alors un schéma), never as a
substitute for addressing the reader, never mixed with nous for one referent.
- "The user" — the human in front of the host — is l’utilisateur, the generic
form French documentation uses; no typographic inclusive forms
(utilisateur·rice). Where a sentence is really about the reader, say vous.
## 2. Voice
The English source is warm, direct and confident: short sentences, the
occasional one-line payoff. Aim for an experienced French engineer explaining a
library to a colleague — professional, warm, plain-spoken; not stiff, not chatty.
- Keep the payoff sentences short: "That's the whole API." → C’est toute
l’API. — not a formal summary sentence. Split a long English sentence rather
than mirroring its clause chain; never merge, drop or reorder the technical
claims themselves.
- Verbs, not nominal chains: procéder à l’installation de → installer;
effectuer la configuration → configurer. Active voice: "The tool is called by
the model" → Le modèle appelle l’outil.
- No administrative French (il convient de, il est à noter que, dans le cadre
de, afin de pouvoir, ledit, ce dernier as an all-purpose pronoun) and no hype
(puissant, en toute simplicité, révolutionnaire).
- No English-shaped French: supporter for "support" (→ prendre en charge),
retourner une valeur (→ renvoyer), consistant for "consistent" (→ cohérent),
adresser un problème (→ traiter), faire sens (→ avoir du sens), définitivement
for "definitely", and bare en 2026-07-28 (→ en version 2026-07-28).
- Body prose uses cela rather than the spoken ça; ça is tolerable only in a
deliberately conversational payoff line, never in reference material.
- Example — "You don't construct it and you don't configure it. You ask for
it." → Vous ne le construisez pas, vous ne le configurez pas. Vous le
demandez. Not the administrative Il n’est pas nécessaire de procéder à son
instanciation ni à sa configuration ; il suffit d’en effectuer la demande. —
nor the calque Tu ne le construis pas … Tu le demandes, c’est tout !
## 3. Humour and idioms
- The English is friendly and dry rather than jokey; French technical prose
tolerates warmth but less wit than English. Never translate a pun, idiom or
aside literally: say what it means as a short, natural French sentence in
the same register; a French idiom at home in technical prose is welcome
(sous le capot for "under the hood"). An aside with no information may go —
a technical caveat phrased lightly never does.
- Recurring English tags get fixed renderings: "**[X](…)** has the whole story"
/ "The whole story is in **[X](…)**" → Tous les détails sont dans
**[X](…)**; "That's the whole API." / "That's the whole protocol." → C’est
toute l’API. / C’est tout le protocole.; "That's it. It's just Python." →
C’est tout. C’est du Python, tout simplement. (not C’est ça. C’est juste du
Python !); "You get `3` back. ✨" → Vous obtenez `3` en retour. ✨ (not Vous
récupérez 3 en retour ! ✨ — lost code span, added exclamation mark).
- Idioms take the plain meaning, not the picture: "Out of the box the app
answers **only** requests addressed to localhost." → Par défaut,
l’application répond **uniquement** aux requêtes adressées à localhost — not
a calqued sortie de la boîte. "it stops being required" → il cesse d’être
obligatoire, not il arrête d’être requis.
- Exclamation marks are rare in French documentation: keep one only where the
English carries genuine emphasis, with its espace insécable (§4); never add,
never double, never in a heading. Emoji: keep the source's rare, deliberately
placed emoji exactly where they are; never add new ones.
## 4. Typography
- Espace insécable: put a no-break space (the character U+00A0 itself, never
` ` and never an ordinary space) before ; : ! ? and %, and inside
guillemets — after « and before ». So: Où placer ce code ? / le schéma
suit : / « bonjour » / 100 %. Never inside code spans, code blocks, URLs,
link targets or `{#id}` attributes; never after the `!!!` / `???` admonition
markers or inside the `![` of an image; and no space at all before , or .
- Quotation marks are guillemets « … » for quotations, scare quotes and
example utterances; English "…" and “…” in the source prose become « … »,
with “…” for a quote inside a quote. Quotes inside code stay exactly as they
are, and a code span is never wrapped in guillemets.
- Apostrophe: the typographic ’ (U+2019) throughout the prose — l’outil,
jusqu’à, C’est — and the straight ' only inside code. Do not elide onto a
code span: la fonction `add`, le paramètre `a`, not l’`add`.
- Accented capitals are mandatory (À partir de, État, Ça, Échantillonnage);
the ligature is œ (cœur, nœud); ordinals are 1er, 2e, 3e (not 2ème).
- Sentence case everywhere; French has no title case (Gérer les erreurs, not
Gérer Les Erreurs). Language names, weekdays and months are lower-case (en
anglais, en juillet); proper nouns keep their capitals (Python, GitHub).
- Digits stay ASCII. Protocol revision strings such as `2026-07-28`, version
numbers, ports, status and error codes, RFC and SEP numbers are identifiers,
copied byte for byte — never 28/07/2026, never 28 juillet 2026. Prose
quantities take the decimal comma only when nothing but the separator changes
(2,5 secondes), never inside code. Thousands and units take a no-break space
(10 000, 30 s, 100 Mo — byte units are o, ko, Mo, Go in prose, unchanged
inside code or quoted output).
- Dashes: keep the source's em-dash incise with a space on each side (texte —
incise — texte) or recast it with commas or parentheses. Ranges read de 3.10
à 3.14, never a hyphen. The ellipsis is the single character … in prose.
- Abbreviations: e.g. → par exemple, i.e. → c’est-à-dire, etc. → etc., vs →
ou / par rapport à; & in prose → et. No comma before et / ou closing a list.
- Bold and italics land on the words that carry the source's emphasis; a bolded
negation ("**not**" → **pas** / **aucun**) stays bold. English words kept in
French text are set in normal type — no italics, no guillemets.
## 5. Terminology pointer
The termbase is `glossary.json` next to this file. It is injected into the
prompt separately and its renderings override anything written here. This
section only fixes the conventions the glossary assumes:
- Terms in the glossary's `keep` list are copied exactly — same spelling and
casing, not translated, italicised or quoted — and invariable in French (les
SDK, les API, no plural s). They take an article by gender: le SDK, l’API
(f.), le JSON, l’URL (f.), l’URI (m.), la CLI, le LLM, la SEP, la RFC.
- Everything in code font — class, function, parameter and module names,
protocol method strings (`tools/call`), header names, error text, config keys
— stays byte-identical. Name the kind of thing in front where it helps: la
classe `Context`, le paramètre `lifespan=`. A glossary term used as a
code-font identifier stays English although its prose noun is translated:
"the `sampling` capability" → la capacité `sampling`.
- Text quoted from what the example code prints or displays — an output line, a
log message, an error string, a UI label such as the Inspector's **Tools**
and **Resources** tabs — stays exactly as the code emits it (usually
English), in or out of code font. The guillemets around it are French; the
text inside does not change.
- France, not Québec, and natural French before anglicism: prefer the French
word wherever developers in France use it — outil, requête, réponse,
gestionnaire, dépendance, bibliothèque, dépôt, fichier, flux, en-tête,
jeton, journal, déploiement, e-mail — and keep the English noun where they
do, masculine, plural in -s: le prompt, le framework, le middleware, le
build, le commit, le hook. Never the purist or Québec coinages cadriciel,
intergiciel, courriel, téléverser. Verbs are French: déployer, fusionner
(not merger), récupérer (not fetcher), analyser (not parser), journaliser
(not logger), mettre en cache, déboguer; créer un commit, never commiter.
- First-use gloss: a translated MCP concept the reader may need to map back to
the English specification carries the English in parentheses on its first
occurrence on a page — l’échantillonnage (sampling) — where the note says so.
- One rendering per term per page: the glossary target, every time. Where an
entry's note marks the choice as open or provisional, still use the listed
target consistently — never requête in one paragraph and demande in the next.
## 6. Provisional note
The register, voice and terminology decisions above, and every entry in
`glossary.json`, are provisional pending review by native French-speaking
readers — in particular the translate-versus-keep line for individual nouns
and the typographic apostrophe. To propose a change, edit this file or
`glossary.json` in a pull request, ideally with a short good/bad example;
never edit the generated `pages/` or `notices.md` next to this file. The tool
cannot tell a hand edit from its own output, so one would persist unchecked
and be carried forward into later runs; a correction made here reaches the
pages when they are regenerated with `translate --lang fr --pages …`.

20
i18n/fr/notices.md Normal file
View file

@ -0,0 +1,20 @@
---
translation:
sections: [aff1b3e872b7876a, 4d80558ad052d586, 0bb81f1e62062d26, d5c35dcec50156bc]
tool: 1
---
# Avis de traduction {#translation-notices}
L’une de ces notes apparaît en haut de chaque page d’un site de documentation traduit.
## Traduction automatique {#translated}
Cette page a été traduite automatiquement à partir de la documentation en anglais, et la [page en anglais](ENGLISH_PAGE) fait foi. Si quelque chose vous semble incorrect, la page [Traductions](TRANSLATIONS_PAGE) explique comment le signaler.
## Traduction en retard sur la page en anglais {#outdated}
La page en anglais a changé depuis cette traduction, si bien que certaines parties peuvent être obsolètes. En cas de doute, consultez la [page en anglais](ENGLISH_PAGE) ; la page [Traductions](TRANSLATIONS_PAGE) explique comment fonctionne la documentation traduite.
## Affichée en anglais {#english}
Il n’existe pas de traduction à jour de cette page, vous la lisez donc en anglais. La page [Traductions](TRANSLATIONS_PAGE) explique comment fonctionne la documentation traduite.

View file

@ -0,0 +1,121 @@
---
translation:
sections: [0355618e5f4d5fe4, 1821eaf50f2d0b64, 82e0b28ebd3abf5a, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487]
tool: 1
---
# MCP Apps {#mcp-apps}
Une **MCP App** est un outil doté d’une interface : en plus de ses données, l’outil désigne un document HTML que l’hôte affiche comme surface interactive.
Deux parties, toujours deux parties :
1. **Un outil** qui fait le travail et renvoie des données, comme n’importe quel autre outil.
2. **Une ressource `ui://`** contenant le HTML que l’hôte affiche pour lui.
L’outil porte une référence `_meta.ui.resourceUri` vers la ressource. L’hôte la récupère avec `resources/read`, l’affiche dans une **iframe isolée (sandbox)** et pousse le résultat de l’outil dans cette iframe via `postMessage`. Votre serveur n’envoie ni ne reçoit jamais de messages `ui/*` : ce trafic circule entre l’hôte et l’iframe. Vous servez un outil et un document HTML ; l’hôte se charge de la mise en scène.
Le SDK fournit cela sous la forme de l’extension intégrée `Apps` (`io.modelcontextprotocol/ui`). Si les [extensions](extensions.md) sont nouvelles pour vous, parcourez d’abord cette page. Une minute, puis revenez.
## Une horloge avec un cadran {#a-clock-with-a-face}
```python title="server.py" hl_lines="19 22 30 32"
--8<-- "docs_src/apps/tutorial001.py"
```
Quatre étapes :
* `Apps()` : une seule instance contient vos outils liés à une interface et leurs ressources.
* `@apps.tool(resource_uri="ui://clock/app.html")` : un outil ordinaire, plus le marquage `_meta.ui.resourceUri`. Tout ce que `@mcp.tool()` accepte (name, title, description, …) est transmis tel quel.
* `apps.add_html_resource("ui://clock/app.html", CLOCK_HTML)` : la ressource correspondante, servie en `text/html;profile=mcp-app`. C’est ce type MIME exact qui indique à un hôte « ceci est une app, affichez-la ».
* `MCPServer("clock", extensions=[apps])` : vous activez l’extension. Le serveur annonce désormais `io.modelcontextprotocol/ui` sous `capabilities.extensions`.
Le HTML lui-même écoute le `postMessage` de l’hôte et affiche le résultat. Pour de vraies applications, utilisez dans votre HTML le SDK navigateur officiel [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps). Il vous donne `ontoolresult`, `callServerTool`, `getHostContext` et `onhostcontextchanged` au lieu d’événements de message bruts.
## Dégradation gracieuse {#graceful-degradation}
Tous les clients n’affichent pas les apps. La spécification dit sans détour ce que cela implique pour vous :
> Les outils **DOIVENT** renvoyer un tableau `content` significatif même lorsqu’une interface est disponible.
Le modèle lit `content` ; l’iframe est pour les humains. Un hôte capable d’afficher une interface transmet quand même le résultat textuel au modèle, et un client purement textuel ne reçoit *que* cela. Le schéma canonique est donc : un outil, deux réponses. Regardez à nouveau `get_time` :
```python title="server.py" hl_lines="23-27"
--8<-- "docs_src/apps/tutorial001.py"
```
`client_supports_apps(ctx)` ne vaut `True` que lorsque le client a déclaré l’extension `io.modelcontextprotocol/ui` **et** listé `text/html;profile=mcp-app` dans ses paramètres `mimeTypes`. Le champ est obligatoire, donc un client qui l’omet ne compte pas. C’est exactement ce que déclare `main()` dans le même fichier : la moitié client de la négociation, et la réponse riche revient.
!!! warning
Ne renvoyez jamais un texte de substitution comme `"[Rendered UI]"` pour seul contenu. Si le texte de repli est inutile, l’outil est inutile pour tout client purement textuel et pour le modèle lui-même. Écrivez la phrase.
## Verrouiller l’iframe {#locking-the-iframe-down}
C’est le côté ressource qui porte les métadonnées de sécurité : ce que l’iframe peut charger, les permissions du navigateur qu’elle souhaite, la façon dont elle aimerait être encadrée :
```python title="server.py" hl_lines="9 19-22"
--8<-- "docs_src/apps/tutorial002.py"
```
`csp` et `permissions` sont des **demandes adressées à l’hôte**, pas un comportement du serveur. L’hôte construit à partir d’elles la Content-Security-Policy et la Permissions-Policy de l’iframe, et il peut refuser. Faites de la détection de fonctionnalités dans votre JS plutôt que de supposer l’accord acquis.
`ResourceCsp`, champ par champ (nom Python, clé sur la liaison, ce que l’hôte en fait) :
| Python | Liaison (`_meta.ui.csp`) | Contrôle |
|---|---|---|
| `connect_domains` | `connectDomains` | `connect-src` : où `fetch`/XHR peuvent aller |
| `resource_domains` | `resourceDomains` | `img-src`, `style-src`, … : fichiers statiques |
| `frame_domains` | `frameDomains` | `frame-src` : iframes imbriquées |
| `base_uri_domains` | `baseUriDomains` | `base-uri` : ce vers quoi `<base>` peut pointer |
`ResourcePermissions` : chaque champ demande une permission du navigateur pour l’iframe.
| Python | Liaison (`_meta.ui.permissions`) |
|---|---|
| `camera` | `camera` |
| `microphone` | `microphone` |
| `geolocation` | `geolocation` |
| `clipboard_write` | `clipboardWrite` |
!!! note
La CSP et les permissions vivent sur la **ressource**, jamais sur l’outil. Les métadonnées d’outil de la spécification n’ont pas d’emplacement pour elles, et les hôtes les ignorent à cet endroit. Le SDK rend l’erreur impossible à exprimer : `@apps.tool()` n’a tout simplement pas de paramètre `csp`.
### Visibilité {#visibility}
`visibility=["app"]` sur un outil dit « ceci existe pour l’iframe, pas pour le modèle » :
* `"model"` : le modèle peut l’appeler.
* `"app"` : l’iframe peut l’appeler (via `callServerTool`).
* Omis : les deux, ce qui est la valeur par défaut.
Le filtrage est le travail de **l’hôte**. Votre serveur liste les outils réservés à l’app dans `tools/list` comme les autres ; l’hôte les cache au modèle. Ne filtrez pas côté serveur.
## Les règles que le SDK fait respecter {#the-rules-the-sdk-enforces}
Toutes échouent au démarrage, pas en production :
* Un `resource_uri` ou un URI de ressource qui n’est pas `ui://...` lève une `ValueError` au moment de la décoration ou de l’enregistrement.
* Un outil lié à un URI **sans ressource enregistrée correspondante** lève une `ValueError` lorsque `MCPServer(extensions=[apps])` consomme l’extension. Un outil qui annonce du HTML répondant 404 sur `resources/read` est une erreur de configuration, donc le serveur refuse de se construire.
* `meta={"ui": ...}` sur `@apps.tool()` lève une `ValueError`. Le décorateur est propriétaire de `_meta["ui"]` ; exprimez-le avec `resource_uri=` et `visibility=`. Les autres clés `meta=` se fusionnent sans problème à côté.
Ni le SDK TypeScript ext-apps ni FastMCP ne détectent ces cas aujourd’hui ; nous préférons que vous le découvriez avant qu’un hôte ne le fasse.
## Au-delà du HTML inline {#beyond-inline-html}
`add_html_resource` couvre le cas courant : une chaîne de HTML. Pour tout le reste, HTML sur disque ou contenu généré, construisez la ressource vous-même et transmettez-la :
```python title="server.py" hl_lines="12 18"
--8<-- "docs_src/apps/tutorial003.py"
```
`add_resource` renseigne le type MIME `text/html;profile=mcp-app` quand la ressource n’en définit pas explicitement, et rejette une incohérence explicite : une ressource `ui://` sous tout autre type MIME est une ressource qu’aucun hôte n’affichera.
!!! tip
Vous ciblez un hôte d’avant la disponibilité générale qui lit encore la clé plate obsolète `_meta["ui/resourceUri"]` ? Fusionnez-la vous-même : `@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})`. L’objet `ui` imbriqué est la forme prévue par la spécification ; la clé plate est en voie de disparition.
## Le voir en action {#see-it-run}
Le scénario `apps` dans `examples/stories/`, c’est cette page sous forme de paire exécutable : un serveur avec un outil horloge lié à une interface et un client qui négocie Apps, lit le `_meta.ui.resourceUri` de l’outil, récupère le HTML et appelle l’outil.
```bash
uv run python -m stories.apps.client
```

View file

@ -0,0 +1,172 @@
---
translation:
sections: [05891e7cc1938a13, b3c01a6af28c51ee, 7ffc91f5e38bdfe0, 717d3f235a8333a7, f471a13b2fe5d737, ed6af2df4b656dff]
tool: 1
---
# Extensions {#extensions}
Une **extension** est un ensemble de comportements MCP, activable sur demande, regroupé derrière un seul identifiant.
Côté serveur, elle peut apporter des outils (tools), des ressources et de nouvelles méthodes de requête, et elle peut envelopper `tools/call`. Côté client, elle peut revendiquer des formes de résultat `tools/call` supplémentaires et observer des notifications propres à un éditeur. Chaque côté s’annonce sous son propre `capabilities.extensions`, et rien ne change pour quiconque ne l’a pas demandé. C’est le contrat ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)), et il a une règle d’or : **les extensions sont désactivées par défaut**.
## Utiliser une extension {#using-an-extension}
Passez des instances à la construction :
```python title="server.py"
--8<-- "docs_src/extensions/tutorial001.py"
```
C’est fait. Le serveur annonce désormais `io.modelcontextprotocol/ui` sous `capabilities.extensions` et sert tout ce que l’extension apporte.
`Apps` est l’extension de référence intégrée, et elle a sa propre page : **[MCP Apps](apps.md)**.
!!! note
Les extensions sont figées à la construction. Il n’existe pas de `add_extension` à appeler plus tard : la table des capacités d’un serveur ne devrait pas changer pendant que des clients y sont connectés.
La table des capacités transite par `server/discover`, qui est un chemin **2026-07-28**. Une poignée de main (handshake) `initialize` historique n’a aucun endroit où la placer, donc un client historique ne voit tout simplement pas l’extension. Concevez en conséquence : une extension *enrichit* un serveur, elle ne doit pas être la seule manière de le rendre utilisable.
## Écrire la vôtre {#writing-your-own}
Dérivez `Extension` et ne redéfinissez que ce dont vous avez besoin. Chaque méthode a une valeur par défaut.
### L’identifiant {#the-identifier}
```python
--8<-- "docs_src/extensions/tutorial002.py"
```
L’identifiant est une chaîne `vendor-prefix/name` qui suit la grammaire des clés `_meta` de la spécification : des libellés séparés par des points (chacun commence par une lettre et se termine par une lettre ou un chiffre), une barre oblique, puis le nom. Il est validé **au moment où la classe est définie**, de sorte qu’une faute de frappe n’attend pas le démarrage d’un serveur :
```text
TypeError: Stamps.identifier must be a `vendor-prefix/name` string
(reverse-DNS prefix required), got 'stamps'
```
Utilisez comme préfixe un domaine que vous contrôlez. `io.modelcontextprotocol/*` est réservé aux extensions spécifiées par le projet MCP lui-même.
### Apporter des outils {#contributing-tools}
La plus petite extension utile, c’est un outil et une table de paramètres :
```python title="server.py" hl_lines="17 19-20 22-23 26"
--8<-- "docs_src/extensions/tutorial003.py"
```
* `tools()` renvoie des `ToolBinding`. Le serveur enregistre chacun exactement comme si vous aviez appelé `mcp.add_tool(...)` vous-même : même génération de schéma, même injection de `Context`, tout à l’identique.
* `settings()` est la valeur annoncée sous `capabilities.extensions["com.example/stamps"]`. Renvoyez `{}` (la valeur par défaut) pour annoncer l’extension sans paramètres.
* L’extension ne reçoit jamais le serveur. Elle déclare ses contributions sous forme de données ; `MCPServer` les consomme. Il n’y a pas de `self.server` à modifier.
Et `main()` en est la preuve, un client en mémoire branché directement sur `mcp` :
```python title="server.py" hl_lines="29-34"
--8<-- "docs_src/extensions/tutorial003.py"
```
### Servir vos propres méthodes {#serving-your-own-methods}
Une extension peut enregistrer de **nouvelles méthodes de requête** : ses propres verbes, servis à côté de ceux de la spécification :
```python title="server.py" hl_lines="16-22 31 40-48"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `SearchParams` dérive de `RequestParams`, si bien que l’enveloppe `_meta` de 2026 est analysée de façon uniforme et que votre gestionnaire (handler) reçoit des paramètres validés, jamais un dict brut. Bornez ce que le client contrôle : `Field(ge=1, le=100)` rejette un `limit` absurde avant que votre code n’alloue quoi que ce soit pour lui.
* `require_client_extension(ctx, EXTENSION_ID)` est le garde-fou : un client qui n’a pas déclaré l’extension reçoit l’erreur `-32021` (capacité client obligatoire manquante), avec la charge utile `requiredCapabilities` lisible par machine que la spécification demande.
* `protocol_versions=frozenset({"2026-07-28"})` épingle la méthode à une seule version de la liaison. Dans toute autre version, le client reçoit `METHOD_NOT_FOUND`, exactement comme si la méthode n’y existait pas. Pour ce client, elle n’existe pas.
Les méthodes sont **strictement additives**. Le SDK le fait respecter à la construction, pas à l’exécution :
* Un `MethodBinding` pour une méthode définie par la spécification (`tools/list`, `completion/complete`…) lève une `ValueError` lors de la construction du binding. Les verbes de base appartiennent au serveur.
* Deux extensions qui lient la même méthode lèvent une exception quand la seconde s’enregistre. Laisser la dernière écriture l’emporter, c’est ainsi que des plugins se corrompent mutuellement ; nous ne faisons pas cela.
* Un ensemble `protocol_versions` vide lève aussi une exception : une méthode qui ne peut jamais être servie est un bogue, pas une configuration.
### Le côté client {#the-client-side}
Le `main()` du même fichier raconte toute l’histoire côté client, ses deux moitiés :
```python title="server.py" hl_lines="54-58"
--8<-- "docs_src/extensions/tutorial004.py"
```
* `Client(..., extensions=[advertise(EXTENSION_ID)])` déclare l’extension. Les déclarations deviennent `ClientCapabilities.extensions` : sur une connexion 2026-07-28, la table voyage dans l’enveloppe `_meta` de chaque requête, donc le serveur la voit sur **chaque** requête ; sur une connexion historique, elle transite par la poignée de main `initialize`. Le code serveur ne s’en soucie pas : `require_client_extension(ctx, ...)` et `ctx.session.check_client_capability(...)` lisent la bonne source dans les deux cas.
* Les méthodes propres à un éditeur descendent d’un niveau, vers `client.session.send_request(...)` ; `Client` n’acquiert de méthodes de premier rang que pour les verbes de la spécification. `send_request` accepte n’importe quelle sous-classe de `Request`, donc la requête de l’éditeur passe telle quelle.
### Intercepter `tools/call` {#intercepting-toolscall}
Le seul hook d’interception. Redéfinissez `intercept_tool_call` pour observer, court-circuiter ou opposer un veto à un appel d’outil :
```python title="server.py" hl_lines="17-24"
--8<-- "docs_src/extensions/tutorial005.py"
```
* `params` est le `CallToolRequestParams` validé : vous obtenez `params.name` et `params.arguments` sans toucher au JSON brut. C’est aussi lui qui décide quel appel d’outil s’exécute : passer un contexte réécrit à `call_next` change ce que le gestionnaire observe sur `ctx`, pas l’invocation de l’outil. La réécriture de requêtes au niveau de la liaison relève du [Middleware](middleware.md).
* `call_next(ctx)` exécute le reste de la chaîne et renvoie le résultat du gestionnaire. Renvoyez-le tel quel (observer), renvoyez autre chose (remplacer) ou levez une `MCPError` (refuser). Ce que vous renvoyez est sérialisé comme n’importe quel résultat de gestionnaire, y compris l’estampille d’identité `serverInfo` de la génération 2026, si bien qu’un intercepteur qui court-circuite ne produit jamais de réponse anonyme ou hors schéma.
* Avec plusieurs extensions, les intercepteurs s’imbriquent dans l’ordre d’enregistrement : la première extension de `extensions=[...]` est la plus externe.
* L’implémentation par défaut laisse passer sans rien faire, et un serveur dont les extensions ne redéfinissent jamais ce hook conserve le gestionnaire `tools/call` nu, intact. Vous ne payez pas pour ce que vous n’utilisez pas.
Le hook enveloppe `tools/call` et rien d’autre. Pour ce qui concerne chaque message, utilisez le [Middleware](middleware.md). Il est fait pour cela.
## Utiliser une extension client {#using-a-client-extension}
Une **extension client**, c’est le même contrat vu du côté consommateur : un ensemble de comportements côté client derrière un seul identifiant. Passez des instances à `Client(extensions=[...])` et appelez les outils normalement :
```python title="client.py" hl_lines="66-68"
--8<-- "docs_src/extensions/tutorial006.py"
```
`call_tool("buy", ...)` renvoie un simple `CallToolResult`, comme tout autre appel. Ce que l’extension a changé : le serveur peut désormais répondre à `buy` par une **forme de résultat** `receipt` au lieu d’un résultat final, et `Receipts` la termine (ici en échangeant le reçu via un appel de suivi) avant que `call_tool` ne renvoie. Rien ne bouge au point d’appel.
Retirez l’extension et rien de tout cela n’existe : le garde-fou du serveur refuse un client qui ne l’a pas déclarée (erreur -32021), et une forme revendiquée venant d’un serveur qui saute le garde-fou échoue à la validation, exactement comme la spécification l’exige pour un `resultType` non reconnu. Désactivé par défaut, aux deux bouts de la liaison.
Pour annoncer un identifiant **sans aucun** comportement côté client (le serveur filtre sur la capacité, le client ne fait rien, comme dans le client de recherche ci-dessus), utilisez `advertise()` :
```python
from mcp.client import advertise
client = Client(mcp, extensions=[advertise("com.example/search")])
```
## Écrire une extension client {#writing-a-client-extension}
Dérivez `ClientExtension` et ne redéfinissez que ce dont vous avez besoin. Trois types de contributions, chacun avec une valeur par défaut : `settings()`, `claims()` et `notifications()`.
```python title="client.py" hl_lines="17-18 43-44 46-47"
--8<-- "docs_src/extensions/tutorial006.py"
```
* L’identifiant suit la même grammaire que celui du serveur, validé au moment où la classe est définie.
* `claims()` renvoie des `ResultClaim` : une étiquette de liaison, le modèle qui l’analyse et le résolveur qui la termine. Le modèle doit épingler l’étiquette avec `result_type: Literal["receipt"]` et ne doit pas dériver des types de résultat de base du verbe ; les deux sont vérifiés à la construction du claim. Les champs d’éditeur comme `receipt_token` voyagent tels quels sur la liaison : une forme substituée parvient au client à l’identique.
* Le résolveur reçoit le modèle analysé et un `ClaimContext` ; `ctx.session` est le même point d’accès public que `client.session`, donc les appels de suivi sont des appels de session ordinaires. Il renvoie le `CallToolResult` normal du verbe.
* `settings()` est la valeur annoncée sous `ClientCapabilities.extensions[identifier]`, lue une fois à la construction de `Client`.
`notifications()` déclare les notifications serveur d’éditeur à observer :
```python
def notifications(self) -> Sequence[NotificationBinding[Any]]:
return [NotificationBinding(method="notifications/receipts", params_type=ReceiptEvent, handler=self.on_receipt)]
```
Le gestionnaire reçoit des paramètres validés un par un, dans l’ordre de distribution. Il observe ; il ne peut ni opposer de veto ni répondre.
Deux règles discrètes. Les claims ne sont actifs que sur les connexions 2026-07-28, et l’annonce des capacités les suit : sur une connexion historique, les claims se dissolvent et l’identifiant disparaît de l’annonce avec eux, si bien que le client n’annonce jamais une extension dont il rejetterait les formes. Et lorsque vous voulez la forme revendiquée elle-même plutôt que le résolveur, appelez `client.session.call_tool(..., allow_claimed=True)` ; sans ce drapeau, une forme revendiquée qui atteint un appelant au niveau session lève `UnexpectedClaimedResult`.
### Verbes d’extension {#extension-verbs}
Les méthodes de requête propres à une extension n’ont besoin d’aucun enregistrement côté client. Un type de requête d’éditeur dérive de `mcp.types.Request` et passe par `client.session.send_request`, comme dans [Servir vos propres méthodes](#serving-your-own-methods). Un ajout : lorsqu’une clé des paramètres doit transiter par l’en-tête `Mcp-Name` (des spécifications d’extension comme tasks l’exigent pour leurs verbes), le type de requête déclare `name_param` :
```python title="client.py" hl_lines="22-25 46-47"
--8<-- "docs_src/extensions/tutorial007.py"
```
La session reflète `params["jobId"]` dans `Mcp-Name` sur chaque chemin d’envoi, et une valeur manquante échoue bruyamment au lieu d’omettre silencieusement un en-tête obligatoire.
## Ce qu’une extension ne peut pas faire {#what-an-extension-cannot-do}
La surface de contribution est **fermée** à dessein. Côté serveur : paramètres, outils, ressources, méthodes, un intercepteur `tools/call`. Côté client : paramètres, claims de résultat, bindings de notification. Une extension ne peut pas :
* **Atteindre l’hôte.** Elle déclare des données ; elle ne détient aucune référence au serveur ni au client.
* **Remplacer le comportement de base.** Les méthodes de la spécification et les étiquettes de résultat de base sont rejetées à la construction (`initialize` est purement et simplement réservé par le runner) ; un binding de notification masqué par le vocabulaire de base se tait avec un avertissement à la place.
* **S’enregistrer tardivement.** Une fois que `MCPServer(...)` ou `Client(...)` a renvoyé, l’ensemble des extensions est ce qu’il est.
Si vous vous battez contre ces murs, vous n’écrivez pas une extension. Vous écrivez un fork. Les murs sont la fonctionnalité : un utilisateur qui lit `extensions=[Apps(), Stamps()]` sait *tout* ce que ces deux-là ont pu toucher.

View file

@ -0,0 +1,34 @@
---
translation:
sections: [ca6988b7503cd2d3]
tool: 1
---
# Avancé {#advanced}
Tout ce dont un serveur ou un client ordinaire a besoin a sa place, par thème, dans les sections ci-dessus.
Cette section regroupe les échappatoires vers lesquelles vous vous tournez quand la couche de commodité
de `MCPServer` vous gêne :
* **[Le Server de bas niveau](low-level-server.md)** : la classe sur laquelle `MCPServer` est construit.
Des schémas écrits à la main, des gestionnaires `on_*`, rien de vérifié à votre place, et vos propres
méthodes JSON-RPC personnalisées.
* **[Pagination](pagination.md)** et **[Middleware](middleware.md)** : deux choses que vous
ne pouvez faire *que* sur le `Server` de bas niveau.
* **[Extensions](extensions.md)** et **[MCP Apps](apps.md)** : la surface d’extension
du protocole. Combinez des paquets d’extension dans un serveur, ou écrivez les vôtres.
Quelques éléments que vous pourriez légitimement chercher ici se trouvent plutôt là où vous
les utilisez réellement :
* **L’autorisation** se trouve sous **[Exécuter votre serveur](../run/index.md)**, parce que vous
protégez un serveur là où vous le déployez.
* **OAuth**, **l’assertion d’identité**, la connexion à **plusieurs serveurs** et le
**cache** de réponses se trouvent tous sous **[Clients](../client/index.md)**.
* **Les requêtes à plusieurs allers-retours** (multi-round-trip) et **les abonnements** se trouvent sous
**[Dans votre gestionnaire](../handlers/index.md)**, parce que ce sont deux choses qu’un
gestionnaire *fait*.
* **Les modèles d’URI** se trouvent sous **[Serveurs](../servers/index.md)**, à côté des ressources.
* **[Versions du protocole](../protocol-versions.md)** et
**[Fonctionnalités obsolètes](../deprecated.md)** ont chacune leur propre page de premier niveau.
Si vous n’êtes pas sûr d’avoir besoin de cette section, c’est que vous n’en avez pas besoin.

View file

@ -0,0 +1,207 @@
---
translation:
sections: [2c79b6338e09b7ac, 7edc43b3fae11314, 1086e77ce561cd7f, a3f71823df5efc31, 9fc7109f72201cae, 7bf25983df655b66, 6330e1f4c6029683, 2f1749c8c133fa1c, b3530fcf4d11fd56, ebc33704fbd74262, cd0e9c933350390e]
tool: 1
---
# Le Server de bas niveau {#the-low-level-server}
`@mcp.tool()` est une couche. En dessous se trouve une seconde classe de serveur, `Server`, qui parle le MCP brut : vous lui donnez les objets du protocole et elle les place sur la liaison, tels quels.
`MCPServer` est construit par-dessus. Vous descendez d’un niveau lorsque la couche de confort vous gêne :
* Vous devez émettre un schéma **exact** (chargé depuis un fichier, généré à partir d’une base de données), et non un schéma dérivé d’une signature Python.
* Vous avez besoin d’un contrôle total sur le résultat : `_meta`, `is_error`, chaque clé de `structured_content`.
* Vous devez traiter une méthode que MCP ne définit pas.
Pour tout le reste, restez sur `MCPServer`.
## Le même outil, à la main {#the-same-tool-by-hand}
Voici l’outil `search_books` que **[Outils](../servers/tools.md)** écrit en neuf lignes de `@mcp.tool()`, sans le sucre syntaxique :
```python title="server.py" hl_lines="22 26 32"
--8<-- "docs_src/lowlevel/tutorial001.py"
```
Trois choses ont changé, et elles constituent toute l’API de bas niveau :
* **Les gestionnaires (handlers) sont des paramètres du constructeur.** `on_list_tools=` et `on_call_tool=` vont dans `Server(...)`. Il n’y a pas de décorateurs à ce niveau, et chaque gestionnaire a la même forme : `async (ctx, params) -> result`.
* **Vous écrivez le schéma d’entrée.** `Tool.input_schema` est un simple `dict` JSON Schema. Personne ne le dérive d’annotations de type, car il n’y a aucune annotation de type dont le dériver.
* **Vous construisez le résultat.** `CallToolResult(content=[TextContent(...)])`, à la main. Rien n’est enveloppé, converti ni déduit d’une annotation de retour.
`params` est la requête analysée : `CallToolRequestParams` vous donne `.name` et `.arguments`. `ctx` est un `ServerRequestContext` : `ctx.session` pour répondre au client, `ctx.lifespan_context`, `ctx.request_id` et `ctx.meta`, le `_meta` entrant de la requête.
!!! info
Si vous avez utilisé FastAPI, vous connaissez déjà cette relation. `MCPServer` est la couche des décorateurs et des annotations de type ; `Server` est le Starlette qui se trouve en dessous. Ils ne sont pas rivaux : `MCPServer` construit un `Server` et y enregistre des gestionnaires exactement comme ceux-ci.
### Essayer {#try-it}
Pas d’Inspector pour celui-ci : `mcp dev` et `mcp run` n’acceptent qu’un `MCPServer`. Le `Client` en mémoire s’en moque ; il accepte un `Server` de bas niveau exactement comme il accepte un `MCPServer` :
```python title="main.py"
import asyncio
from mcp import Client
from server import server
async def main() -> None:
async with Client(server) as client:
result = await client.call_tool("search_books", {"query": "dune", "limit": 5})
print(result.content)
asyncio.run(main())
```
```text
[TextContent(type='text', text="Found 3 books matching 'dune' (showing up to 5).", annotations=None, meta=None)]
```
Le même texte que celui produit par la version `@mcp.tool()`. Deux différences, en toute honnêteté :
* `result.structured_content` vaut `None`. Le serveur de haut niveau enveloppe pour vous un `-> str` dans `{"result": ...}` ; ici, personne ne construit ce que vous n’avez pas construit.
* `list_tools` renvoie le schéma que **vous** avez saisi, caractère pour caractère. La version de haut niveau avait `"title": "Query"` sur chaque propriété et un `"title": "search_booksArguments"` à la racine : des artefacts de Pydantic. À ce niveau, si quelque chose est sur la liaison, c’est vous qui l’y avez mis.
## Rien n’est vérifié pour vous {#nothing-is-checked-for-you}
`MCPServer` rejette un mauvais argument avant même que votre fonction s’exécute, en validant l’appel par rapport au schéma qu’il a généré (**[Outils](../servers/tools.md)**).
`Server` ne fait pas cela. Votre `input_schema` est *annoncé* au client ; il n’est jamais *appliqué* à `params.arguments`.
!!! check
Appelez `search_books` sans `limit` et votre `args["limit"]` lève `KeyError`. Le client voit :
```text
MCPError: Internal server error
```
Une erreur JSON-RPC, code `-32603`, avec un message volontairement générique : le SDK ne divulgue pas votre traceback à un appelant distant. Le modèle ne découvre jamais ce qu’il a mal fait, il ne peut donc pas réessayer. (Dans un test, `raise_exceptions=True` fait remonter la véritable exception à la place ; voir **[Tests](../get-started/testing.md)**.)
Cela se généralise. Une exception levée depuis un gestionnaire de bas niveau est **toujours** une erreur de protocole, jamais un résultat d’outil avec `is_error=True`. Si vous voulez que le modèle lise l’échec et se rattrape, validez vous-même `params.arguments` et renvoyez `CallToolResult(content=[TextContent(...)], is_error=True)`. Les deux types d’échec sont le sujet de **[Gérer les erreurs](../servers/handling-errors.md)**.
## Deux outils, un gestionnaire {#two-tools-one-handler}
`on_call_tool` est l’unique point d’entrée pour tous les outils du serveur. Vous aiguillez selon `params.name` :
```python title="server.py" hl_lines="38-43"
--8<-- "docs_src/lowlevel/tutorial002.py"
```
* `list_tools` annonce les deux. `call_tool` répartit selon le nom.
* La branche `else` compte : `Server` transmettra sans hésiter à votre gestionnaire un `tools/call` pour un nom que vous n’avez jamais listé. Lever une exception à cet endroit transforme l’appel en le même `-32603` que ci-dessus.
## Sortie structurée, à la main {#structured-output-by-hand}
Déclarez `output_schema` sur le `Tool` et placez `structured_content` sur le résultat. Les deux vous appartiennent :
```python title="server.py" hl_lines="19-23 36"
--8<-- "docs_src/lowlevel/tutorial003.py"
```
Appelez-le et le résultat porte les deux représentations :
```json
{
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": false,
"resultType": "complete",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
}
```
Le bloc `_meta` est la marque d’identité du serveur : le SDK l’ajoute à chaque résultat de génération 2026, avec la `version` issue du constructeur (un serveur qui n’en définit aucune renvoie une chaîne vide). Un serveur qui ne doit pas s’identifier peut retirer la clé avec un middleware, lequel est maître des résultats qu’il renvoie.
Le serveur ne compare jamais les deux champs. Le `Client` de ce SDK, si : renvoyez un `structured_content` qui ne satisfait pas le `output_schema` que vous avez déclaré et `call_tool` lève une `RuntimeError` qui commence par `Invalid structured content returned by tool search_books` puis cite l’échec de `jsonschema`. Promettre un schéma ne coûte rien ; le tenir vous incombe. Toute l’échelle des types de retour et des schémas est dans **[Sortie structurée](../servers/structured-output.md)**.
## `_meta` : pour l’application, pas pour le modèle {#\_meta-for-the-application-not-the-model}
`content` est la partie de la réponse que lit le modèle. `structured_content` est la même réponse sous forme de données typées. `_meta` est le troisième canal : des données qui voyagent avec le résultat à destination de l’**application cliente**, sans faire partie de la réponse du tout.
Utilisez-le pour des identifiants d’enregistrement, des identifiants de trace, tout ce dont votre interface a besoin mais pas votre prompt :
```python title="server.py" hl_lines="37"
--8<-- "docs_src/lowlevel/tutorial004.py"
```
* Vous le construisez sous le nom `_meta=`, le nom sur la liaison. Le client le relit sous la forme `result.meta`.
* Préfixez vos clés d’un espace de noms (`bookshop/record_ids`). Les clés `io.modelcontextprotocol/*` sont réservées par le protocole.
!!! warning
`_meta` est une convention entre vous et l’application cliente, pas une garantie sur ce qui parvient
au modèle. L’hôte décide de ce qu’il affiche. Ne mettez jamais de secret dans quelque partie que ce soit d’un résultat d’outil.
## Les capacités suivent vos gestionnaires {#capabilities-follow-your-handlers}
Un `Server` annonce exactement les familles de méthodes pour lesquelles vous lui avez fourni des gestionnaires. Le `Bookshop` ci-dessus passe `on_list_tools` et `on_call_tool` et rien d’autre, donc un client qui s’y connecte voit :
```json
{"tools": {"listChanged": false}}
```
Pas de `resources`, pas de `prompts` : rien ne les soutient. Passez `on_list_prompts` et `prompts` apparaît ; passez `on_completion` et `completions` apparaît.
`MCPServer` annonce toujours les outils, les ressources et les prompts, que vous en ayez enregistré ou non, car ses managers existent toujours. À ce niveau, la déclaration *est* l’appel au constructeur.
## Le type générique du cycle de vie {#the-lifespan-generic}
`Server` est générique sur le type que produit son cycle de vie (lifespan). Annotez-le une fois et l’objet est typé partout où il apparaît :
```python title="server.py" hl_lines="24-26 44-45 50"
--8<-- "docs_src/lowlevel/tutorial005.py"
```
* Le cycle de vie est un `Callable[[Server[Catalog]], AbstractAsyncContextManager[Catalog]]` ; `@asynccontextmanager` sur un générateur `async` vous donne exactement cela.
* Ce qu’il produit via `yield` devient `ctx.lifespan_context`, et comme les gestionnaires sont annotés `ServerRequestContext[Catalog]`, `.search(...)` bénéficie de l’autocomplétion et de la vérification de types.
* On y entre une fois au démarrage du serveur et on en sort une fois à son arrêt. Le démarrage, l’arrêt et la version `MCPServer` de la même idée sont dans **[Cycle de vie](../handlers/lifespan.md)**.
Sans `lifespan=`, `ctx.lifespan_context` est un `dict` vide.
## Une méthode à vous {#a-method-of-your-own}
Le constructeur couvre les méthodes que MCP définit. `add_request_handler` couvre tout le reste :
```python title="server.py" hl_lines="35-36 39-40 43-44 48"
--8<-- "docs_src/lowlevel/tutorial006.py"
```
* Le premier argument est la chaîne de la méthode. Les notifications ont un jumeau, `add_notification_handler`.
* `params_type` est le modèle par rapport auquel les `params` entrants sont validés **avant** l’exécution de votre gestionnaire ; les méthodes personnalisées *ont* donc droit à la validation dont les outils sont privés. Dérivez de `RequestParams` pour que le champ `_meta` s’analyse comme celui de toute autre méthode.
* Le gestionnaire renvoie un `BaseModel`, un `dict` ou `None`. Le SDK le sérialise dans le résultat JSON-RPC.
Une réserve, en toute honnêteté : le `Client` de haut niveau n’a de verbes que pour les méthodes que MCP définit, il n’y a donc pas de `client.reindex()`. Une méthode propriétaire s’adresse à un pair qui sait déjà qu’elle existe : un client que vous livrez aussi, ou un autre de vos services parlant JSON-RPC.
Une méthode que vous ne pouvez pas vous approprier :
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
La poignée de main (handshake) appartient à l’exécuteur (runner). Vous êtes libre de remplacer `server/discover`, `ping` et toutes les autres méthodes intégrées.
!!! tip
`Server.middleware`, mentionné dans cette erreur, enveloppe **chaque** message entrant, `initialize` compris. Si ce que vous voulez est observer ou réécrire le trafic plutôt que répondre à une nouvelle méthode, commencez par **[Middleware](middleware.md)**.
## Les autres gestionnaires {#the-other-handlers}
Chacun d’eux correspond à une idée pour laquelle vous avez désormais le vocabulaire ; chacun a sa propre page.
* `on_call_tool`, `on_get_prompt` et `on_read_resource` peuvent renvoyer un `InputRequiredResult` au lieu de leur résultat normal pour mettre l’appel en pause et demander une saisie au client ; voir **[Requêtes à plusieurs allers-retours (multi-round-trip)](../handlers/multi-round-trip.md)**. Fidèle à ce niveau, rien n’est installé pour vous : là où `MCPServer` scelle `requestState` par défaut, ici le `request_state` que vous définissez traverse la liaison exactement tel qu’écrit, jusqu’à ce que vous optiez pour `server.middleware.append(RequestStateBoundary(RequestStateSecurity(keys=[...]), default_audience=server.name))` : une seule ligne (les deux noms s’importent depuis `mcp.server.request_state`) pour un scellement et une vérification identiques à ceux qu’effectue `MCPServer` (**[Protéger `requestState`](../handlers/multi-round-trip.md#protecting-requeststate)**).
* `on_list_resources`, `on_read_resource`, `on_list_prompts`, `on_get_prompt`, `on_completion` ont la même forme `(ctx, params) -> result` pour les autres primitives.
* `on_subscriptions_listen` sert le flux `subscriptions/listen` de la version 2026-07-28. Passez un `ListenHandler` construit sur un `SubscriptionBus` et publiez des événements sur le bus depuis vos autres gestionnaires ; voir **[Abonnements](../handlers/subscriptions.md)** pour la composition complète.
* `server.streamable_http_app()` renvoie la même application Starlette que celle de `MCPServer` ; déployez-la comme **[Exécuter votre serveur](../run/index.md)** déploie n’importe quelle autre application ASGI. Il n’y a pas de `server.run(transport=...)` à ce niveau : `server.run(read_stream, write_stream, server.create_initialization_options())` pilote une connexion sur une paire de flux, et cette seule ligne dit tout.
## Récapitulatif {#recap}
* Le `Server` de bas niveau reçoit ses gestionnaires sous forme de **paramètres de constructeur** `on_*` ; chaque gestionnaire est `async (ctx, params) -> result`.
* Vous écrivez le dict `input_schema` et vous construisez le `CallToolResult`. Rien n’est dérivé, enveloppé ni validé pour vous.
* Une exception dans un gestionnaire est une erreur de protocole `-32603`. Une erreur d’outil que le modèle peut lire est un `CallToolResult` avec `is_error=True` que **vous** renvoyez.
* Le `_meta` du résultat s’adresse à l’application cliente, pas au modèle.
* `Server[T]` est générique sur ce que produit son cycle de vie ; `ctx.lifespan_context` est un `T` typé.
* `add_request_handler(method, params_type, handler)` sert n’importe quelle méthode. `initialize` est réservée.
* Les capacités qu’annonce un `Server` découlent des gestionnaires que vous avez enregistrés.
`Client(server)` a traité les deux serveurs de façon identique parce qu’ils *sont* le même protocole, et c’est tout l’intérêt. La couche suivante vers le bas n’est pas une classe du tout : c’est le **[Middleware](middleware.md)**.

View file

@ -0,0 +1,132 @@
---
translation:
sections: [6048b4f308edbb8c, 068bda0f21ee9c1b, c3e565b61acd75c5, c62422b159c6ed09, 47204fab253cc45c]
tool: 1
---
# Middleware {#middleware}
Un **middleware** est une fonction asynchrone qui enveloppe chaque message que votre serveur reçoit.
Vous l’écrivez sous la forme `async (ctx, call_next)` et vous l’ajoutez à `server.middleware`. C’est toute l’API.
!!! warning
La liste de middlewares est marquée **provisoire** dans le code source : sa signature et sa
sémantique peuvent changer dans une version mineure 2.x. Utilisez-la pour *observer*
(chronométrage, journalisation, traçage) et pour *refuser* des messages ; n’en faites pas la
fondation sur laquelle repose votre serveur.
`MCPServer` reçoit la liste à la construction (`MCPServer(name, middleware=[...])`) et l’expose sous
`mcp.middleware` ; le `Server` bas niveau expose la même liste sous `server.middleware`. L’exemple
ci-dessous utilise le `Server` bas niveau ; si `Server(name, on_call_tool=...)` est nouveau pour
vous, lisez d’abord **[Le Server bas niveau](low-level-server.md)**.
## Un middleware de chronométrage {#a-timing-middleware}
Un serveur, un outil, un middleware qui journalise le temps pris par chaque message :
```python title="server.py" hl_lines="39-45 49"
--8<-- "docs_src/middleware/tutorial001.py"
```
* `ctx` est le même `ServerRequestContext` que celui que reçoivent vos gestionnaires (handlers).
`ctx.method` est la chaîne de méthode brute ; `ctx.params` contient les paramètres bruts,
**avant** toute validation.
* `call_next(ctx)` exécute le reste de la chaîne : la validation, la recherche du gestionnaire,
votre gestionnaire. Renvoyez ce qu’il a renvoyé et la réponse reste intacte.
* Le `try`/`finally` est délibéré : un gestionnaire qui lève une exception est tout de même
chronométré, car l’échec atteint votre middleware sous la forme de l’exception qui sort de
`call_next`.
* `server.middleware.append(...)` l’enregistre. La liste s’exécute de l’extérieur vers
l’intérieur, donc `middleware[0]` est celui qui est le plus proche de la liaison.
### Essayer {#try-it}
Connectez un client, listez les outils, appelez-en un. Votre journal contient **trois** lignes :
```text
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
```
Vous avez fait deux appels et obtenu trois lignes. La première est `server/discover` : la requête
que le client a envoyée pour établir la connexion, avant que vous ne demandiez quoi que ce soit.
C’est tout l’intérêt. Le middleware enveloppe **chaque** message entrant :
* La mise en place de la connexion : `server/discover`, ou `initialize` et
`notifications/initialized` sur une session historique.
* Chaque requête et chaque notification. Pour une notification, `ctx.request_id is None`,
`call_next(ctx)` renvoie `None`, et tout ce que vous renvoyez est ignoré.
* Même une méthode pour laquelle le serveur n’a pas de gestionnaire : `call_next` lève
`MCPError(-32601, "Method not found")` *à travers* votre middleware en route vers le client.
## Ce que vous pouvez y faire {#what-you-can-do-inside-one}
Du geste le plus anodin à celui devant lequel vous devriez le plus hésiter :
* **Observer.** Chronométrer, compter, journaliser. C’est l’exemple ci-dessus.
* **Refuser.** Levez une `MCPError` *au lieu* d’appeler `call_next(ctx)` et ce message-là
reçoit pour réponse une erreur JSON-RPC. La connexion reste ouverte ; le message suivant passe.
C’est ainsi qu’un serveur contrôle l’accès à `subscriptions/listen` appelant par appelant : la
section **[Décider qui peut observer](../handlers/subscriptions.md#deciding-who-may-watch)** de
la page Abonnements détaille la démarche.
* **Réécrire.** `ctx` est une dataclass : `await call_next(dataclasses.replace(ctx, params=...))`
transmet au reste de la chaîne d’autres paramètres que ceux envoyés par le client. Ne faites
jamais cela pour `initialize` : le résultat que le client reçoit en retour est construit à
partir de vos paramètres réécrits, mais le serveur fixe l’état de sa connexion à partir des
paramètres d’origine reçus sur la liaison. Les deux côtés peuvent terminer la poignée de main
(handshake) en désaccord sur ce qu’ils ont négocié.
* **Répondre.** Renvoyez un résultat sans appeler `call_next(ctx)` et il part au client comme
votre réponse. `call_next` vous remet la forme finale telle qu’elle circule sur la liaison, et
le pipeline ne retouche jamais ce que vous renvoyez ; toute l’enveloppe est donc à votre
charge : sur une connexion de génération 2026, cela inclut l’estampille `_meta` `serverInfo`,
que le SDK ajoute aux résultats des gestionnaires mais pas aux vôtres.
!!! check
`initialize` fait partie de ce que le middleware enveloppe, et c’est le *seul* hook dont
vous disposez pour lui. Essayez d’en prendre le contrôle avec `add_request_handler` et le
SDK refuse :
```text
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
```
!!! warning
`initialize` est traité en ligne : le serveur ne lit aucun autre message entrant tant que
votre chaîne de middlewares n’est pas revenue. Attendre une requête du serveur vers le client
(`ctx.session.send_request(...)`, une élicitation (elicitation)) pendant le traitement de
`initialize` **provoque donc l’interblocage de la connexion** : la réponse que vous attendez
ne pourra jamais être lue. Les notifications envoyées sans attente de réponse ne posent pas
de problème.
## Le seul middleware activé par défaut {#the-one-middleware-that-ships-on-by-default}
Le SDK fournit exactement un middleware, et il figure déjà dans la liste de votre serveur : celui
qui émet un span OpenTelemetry pour chaque message. Vous ne l’ajoutez pas et, la plupart du temps,
vous n’y pensez pas. Il ne fait rien tant que vous n’installez pas d’exporteur, et il a sa propre
page : **[OpenTelemetry](../run/opentelemetry.md)**.
!!! info
Si vous avez déjà écrit un middleware ASGI, vous connaissez cette forme. Le
`(scope, receive, send)` de Starlette est devenu `(ctx, call_next)`, et il s’exécute *après*
le transport, sur le message décodé plutôt que sur la requête HTTP brute. Les deux se
composent : un middleware Starlette sur `streamable_http_app()` voit du HTTP ; celui-ci voit
du MCP.
## Récapitulatif {#recap}
* Un middleware est `async (ctx, call_next) -> result`, passé via `MCPServer(middleware=[...])`
(ou ajouté à `mcp.middleware`), et ajouté à `server.middleware` sur le `Server` bas niveau.
* Il enveloppe **chaque** message entrant (`server/discover`, `initialize`, requêtes,
notifications, méthodes inconnues) et s’exécute de l’extérieur vers l’intérieur.
* `ctx.request_id is None` est ce qui distingue une notification d’une requête.
* Levez une exception au lieu d’appeler `call_next` pour refuser un message ; la connexion
survit.
* Le traçage OpenTelemetry du SDK est lui aussi un middleware, déjà dans la liste. Voir
**[OpenTelemetry](../run/opentelemetry.md)**.
* Toute cette surface est provisoire. Servez-vous-en pour observer ; ne construisez pas dessus.
C’est tout ce qui enveloppe une requête. Quant à savoir si la requête a seulement le droit de
s’exécuter, c’est l’**[Autorisation](../run/authorization.md)** qui en décide.

View file

@ -0,0 +1,85 @@
---
translation:
sections: [a9aba7a026c7bd85, ed32bda7ba9ae33a, 7e64cc5646abb91f, 22a0129ee78b3c63, d875373c06d8d2f9]
tool: 1
---
# Pagination {#pagination}
La plupart des serveurs n’en ont jamais besoin.
`MCPServer` répond à chaque requête `list_*` avec tout ce qu’il a, en une seule page, `next_cursor=None`. Pour quelques dizaines d’outils, de ressources ou de prompts, c’est la bonne réponse et il n’y a rien à configurer.
La pagination sert au serveur dont la liste de ressources est en réalité une base de données : des milliers de lignes qu’il refuse de sérialiser en une seule réponse. La réponse du protocole est un **curseur** : le serveur renvoie une page accompagnée d’un jeton opaque, et le client renvoie ce jeton pour obtenir la page suivante.
`@mcp.resource()` n’offre aucun point d’accroche pour cela. Pour paginer, vous écrivez vous-même le gestionnaire (handler) de liste, sur le **[Server de bas niveau](low-level-server.md)**.
## Un serveur qui pagine {#a-server-that-pages}
```python title="server.py" hl_lines="12 15-16"
--8<-- "docs_src/pagination/tutorial001.py"
```
* Sur un `Server` de bas niveau, les gestionnaires sont des arguments du constructeur, pas des décorateurs. `on_list_resources` répond à chaque requête `resources/list` ; c’est tout le branchement nécessaire.
* Chaque gestionnaire paginé est typé `params: PaginatedRequestParams | None`, et l’exemple accepte les deux. Sur une connexion, cependant, le SDK ne vous passe jamais `None` (une requête sans membre `params` arrive au gestionnaire sous la forme du modèle avec ses valeurs par défaut), donc le signal qui compte est `params.cursor is None` : **commencer par le début**.
* C’est vous qui décidez ce qu’*est* un curseur. Ici, c’est un décalage (offset) rendu sous forme de chaîne. Un horodatage, une clé primaire, un blob base64 : tout ce que vous pouvez émettre à l’aller et reconnaître au retour.
* `next_cursor=None` est votre façon de dire « c’était la dernière page ». Il n’y a ni décompte, ni total, ni `has_more`. `None` est le signal à lui seul.
!!! tip
Une valeur de `PAGE_SIZE` de 10 rend l’exemple lisible. Choisissez la vôtre par point de terminaison : une liste de
ressources d’une ligne peut se permettre une page de 500 ; une liste de gros modèles de prompts, non.
Le client n’a pas son mot à dire, et c’est voulu.
### Essayer {#try-it}
`Client(server)` se connecte à un `Server` de bas niveau en mémoire exactement comme il se connecte à un `MCPServer`.
Appelez `list_resources()` sans argument. Vous obtenez dix ressources, de `book-1` à `book-10`, et `next_cursor` vaut la chaîne `"10"`.
Renvoyez-la avec `list_resources(cursor="10")` : la première ressource est `book-11`, le nouveau `next_cursor` vaut `"20"`.
La dixième page revient avec `next_cursor` à `None`. Terminé.
## La boucle côté client {#the-client-loop}
Chaque méthode `list_*` de `Client` (`list_tools`, `list_resources`, `list_resource_templates`, `list_prompts`) accepte un argument nommé `cursor=`. Vider une liste paginée tient en un `while True` :
```python title="client.py" hl_lines="26-32"
--8<-- "docs_src/pagination/tutorial002.py"
```
* `cursor` démarre à `None`, donc la première requête ne porte aucun curseur.
* Étendez la liste **avant** de regarder `next_cursor` : la dernière page contient elle aussi des ressources.
* `next_cursor is None` est la sortie. Toute autre valeur repart directement dans `cursor=`, telle quelle.
Lancez son `main()` et il affiche `100 resources` : dix pages de dix, assemblées par une boucle qui n’a jamais su qu’il y avait dix pages.
C’est la même boucle que montre **[Le client](../client/index.md)** pour chaque verbe `list_*`, et elle ne coûte rien face à un serveur qui ne pagine pas : `next_cursor` vaut `None` dès la première réponse et la boucle s’exécute une seule fois.
## Les trois règles {#the-three-rules}
**Les curseurs sont opaques.** Un client ne doit jamais en analyser, en construire ni en deviner un. La seule source légitime d’un curseur est le `next_cursor` de la page précédente, tel quel.
**Le serveur choisit la taille de page.** Il n’y a pas de `limit=` dans le protocole. S’il vous faut une autre taille de page, vous modifiez le serveur.
**Un client qui ignore la pagination fonctionne quand même.** Il appelle `list_resources()` une fois, obtient les dix premières, et ne remarque jamais le `next_cursor` qu’il a jeté. Rien ne casse ; il en voit moins.
!!! check
Opaque veut dire opaque. Inventez un curseur (`list_resources(cursor="page-2")`) et le
protocole ne peut rien pour vous. Ce serveur tente `int("page-2")`, le gestionnaire lève une exception,
et ce qui revient au client est :
```text
MCPError(-32603, 'Internal server error', None)
```
Un curseur que vous n’avez pas obtenu du serveur est un bogue, pas une demande de fonctionnalité.
## Récapitulatif {#recap}
* `MCPServer` renvoie tout en une seule page. La pagination est facultative, et vous l’activez sur le `Server` de bas niveau.
* `on_list_resources` (ainsi que `on_list_tools`, `on_list_prompts`, `on_list_resource_templates`) reçoit `PaginatedRequestParams | None` ; `params.cursor` vaut `None` pour la première page.
* Vous renvoyez une page plus un `next_cursor` : n’importe quelle chaîne que vous reconnaîtrez plus tard, ou `None` quand il ne reste rien.
* La boucle côté client : passez `cursor=`, accumulez, répétez jusqu’à ce que `next_cursor is None`.
* Les curseurs sont opaques, la taille de page appartient au serveur, et un client qui ne pagine pas obtient quand même la première page.
Le reste de l’API `Server` écrite à la main (`on_call_tool`, les dicts `input_schema`, `_meta`) se trouve dans **[Le Server de bas niveau](low-level-server.md)**.

View file

@ -0,0 +1,122 @@
---
translation:
sections: [9e7b9a1710e5aeba, b74ca4c1d2ddddee, fa8714e61bf90c5a, 04db67a886b7271c, 857690fb8f876800]
tool: 1
---
# Indications de mise en cache {#caching-hints}
Sur le protocole 2026-07-28, chaque résultat qu’un serveur renvoie pour `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read` et `server/discover` porte deux champs : `ttlMs`, le nombre de millisecondes pendant lesquelles un client peut considérer le résultat comme frais, et `cacheScope`, qui indique si un résultat mis en cache peut être partagé entre utilisateurs (`"public"`) ou appartient à un seul contexte d’autorisation (`"private"`).
Le serveur ne met rien en cache. Ces champs sont une *déclaration* : « cette liste d’outils est la même pour tout le monde et ne changera pas pendant une minute ». Un client (ou une passerelle placée devant vous) peut alors s’épargner l’aller-retour. Respecter ces indications relève du choix du client ; les émettre est le travail du serveur, et le SDK le fait pour vous.
Par défaut, chaque résultat indique `ttlMs: 0, cacheScope: "private"` : périmé immédiatement, jamais partagé. C’est toujours sûr et toujours conforme. Si vos listes sont réellement stables et identiques pour tous les appelants, dites-le à la construction :
```python title="server.py" hl_lines="5-8"
--8<-- "docs_src/caching/tutorial001.py"
```
* Le dictionnaire est indexé par **nom de méthode**, et les six méthodes pouvant être mises en cache sont les seules clés autorisées. Le paramètre est typé `Mapping[CacheableMethod, CacheHint]` : votre éditeur complète donc les clés automatiquement et signale une faute de frappe avant l’exécution ; tout ce qui échappe au vérificateur de types lève une exception à la construction.
* Une méthode que vous ne mentionnez pas garde les valeurs par défaut. Le dictionnaire est un ensemble de surcharges, pas un manifeste.
* `CacheHint(ttl_ms=5_000)` n’a pas défini `scope`, qui reste donc `"private"` : cinq secondes de fraîcheur, par appelant. La portée et le TTL sont deux décisions indépendantes.
* `"server/discover"` est aussi une clé autorisée, puisque le résultat de découverte peut être mis en cache comme n’importe quelle liste.
!!! warning
`cacheScope: "public"` signifie que *n’importe qui* peut recevoir votre réponse mise en cache. Une passerelle
partagée transmettra sans hésiter le résultat d’un utilisateur à un autre, même lorsque la requête était
authentifiée. Ne marquez un résultat `"public"` que s’il est identique pour chaque appelant, et
n’utilisez jamais `cacheScope` comme contrôle d’accès : c’est une étiquette, pas un verrou.
## Surcharge par gestionnaire {#per-handler-override}
Sur le `Server` bas niveau, les gestionnaires (handlers) construisent leurs résultats à la main, et `ttl_ms` / `cache_scope` sont de simples champs des modèles de résultat. Un gestionnaire qui les définit explicitement l’emporte toujours sur le dictionnaire du constructeur, champ par champ :
```python title="server.py" hl_lines="10 16"
--8<-- "docs_src/caching/tutorial002.py"
```
Le gestionnaire a indiqué `ttl_ms=1_000` et rien sur la portée. Sur la liaison : `ttlMs: 1000` (la valeur du gestionnaire, pas le `60_000` du dictionnaire) et `cacheScope: "public"` (la valeur du dictionnaire, puisque le gestionnaire ne l’a pas définie). L’explicite l’emporte sur le configuré, et le configuré sur la valeur par défaut. Cela vaut champ par champ : un gestionnaire peut donc fixer un champ et laisser l’autre à la politique du serveur.
C’est aussi l’échappatoire pour les comportements dynamiques que le constructeur ne peut pas connaître : un gestionnaire qui filtre `resources/read` par utilisateur peut renvoyer `cache_scope="private"` pour un URI donné sur un serveur par ailleurs public.
Une réserve sur les listes paginées : le protocole exige **le même `cacheScope` sur chaque page** d’une même liste. Le dictionnaire du constructeur y satisfait par construction, puisqu’il est indexé par méthode et non par page. Mais un gestionnaire qui surcharge lui-même la portée devient responsable de cette cohérence : surchargez-la sur *chaque* page, jamais uniquement lorsqu’un curseur est présent, sinon la page un et la page deux se contrediront.
## Ce que voit le client {#what-the-client-sees}
Sur une session 2026-07-28, `Client` respecte les indications pour vous : il embarque un cache de réponses, activé par défaut. Un résultat qui arrive avec un `ttlMs` est stocké, et un appel identique effectué dans ce TTL est servi depuis le cache, sans aller-retour. Un résultat qui ne porte *aucune* indication n’est pas mis en cache : les résultats sans indication reçoivent `CacheConfig.default_ttl_ms`, dont la valeur par défaut est `0` (périmé immédiatement), si bien qu’un serveur qui ne déclare rien voit exactement le même trafic, appel pour appel, qu’auparavant.
```python title="client.py" hl_lines="33 35 38"
--8<-- "docs_src/caching/tutorial003.py"
```
Quatre appels, trois récupérations. Le deuxième appel a trouvé une entrée fraîche et n’a jamais atteint le serveur ; avancer l’horloge (injectée) au-delà du TTL a fait que le troisième récupère à nouveau ; le quatrième a indiqué `cache_mode="refresh"`. Cet argument nommé existe sur les cinq verbes avec cache (`list_tools`, `list_prompts`, `list_resources`, `list_resource_templates`, `read_resource`) :
* `"use"` (la valeur par défaut) sert une entrée fraîche s’il y en a une, et stocke le résultat récupéré sinon.
* `"refresh"` ne sert jamais depuis le cache : il récupère et stocke le résultat, en remplaçant ce qui était en cache.
* `"bypass"` effectue l’aller-retour sans toucher du tout au cache : ni lecture, ni écriture.
Une règle prime sur `"use"` : **les appels portant `meta` atteignent toujours le serveur.** Une requête avec `meta` défini (un jeton de progression, des champs de traçage) attend une requête sur la liaison ; sous `cache_mode="use"`, elle est donc traitée comme `"refresh"` : la lecture du cache est sautée, et le résultat récupéré remplace quand même l’entrée en cache. `"bypass"` et un `"refresh"` explicite se comportent comme d’habitude.
Pour désactiver entièrement la mise en cache, construisez avec `Client(server, cache=None)` : chaque appel redevient un aller-retour, et `cache_mode`, bien que toujours accepté, n’a aucun effet.
La portée est elle aussi respectée automatiquement : les entrées `"private"` sont indexées sur la *partition* du cache (ci-dessous), tandis que les entrées `"public"` peuvent opter pour un partage plus large. Et **les notifications priment sur le TTL** pour les entrées exactes qu’elles désignent : une notification `list_changed` évince la liste correspondante en cache, et `resources/updated` évince la lecture en cache stockée exactement sous son URI, aussi fraîches soient-elles. Sur une connexion 2026-07-28, ces notifications arrivent sur un flux `subscriptions/listen` que vous ouvrez avec `client.listen(...)`, et l’éviction se termine avant que votre observateur ne voie l’événement ; tous les détails sont dans **[Abonnements](subscriptions.md)**.
Une réserve sur `resources/updated` : l’éviction ne porte que sur l’URI exact. Le contrat du magasin n’a ni opération d’énumération ni de parcours (comme l’implémentation TypeScript de référence), donc une notification portant l’URI d’une *sous*-ressource n’évince pas la lecture en cache de son parent. Si votre serveur signale ainsi des sous-ressources, récupérez à nouveau le parent avec `cache_mode="refresh"`.
### Configurer le cache : `CacheConfig` {#configuring-it-cacheconfig}
```python
from mcp.client import CacheConfig
client = Client("https://api.example.com/mcp", cache=CacheConfig(default_ttl_ms=5_000))
```
* `store` : l’endroit où vivent les entrées. Par défaut, un nouveau magasin en mémoire par client ; passez votre propre implémentation de `ResponseCacheStore` (adossée à Redis, par exemple) pour partager un cache entre clients ou processus. Les types du contrat (`ResponseCacheStore`, `CacheKey`, `CacheEntry` et le `InMemoryResponseCacheStore` par défaut) sont importables depuis `mcp.client`. Une recherche peut émettre jusqu’à deux `get` séquentiels sur le magasin (la branche privée, puis la publique) ; dimensionnez donc en conséquence vos attentes de latence pour un magasin distant. Un magasin personnalisé **exige** une `partition` explicite.
* `partition` : l’étiquette de contexte d’autorisation qui empêche les entrées `"private"` d’un principal d’être servies à un autre au sein d’un magasin partagé.
* `target_id` : identité explicite du serveur, pour les transports personnalisés et les serveurs en processus (ci-dessous).
* `default_ttl_ms` : TTL appliqué aux résultats qui ne portent aucune indication `ttlMs`. La valeur par défaut `0` laisse les résultats sans indication hors du cache.
* `share_public` : servir entre partitions les entrées que le serveur affirme `"public"` (ci-dessous). Désactivé par défaut.
* `clock` : la source d’horloge murale, en secondes depuis l’epoch. Injectez-en une, comme le fait l’exemple ci-dessus, et les tests d’expiration n’ont pas besoin d’attendre.
!!! warning "Partition = principal vérifié"
Dérivez `partition` d’**informations d’identification vérifiées**, comme le sujet d’un jeton validé. Ne la dérivez jamais de données fournies par la requête, ni de l’URL du serveur (l’identité du serveur est un axe de clé distinct). Le SDK est une bibliothèque sans authentification propre : l’ancre de confiance est celui qui construit le `CacheConfig`, c’est-à-dire le déploiement, pas le locataire. Une passerelle multi-locataire crée un `CacheConfig` par principal authentifié.
La partition est aussi figée pour toute la durée de vie du `Client`. Si le contexte d’autorisation de la connexion change en cours de session (une réauthentification sous un autre principal, par exemple), le cache ne suit pas ; construisez un nouveau `Client` pour le nouveau principal.
Les clés du cache portent aussi **l’identité du serveur** : la chaîne d’URL que vous avez appelée, débarrassée de toute partie userinfo `user:pass@` et sinon conservée à l’octet près. Pas de normalisation de la casse, pas de réordonnancement des paramètres de requête, pas de nettoyage de la barre oblique finale. Sous-normaliser ne coûte que du partage, alors que sur-normaliser pourrait fusionner deux locataires (`?tenant=a` et `?tenant=b`) : des URL superficiellement différentes ne partagent tout simplement pas d’entrées. Lorsqu’il n’y a pas d’URL (un serveur en processus, ou une instance de `Transport`), le client reçoit à la place une identité aléatoire par instance ; définissez `CacheConfig.target_id` pour nommer le serveur (avec un magasin personnalisé, c’est obligatoire, et la construction vous le dit). L’identité est hachée en sha256 avant d’entrer dans le matériau de clé, si bien qu’une URL portant des secrets dans sa chaîne de requête n’apparaît jamais dans les clés du magasin. Ne journalisez pas non plus vous-même la forme avant hachage.
!!! warning "`share_public` fait confiance au serveur, pour tout le parc"
Par défaut, même les entrées `"public"` restent dans leur partition. `share_public=True` sert les entrées que le serveur a marquées `cacheScope: "public"` à **toutes** les partitions qui utilisent le magasin, en faisant confiance à la classification du serveur au nom de chacune d’elles. Un serveur qui appose `"public"` sur des données propres à un locataire (par bogue ou par malveillance) fait alors fuiter la réponse d’un locataire vers les autres. L’option est délibérément limitée au constructeur : le `cache_mode` par appel peut restreindre la mise en cache, mais rien au niveau de l’appel ne peut élargir le partage.
### Ce que le cache ne fait jamais {#what-the-cache-never-does}
* **Les appels au niveau session le contournent.** `client.session.list_tools()` et consorts font toujours l’aller-retour ; le cache vit sur les verbes de `Client`.
* **`server/discover` reste en dehors.** Le résultat de découverte est livré une fois, à la connexion, et n’entre jamais dans le cache de réponses, même lorsqu’il porte un `ttlMs`. Si vous en persistez un vous-même pour éviter la sonde de reconnexion ([`prior_discover`](../protocol-versions.md#reconnecting-with-prior_discover)), sa fraîcheur relève de votre propre suivi : `DiscoverResult` porte `ttl_ms` et `cache_scope`, déjà analysés, précisément à cette fin.
* **Les pages de continuation ne sont jamais mises en cache.** Seuls les appels sans curseur participent. Une page de continuation rejetée pour curseur expiré *évince* bien la liste en cache, car la liste a changé entre-temps.
* **Les lectures à plusieurs allers-retours (multi-round-trip) ne sont jamais mises en cache.** Un `read_resource` amorcé avec `input_responses`/`request_state`, ou qui se résout au fil de tours de saisie, n’entre jamais dans le cache (un MUST de la spécification).
* **L’éviction par notification a besoin de notifications.** L’éviction ne vaut que ce que vaut la livraison du transport, et le chemin moderne en processus (`Client(server)` avec le `mode="auto"` par défaut) ne livre pas aujourd’hui les notifications autonomes.
* **L’éviction se produit à terme, pas instantanément.** Les notifications qui arrivent par la liaison sont distribuées depuis des tâches lancées à part ; un appel en concurrence avec l’arrivée d’une notification peut donc se voir servir une fois de plus l’entrée d’avant l’éviction ; la fenêtre est bornée par la latence de distribution, et l’éviction a tout de même lieu.
* **Pas de stale-if-error.** Une entrée expirée n’est jamais servie parce que la nouvelle récupération a échoué ; l’erreur se propage.
* **Pas de récupération anticipée.** Une entrée stockée est servie jusqu’à expiration de son TTL, et l’appel suivant paie l’aller-retour ; rien ne se rafraîchit en arrière-plan.
* **Pas de regroupement.** Deux appels identiques concurrents font deux récupérations.
* **Pas de TTL au-delà de 24 heures.** Un `ttlMs` supérieur, qu’il vienne du serveur ou de la configuration, est ramené à ce plafond au stockage (`mcp.client.caching.MAX_TTL_MS`), ce qui borne la durée pendant laquelle une entrée, si généreuse soit son indication, peut être servie.
* Sur un **magasin partagé**, les clients sont en concurrence. Chaque client abandonne sa propre écriture lorsqu’une éviction a doublé la récupération en cours, mais un client *colocataire* peut toujours réécrire une entrée qu’une éviction qu’il n’a jamais vue avait supprimée ; et ce suivi des concurrences est lui-même borné : au-delà de 4 096 clés suivies, la garde de la clé la plus ancienne est abandonnée en premier. Les deux fenêtres sont acceptées, et refermées par le plafond de TTL ci-dessus.
* **Pas de service d’une génération de protocole à l’autre.** Les entrées sont rattachées à la version de protocole négociée : sur un magasin persistant partagé, une session ne sert jamais une entrée écrite sous une autre version négociée (la même liste diffère réellement selon la génération, puisque le SDK retire les champs 2026 pour les sessions plus anciennes). L’éviction, de même, ne touche que les entrées de la génération courante ; les entrées d’une autre génération expirent simplement avec leur TTL.
### Lire les indications vous-même {#reading-the-hints-yourself}
Les indications sont aussi de simples champs sur chaque résultat pouvant être mis en cache (`result.ttl_ms` et `result.cache_scope`, déjà analysés), au cas où vous voudriez superposer votre propre suivi au cache intégré (ou le remplacer).
Face à un **serveur plus ancien** (protocole antérieur à 2026), les champs sont tout simplement absents de la liaison, et les modèles affichent leurs valeurs par défaut prudentes : `ttl_ms == 0` et `cache_scope == "private"`, périmé et non partagé, la bonne hypothèse pour un serveur qui n’a rien déclaré. Le cache traite une session historique de la même façon : les indications n’y sont jamais consultées (quelles que soient les clés présentes sur la liaison), seul `default_ttl_ms` s’applique, et sa valeur par défaut de `0` ne met rien en cache, de sorte qu’une connexion antérieure à 2026 se comporte exactement comme avant l’existence du cache. Si vous devez distinguer « le serveur a dit 0 » de « le serveur n’a rien dit », testez `"ttl_ms" in result.model_fields_set` : il n’est défini que lorsque le champ est réellement arrivé.
## Clients plus anciens {#older-clients}
Les clients sur des versions de protocole antérieures à 2026 ne voient jamais ni l’un ni l’autre de ces champs ; le SDK les retire à la sérialisation pour ces connexions. Configurez vos indications une fois pour toutes ; il n’y a rien de propre à une version à écrire.
## Récapitulatif {#recap}
* Six méthodes portent `ttlMs`/`cacheScope` ; le SDK leur donne par défaut `0`/`"private"`, périmé et non partagé, toujours sûr.
* `cache_hints={method: CacheHint(...)}` à la construction (`MCPServer` comme `Server`) fixe des valeurs par méthode pour tout le serveur.
* Un gestionnaire qui définit les champs sur son résultat surcharge le dictionnaire, champ par champ.
* `"public"` est la promesse que le résultat est identique pour chaque appelant. Ce n’est pas un contrôle d’accès.
* `Client` respecte les indications automatiquement : son cache de réponses est activé par défaut, sert les entrées fraîches au lieu de les récupérer à nouveau, et ne met rien en cache pour les serveurs (ou les sessions) qui ne fournissent aucune indication.
* Par appel, `cache_mode="refresh"` récupère à nouveau et `"bypass"` saute le cache ; `cache=None` à la construction le désactive entièrement.

View file

@ -0,0 +1,154 @@
---
translation:
sections: [adf3c545b5be46b6, 916cd3ab1c03f461, e9be7a8d0eb0a456, 565890a636288ecf, 6af7e49db9129ec3, 06b0238c174186af, 90c6043be435fcb0]
tool: 1
---
# Fonctions de rappel du client {#client-callbacks}
Presque toutes les requêtes dans MCP vont dans un seul sens : du client vers le serveur.
Un serveur peut aussi demander des choses au **client** : poser une question à l’utilisateur, échantillonner le modèle de l’utilisateur, lister les dossiers de son espace de travail. Vous répondez à ces requêtes en passant des **fonctions de rappel** (callbacks) à `Client(...)`.
## Un serveur qui demande {#a-server-that-asks}
Voici un serveur dont l’outil ne peut pas terminer tout seul :
```python title="server.py" hl_lines="16"
--8<-- "docs_src/client_callbacks/tutorial001.py"
```
* `ctx.elicit(...)` envoie une requête `elicitation/create` **au client** et attend.
* L’outil ne renvoie rien tant que quelqu’un (une personne devant un formulaire, ou votre code) n’a pas fourni un `name`.
C’est la moitié serveur, et la page **[Élicitation](../handlers/elicitation.md)** la couvre en détail. Cette page-ci se tient à l’autre bout de la liaison.
## La fonction de rappel d’élicitation {#the-elicitation-callback}
```python title="client.py" hl_lines="6-10 16-17"
--8<-- "docs_src/client_callbacks/tutorial002.py"
```
* Une fonction de rappel d’élicitation (elicitation) a pour signature `async (context, params) -> ElicitResult`.
* `params.message` est la question. `params.requested_schema` est le JSON Schema de la réponse que le serveur attend. Un vrai client en tire un formulaire ; celui-ci le remplit automatiquement.
* Vous renvoyez `ElicitResult(action="accept", content={...})`, ou `action="decline"`, ou `action="cancel"`. La seule autre option est `ErrorData(...)`, qui refuse la requête et fait échouer l’appel entier.
* `context` est un `ClientRequestContext` : la `session` active, le `request_id` du serveur et les éventuelles `meta` qu’il a jointes.
!!! tip
`params` est une union des deux modes d’élicitation. Ici `params.mode` vaut `"form"` ; une requête `"url"`
porte `params.url` au lieu d’un schéma. Une seule fonction de rappel gère les deux ; branchez sur `params.mode`.
**[Élicitation](../handlers/elicitation.md)** montre le motif complet.
### Essayer {#try-it}
Appelez `issue_card` et observez les deux extrémités.
Votre fonction de rappel reçoit la question du serveur, déjà analysée :
```python
params.mode # 'form'
params.message # 'What name should go on the card?'
params.requested_schema # {'properties': {'name': {'title': 'Name', 'type': 'string'}},
# 'required': ['name'], 'title': 'CardHolder', 'type': 'object'}
```
Elle répond, `ctx.elicit(...)` reprend à l’intérieur de l’outil, et l’outil termine :
```python
result.content # [TextContent(type='text', text='Card issued to Ada Lovelace.')]
```
Un `tools/call` de votre part, un `elicitation/create` en retour du serveur, auquel votre fonction répond, le tout à l’intérieur d’un seul appel d’outil.
!!! info
`mode="legacy"` dans l’appel `Client(...)` fait un vrai travail. Par défaut, `Client(...)` négocie le chemin
moderne du protocole, et ce chemin n’a pas de canal de retour (back-channel) pour les requêtes du serveur vers le client : `ctx.elicit`
échoue avant même que votre fonction de rappel ne s’exécute. Ce n’est pas le transport qui en décide ; c’est le
protocole négocié, en mémoire comme via une URL. Fixez `mode="legacy"` dès que votre client doit
répondre à l’une d’elles ; tous les tests derrière cette page le font. Tous les détails sont dans **[Versions du protocole](../protocol-versions.md)**.
Sur une session 2026-07-28, la fonction de rappel n’est pas morte, elle est alimentée autrement : quand un outil renvoie un
`InputRequiredResult` portant une `ElicitRequest`, `Client` transmet cette entrée à la même
`elicitation_callback` et relance l’appel pour vous. Ce flux est décrit dans **[Requêtes à plusieurs allers-retours](../handlers/multi-round-trip.md)** (multi-round-trip).
## Une fonction de rappel est une capacité {#a-callback-is-a-capability}
Vous n’avez jamais dit au serveur que votre client sait répondre aux requêtes d’élicitation. Le SDK l’a fait.
Quand un client se connecte, il déclare ses `capabilities`, l’image miroir de celles du serveur. Vous n’écrivez pas cet objet. **Enregistrer une fonction de rappel vaut déclaration.**
| vous passez | le client déclare |
| --- | --- |
| `elicitation_callback=` | `"elicitation": {"form": {}, "url": {}}` |
| `sampling_callback=` | `"sampling": {}` |
| `list_roots_callback=` | `"roots": {"listChanged": true}` |
| aucune d’elles | `{}` |
Les sous-capacités d’échantillonnage (sampling) sont le seul raffinement : passez `sampling_capabilities=SamplingCapability(tools=SamplingToolsCapability())` en plus de `sampling_callback` lorsque votre échantillonneur gère les paramètres `tools` / `tool_choice`. Les serveurs doivent voir `sampling.tools` déclaré avant de pouvoir les envoyer.
`logging_callback` et `message_handler` ne figurent pas dans le tableau. Ils traitent des notifications, et les notifications n’exigent aucune capacité.
Le serveur relit la déclaration avec `ctx.session.check_client_capability(...)`. Ajoutez un outil qui le fait :
```python title="server.py" hl_lines="23-31"
--8<-- "docs_src/client_callbacks/tutorial003.py"
```
Connectez-vous avec seulement `elicitation_callback` et appelez-le :
```python
result.structured_content # {'result': ['elicitation']}
```
Passez les trois fonctions de rappel et vous obtenez `['elicitation', 'sampling', 'roots']`. N’en passez aucune et vous obtenez `[]`.
!!! check
Faites maintenant ce qu’il ne faut pas : connectez-vous **sans** `elicitation_callback` et appelez `issue_card` quand même.
La requête `elicitation/create` du serveur atteint toujours votre client, et le SDK y répond à votre
place, par une erreur, puisque vous n’avez jamais dit pouvoir la traiter. Cette erreur coule l’appel entier.
`call_tool` ne renvoie pas un résultat `is_error` ; il lève une exception :
```text
MCPError: Elicitation not supported
```
C’est une erreur de protocole (`-32600`, *requête invalide*), pas une erreur d’outil : le modèle n’a rien
à lire ni à retenter. C’est pourquoi `client_features` vaut la peine : un serveur bien élevé
vérifie avant de demander.
## La paire obsolète {#the-deprecated-pair}
`sampling_callback` répond à `sampling/createMessage` : le serveur demande à *votre* modèle de compléter quelque chose. `list_roots_callback` répond à `roots/list` : le serveur demande dans quels répertoires il peut travailler.
Les deux fonctionnent. Les deux suivent la règle ci-dessus. Et les deux servent des RPC que la **spécification 2026-07-28 supprime** : un serveur moderne ne rappelle pas votre client en pleine requête, il vous rend la requête dans le résultat de l’outil (**[Requêtes à plusieurs allers-retours](../handlers/multi-round-trip.md)**). Les fonctions de rappel elles-mêmes ne sont pas mortes. Quand un `InputRequiredResult` porte une `CreateMessageRequest` ou une `ListRootsRequest`, la boucle automatique de `Client` la transmet à la même `sampling_callback` ou `list_roots_callback` que vous avez enregistrée ici. La liste complète est dans **[Fonctionnalités obsolètes](../deprecated.md)**.
Vous avez encore besoin de ces fonctions de rappel pour parler aux serveurs qui n’ont pas migré. Les signatures :
```python title="client.py"
--8<-- "docs_src/client_callbacks/tutorial004.py"
```
* Une fonction de rappel d’échantillonnage reçoit le `CreateMessageRequestParams` complet (`messages`, `model_preferences`, `max_tokens`) et renvoie un `CreateMessageResult`. C’est *vous* qui exécutez le modèle, comme bon vous semble ; le SDK ne fait que transporter la requête.
* Une fonction de rappel de racines (roots) ne prend aucun paramètre et renvoie un `ListRootsResult`.
* L’une comme l’autre peut renvoyer `ErrorData(...)` à la place, pour refuser.
Passez-les à `Client(...)` exactement comme `elicitation_callback`.
## Les fonctions de rappel de notification {#the-notification-callbacks}
Deux de plus. Aucune ne déclare quoi que ce soit.
`logging_callback` reçoit les `notifications/message` qu’un serveur envoie, sous forme de `LoggingMessageNotificationParams` (`level`, `logger`, `data`). La journalisation par le protocole est elle-même rendue obsolète par la spécification 2026-07-28 (**[Journalisation](../handlers/logging.md)** explique quoi faire à la place), donc cette fonction de rappel existe pour les serveurs qui l’émettent encore. Sur une connexion de génération 2026, la fonction de rappel seule ne vous apporte rien, car les serveurs 2026 n’envoient des messages de journal qu’aux requêtes qui en font la demande : passez `log_level="info"` (ou un autre niveau) à `Client(...)` pour apposer cette demande sur chaque requête et recevoir ce niveau et les niveaux supérieurs. Les serveurs antérieurs à 2026 l’ignorent et conservent leur comportement `logging/setLevel`.
`message_handler` est le fourre-tout : chaque notification serveur que la session remonte lui parvient (en plus de sa fonction de rappel spécifique), et sur un transport adossé à un flux, chaque `Exception` de niveau transport aussi. Deux n’y parviennent jamais : `notifications/cancelled` est appliquée par le SDK plutôt que remontée, et l’accusé de réception d’abonnement d’un flux `listen()` actif est consommé par ce flux. Annotez le paramètre avec `IncomingMessage` (`ServerNotification | Exception`, exporté depuis `mcp.client`). Le seul motif à connaître est `if isinstance(message, Exception): raise message`, pour qu’une connexion rompue échoue bruyamment au lieu de disparaître en silence.
## Récapitulatif {#recap}
* Un serveur peut envoyer des requêtes au client. Vous y répondez avec des fonctions de rappel passées à `Client(...)`.
* La fonction de rappel d’élicitation est celle d’actualité : `async (context, params) -> ElicitResult`, une seule fonction pour les modes formulaire et URL.
* **Enregistrer une fonction de rappel, c’est déclarer la capacité.** Sans elle, le SDK refuse la requête du serveur à votre place et l’appel entier échoue avec `MCPError`.
* Un serveur le sait avant de demander grâce à `ctx.session.check_client_capability(...)`.
* `sampling_callback` et `list_roots_callback` fonctionnent de la même manière mais servent des fonctionnalités obsolètes ; les serveurs modernes utilisent à la place les requêtes à plusieurs allers-retours.
* `logging_callback` et `message_handler` reçoivent des notifications. Ils ne déclarent rien.
Le premier argument de `Client(...)` est un objet transport. **[Transports client](transports.md)** couvre tous les types.

View file

@ -0,0 +1,155 @@
---
translation:
sections: [a91322c46111d16d, 8e6fd6d6f59bb568, e7828fd2729b2c9d, a03ec26bfc678b65, 1034c653c0bcf1b0]
tool: 1
---
# Assertion d’identité {#identity-assertion}
Un fournisseur OAuth ordinaire (**[Clients OAuth](oauth-clients.md)**) commence par poser une question au serveur MCP : *à quel serveur d’autorisation faites-vous confiance ?* Il suit la réponse où qu’elle mène, puis soit une personne se connecte, soit un secret pré-partagé en tient lieu.
Une entreprise ne veut voir ni l’un ni l’autre décidé serveur par serveur. Elle exploite déjà un fournisseur d’identité (Okta, Microsoft Entra ID, le vôtre) ; l’utilisateur s’y est déjà connecté ce matin ; et c’est l’unique endroit où l’équipe sécurité veut décider qui peut accéder à quoi. La [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990), l’extension **Enterprise-Managed Authorization**, y déplace la décision. L’IdP signe un JWT de courte durée, un **Identity Assertion JWT Authorization Grant**, l’**ID-JAG** : une déclaration selon laquelle *cet utilisateur*, via *ce client*, peut accéder à *ce serveur MCP*. Le client l’échange contre un jeton d’accès ordinaire. Pas de navigateur, pas d’écran de consentement, pas d’enregistrement dynamique.
Cette page couvre les deux extrémités de cet échange. Le serveur MCP lui-même ne change jamais : il reste le serveur de ressources de **[Autorisation](../run/authorization.md)**, qui vérifie le jeton qui se présente, quel qu’il soit.
## Deux requêtes de jeton {#two-token-requests}
Deux autorités différentes sont en jeu, et bien les distinguer, c’est l’essentiel pour comprendre cette page. L’**IdP d’entreprise** est le fournisseur d’identité de votre organisation : il sait qui est l’employé, c’est là que réside la politique d’accès, et il émet l’ID-JAG. Le SDK ne lui parle jamais. Le **serveur d’autorisation MCP** est le même acteur que dans **[Autorisation](../run/authorization.md)** : l’émetteur nommé dans les métadonnées du serveur MCP, celui qui émet les jetons que ce serveur MCP accepte. Dans un flux OAuth ordinaire, ces deux rôles tiennent généralement dans une seule boîte. Ici ils sont deux, et tout le grant consiste en ce que le second accepte de faire confiance au premier.
Le client adresse une requête de jeton à chacun.
1. **Vers l’IdP d’entreprise.** Le client échange la connexion de l’utilisateur (son jeton d’identité OpenID Connect) contre l’ID-JAG. C’est un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693), c’est entièrement l’API de votre IdP, et **le SDK ne l’effectue pas**. C’est vous qui le faites, dans une seule fonction de rappel (callback) asynchrone. C’est aussi là que se prend la décision de politique : un IdP qui dit non n’émet jamais l’ID-JAG, et il n’y a rien à présenter.
2. **Vers le serveur d’autorisation MCP.** Le client présente l’ID-JAG sous le grant `jwt-bearer` de la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, l’ID-JAG comme `assertion`) et reçoit le jeton d’accès. **C’est la requête que le SDK effectue**, et l’accepter est la seule chose que cette page ajoute à un serveur d’autorisation.
Tout ce qui suit concerne la seconde requête : le client qui l’envoie et le serveur d’autorisation qui y répond.
## Le client {#the-client}
**`IdentityAssertionOAuthProvider`** se trouve dans `mcp.client.auth.extensions.identity_assertion`. Comme tous les fournisseurs de **[Clients OAuth](oauth-clients.md)**, c’est un `httpx2.Auth` : construisez-en un, placez-le sur `auth=`, passez le `httpx2.AsyncClient` au transport.
```python title="client.py" hl_lines="49-50 53-61"
--8<-- "docs_src/identity_assertion/tutorial001.py"
```
Lisez-le en partant du bas.
* `main()` est le `main()` standard d’un client OAuth (**[Clients OAuth](oauth-clients.md)**), inchangé ligne pour ligne. C’est tout l’intérêt : une fois le fournisseur en place, rien en aval ne sait quel grant a produit le jeton.
* Le fournisseur prend ce que les autres fournisseurs ne peuvent pas découvrir : un `client_id` et un `client_secret` que quelqu’un a **pré-enregistrés** auprès du serveur d’autorisation, la valeur `issuer` de ce serveur d’autorisation, et `assertion_provider`, une fonction de rappel asynchrone qui renvoie un ID-JAG tout neuf à la demande.
* `storage` est le même protocole `TokenStorage`. Seules les deux méthodes de jeton sont appelées ; il n’y a pas d’enregistrement dynamique ici, donc pas de `client_info` à mémoriser.
### Le fournisseur d’assertion {#the-assertion-provider}
`fetch_id_jag(audience, resource)` est le seul code que vous écrivez. Il est attendu (await) une fois par échange de jeton, jamais à la construction, et seulement *après* que les métadonnées du serveur d’autorisation ont été récupérées et validées, si bien qu’un émetteur mal configuré ne laisse jamais fuiter une assertion. Ses deux arguments sont deux des claims avec lesquels l’ID-JAG doit être émis : `audience` est l’émetteur du serveur d’autorisation (le `aud` de l’ID-JAG) et `resource` est l’identifiant canonique du serveur MCP (le `resource` de l’ID-JAG). Le troisième, vous le détenez déjà : le claim `client_id` de l’ID-JAG doit nommer le `client_id` que vous avez donné au fournisseur, faute de quoi le serveur d’autorisation refuse l’échange.
`idp_issue_id_jag`, juste au-dessus, n’est **pas votre code**. Il tient lieu de fournisseur d’identité et signe l’assertion dans le processus même, pour que le fichier soit complet et que vous puissiez lire chaque claim que porte un ID-JAG. Un vrai `fetch_id_jag` effectue à la place la première requête de jeton de la section précédente : un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) auprès de votre IdP, défini par le draft Identity Assertion JWT Authorization Grant dont la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) définit un profil. Le jeton d’identité de l’utilisateur connecté y entre comme `subject_token`, le `requested_token_type` est l’URN propre à l’ID-JAG (`urn:ietf:params:oauth:token-type:id-jag`), `audience` et `resource` sont transmis tels quels, et la réponse porte l’ID-JAG. Cet échange, sous ces noms-là, est ce qu’il faut chercher dans la documentation de votre IdP.
!!! tip
Un nouvel ID-JAG est demandé à chaque échange, et c’est voulu : c’est un grant à usage unique,
valable quelques minutes, et le serveur d’autorisation de cette page refuse d’accepter deux fois
le même. Ne le mettez pas en cache. C’est le jeton d’accès qu’il vous procure qui est réutilisé.
### L’émetteur relève de la configuration {#the-issuer-is-configuration}
Voici l’inversion. `OAuthClientProvider` demande au serveur de ressources quel serveur d’autorisation utiliser et suit la réponse où qu’elle mène. Ce fournisseur-ci s’y refuse : `issuer` est obligatoire, les métadonnées [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) sont récupérées depuis le chemin well-known de cet émetteur même, le point de terminaison de jeton doit se trouver sur l’origine de cet émetteur, et rien n’est jamais demandé au serveur de ressources.
L’extension ne l’exige pas ; c’est un choix délibérément plus strict. Ce client transporte deux choses qui valent d’être volées, un secret pré-enregistré et une assertion liée à une audience, et un client qui laisserait un serveur MCP compromis l’aiguiller vers le serveur d’autorisation d’un attaquant y posterait les deux. Épingler l’émetteur à la construction supprime purement et simplement cette conversation.
!!! warning
La valeur `issuer` configurée est comparée au champ `issuer` du document de métadonnées par la
comparaison de chaînes simple de la RFC 8414 §3.3 : caractère par caractère, barre oblique finale
comprise, sans normalisation. Ne la devinez pas. Récupérez `/.well-known/oauth-authorization-server`
auprès de votre serveur d’autorisation et copiez la valeur `issuer` qu’il renvoie. Pour le serveur
d’autorisation de cette page, c’est `https://auth.example.com/`, avec la barre oblique, parce que
son émetteur a été construit à partir d’un objet URL pydantic. Une discordance arrête le flux
sur `OAuthFlowError: Authorization server metadata issuer
mismatch` avant qu’un seul identifiant ou une seule assertion ne soit envoyé.
### Un client confidentiel {#a-confidential-client}
`client_secret` est obligatoire ; sans lui, le constructeur lève `ValueError`. Le profil IETF sous-jacent à la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) réserve ce grant aux clients confidentiels, la SEP-990 exige que le client s’authentifie, et ce SDK fait respecter les deux en imposant un secret partagé. `token_endpoint_auth_method` choisit par où il transite : `client_secret_post` (la valeur par défaut, dans le corps du formulaire) ou `client_secret_basic` (un en-tête HTTP Basic). Le profil autorise aussi `private_key_jwt` ; ce fournisseur ne le prend pas en charge.
!!! tip
Lisez `client_secret` depuis l’environnement ou un gestionnaire de secrets, jamais depuis le dépôt de code.
### Ce que le fournisseur fait pour vous {#what-the-provider-does-for-you}
La première requête part sans authentification, et le `401` du serveur démarre le flux.
1. **Découverte.** Il récupère les métadonnées du serveur d’autorisation depuis le chemin well-known [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) de l’émetteur configuré, vérifie que la valeur `issuer` du document correspond, et vérifie que le point de terminaison de jeton se trouve sur l’origine de l’émetteur.
2. **L’assertion.** Il attend (await) votre `assertion_provider`.
3. **Échange.** Il envoie en POST le grant `jwt-bearer` au point de terminaison de jeton, stocke le `OAuthToken`, et rejoue votre requête d’origine avec `Authorization: Bearer ...`.
Un `403` dont le `WWW-Authenticate` nomme `insufficient_scope` relance les étapes 2 et 3 avec l’union de votre `scope` et de celui du défi. (`scope` n’est jamais qu’une demande ; le serveur d’autorisation de cette page accorde ce que dit l’ID-JAG et rien d’autre.) Il n’y a de jeton d’actualisation nulle part ici : quand le jeton d’accès expire, le `401` suivant fait émettre un nouvel ID-JAG et relance l’échange, et c’est *là* le levier que détient l’IdP. Les échecs sont les deux mêmes exceptions que dans le reste de **[Clients OAuth](oauth-clients.md)** : `OAuthFlowError` pour la découverte et la validation, sa sous-classe `OAuthTokenError` quand le point de terminaison de jeton dit non.
## Le serveur d’autorisation {#the-authorization-server}
La plupart du temps, vous vous arrêtez ici. Le serveur d’autorisation MCP est le produit de quelqu’un d’autre, accepter les ID-JAG est une option de sa configuration à activer, et la moitié de la [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) qui revient au SDK est le client ci-dessus.
Le SDK peut aussi *être* le serveur d’autorisation : `create_auth_routes` renvoie les routes du serveur d’autorisation sous forme d’une liste que n’importe quelle application Starlette peut monter, et c’est ainsi que `examples/servers/simple-auth/` dans le dépôt en fait tourner un. La SEP-990 ajoute un drapeau et une méthode à cette surface :
```python title="auth_server.py" hl_lines="48-50 105-107"
--8<-- "docs_src/identity_assertion/tutorial002.py"
```
* `identity_assertion_enabled=True` conditionne tout. Désactivé, ce qui est la valeur par défaut, `/token` répond à ce grant par `unsupported_grant_type` même si vous avez implémenté le hook, et les métadonnées n’en font pas mention. Activé, les métadonnées gagnent le type de grant `jwt-bearer` et listent `urn:ietf:params:oauth:grant-profile:id-jag` dans `authorization_grant_profiles_supported`, le champ par lequel l’extension annonce sa prise en charge. (Le client de ce SDK ne le lit jamais : il est provisionné pour un seul émetteur et demande, tout simplement.)
* **`exchange_identity_assertion`** est le hook. Avant qu’il ne s’exécute, le SDK a authentifié le client, refusé les clients publics, et refusé les clients dont l’enregistrement ne liste pas le grant. Vous recevez un `IdentityAssertionParams` (la valeur `assertion` brute, les `scopes` et `resource` demandés) et renvoyez un simple `OAuthToken`.
* L’enregistrement dynamique des clients refuse ce grant sans condition, si bien que `get_client` sert ici un client provisionné à la main. Un client ID-JAG ne peut pas se faire exister en s’enregistrant lui-même.
* La moitié de la classe est faite de refus. `OAuthAuthorizationServerProvider` est le serveur d’autorisation *tout entier*, il réclame donc aussi le flux authorization code ; un serveur qui connecte aussi des utilisateurs implémente ces méthodes pour de bon, et celui-ci n’a qu’une seule porte.
!!! warning
Le SDK ne décode jamais l’assertion : seul votre déploiement sait à quel IdP il fait confiance et
quelles clés cet IdP publie, donc tout ce qui se trouve dans `exchange_identity_assertion` est
déterminant. Vérifiez la signature par rapport aux clés publiées de l’IdP (son JWKS ; le secret
partagé ici est celui de la démo), ainsi que `iss` et `exp`, selon la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523) §3. Exigez
que le `typ` de l’en-tête JWT soit `oauth-id-jag+jwt`, le garde-fou du profil contre le rejeu
d’un autre JWT comme grant. Exigez que `aud` soit votre propre émetteur. Exigez que le claim
`client_id` de l’ID-JAG soit égal au client que le gestionnaire (handler) a authentifié, et que
son claim `resource` nomme une ressource que vous servez réellement. Suivez `jti` jusqu’à la
valeur `exp` de l’assertion pour qu’elle ne soit acceptée qu’une fois. Et tirez les scopes
accordés et, surtout, le `resource` du jeton émis de l’ID-JAG validé, jamais de la requête :
`params.resource` est ce que le client a tapé, quoi que ce soit. Les règles de traitement
complètes sont dans la
[spécification Enterprise-Managed Authorization](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization).
Rejetez une mauvaise assertion avec `TokenError("invalid_grant", ...)`. L’autre code d’erreur de ce flux est `invalid_target` : un ID-JAG qui nomme une ressource que vous ne servez pas est refusé avec lui, et c’est ce qui empêche ce serveur d’émettre des jetons pour celle de quelqu’un d’autre. Et les scopes accordés viennent du claim `scope` de l’ID-JAG (une assertion qui n’en a pas est refusée elle aussi) ; le vôtre pourrait plutôt faire correspondre les groupes de l’utilisateur.
Et remarquez ce que le `OAuthToken` renvoyé ne porte pas : un jeton d’actualisation. L’IdP décide combien de temps cet utilisateur garde l’accès en décidant d’émettre ou non le prochain ID-JAG. Un jeton d’actualisation émis ici reprendrait en douce cette décision à l’IdP.
!!! info
Un serveur qui embarque encore son serveur d’autorisation avec `auth_server_provider=` atteint le
même code via `AuthSettings(identity_assertion_enabled=True)`. **[Autorisation](../run/authorization.md)** explique pourquoi
les nouveaux serveurs ne devraient pas commencer par là.
!!! check
Reliez les deux fichiers de cette page et tout le grant tient en un seul `POST /token` :
```text
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6Im9hdXRoLWlkLWphZytqd3QifQ...
client_id=finance-agent
resource=http://localhost:8001/mcp
scope=notes:read
client_secret=finance-agent-secret
HTTP/1.1 200 OK
{"access_token": "mcp_...", "token_type": "Bearer", "expires_in": 300, "scope": "notes:read"}
```
Pas de `/authorize`, pas de `/register`, pas de récupération des métadonnées de ressource
protégée. Les seules requêtes sur la liaison sont celle qui a provoqué le `401`, la récupération
well-known, cet échange, puis le trafic MCP ordinaire avec le jeton porteur attaché. Et le `sub`
que votre validateur a lu dans l’ID-JAG est exactement ce que `get_access_token().subject`
rapporte à l’intérieur d’un outil.
### Essayer {#try-it}
`examples/stories/identity_assertion/` dans le dépôt du SDK, c’est cette page exécutée pour de bon : le même validateur `exchange_identity_assertion`, un serveur MCP protégé par ses jetons, un IdP de substitution et le client, dans un seul programme qui se vérifie lui-même. `uv run python -m stories.identity_assertion.client --http` exécute tout l’échange et vérifie par assertion que l’utilisateur nommé par l’IdP est bien celui que voit l’outil.
## Récapitulatif {#recap}
* La [SEP-990](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/990) laisse le fournisseur d’identité de l’entreprise, et non l’utilisateur final, décider quels serveurs MCP un client peut atteindre. L’IdP signe cette décision dans un **ID-JAG**.
* Obtenir l’ID-JAG est un échange de jetons [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) auprès de *votre IdP*, et le SDK ne l’effectue pas. Le présenter au serveur d’autorisation MCP relève du grant `jwt-bearer` de la [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), et le SDK en assure les deux côtés.
* `IdentityAssertionOAuthProvider` est un `httpx2.Auth` de plus : un client confidentiel pré-enregistré, un `issuer` épinglé, et une fonction de rappel `assertion_provider(audience, resource)`. Pas de navigateur, pas d’enregistrement, pas de jeton d’actualisation.
* Le serveur d’autorisation n’est jamais découvert à partir du serveur de ressources. Configurez `issuer` avec exactement la chaîne que sert son document de métadonnées ; la comparaison se fait caractère par caractère.
* Côté serveur, `identity_assertion_enabled=True` plus `exchange_identity_assertion`. Le SDK authentifie le client et conditionne le grant ; valider l’ID-JAG vous revient entièrement, et le jeton émis est lié au `resource` de l’ID-JAG, pas à celui de la requête.
Le seul acteur auquel cette page n’a jamais touché est le serveur MCP. Ce qu’il fait du jeton que vous venez d’émettre, il le faisait déjà dans **[Autorisation](../run/authorization.md)**.

View file

@ -0,0 +1,217 @@
---
translation:
sections: [ebef1e7a0df854f4, a4c687d3d627d516, 8e79141fc2985342, b345dd05b9c3c7ab, 80ce41579825a6fa, 5f0fa90494de8f65, 83d10514eaa62fa5, 9190555aa39a5d28, 84a4c9d8bf14dddb, 927d71cf40b58c30]
tool: 1
---
# Le client {#the-client}
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 {#your-first-client}
```python title="client.py" hl_lines="14-18"
--8<-- "docs_src/client/tutorial001.py"
```
Le serveur en haut n’est là que pour vous donner quelque chose à quoi vous connecter. Le client, ce sont les cinq lignes surlignées.
* `Client(mcp)` reçoit **l’objet serveur lui-même**. C’est le transport en mémoire : pas de sous-processus, pas de port, pas de HTTP. C’est ainsi que se connectent tous les exemples de cette page, et tous les tests que vous écrivez.
* `async with` est le **cycle de vie**. Y entrer connecte et négocie ; en sortir déconnecte. Il n’y a pas de paire `connect()` / `close()`, et un `Client` ne 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` {#what-you-can-pass-to-client}
`Client` prend un seul argument positionnel et déduit le transport de son type :
* Une instance de `MCPServer` (ou du `Server` bas niveau) : connexion **dans le processus**.
* Une chaîne d’URL (`Client("http://localhost:8000/mcp")`) : Streamable HTTP, la voie de production.
* Un **transport** : tout ce sur quoi vous pouvez faire `async with ... as (read, write)`, comme `stdio_client(...)` qui enveloppe un sous-processus.
Tout le reste de cette page est identique pour les trois. Les en-têtes, les sous-processus, les délais d’expiration et le protocole `Transport` ont leur propre page : **[Transports côté client](transports.md)**.
### Ce que porte un client connecté {#whats-on-a-connected-client}
Quatre propriétés en lecture seule, renseignées dès que vous entrez dans le bloc :
* `client.server_info` : l’identité du serveur, ou `None` pour 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.name` vaut `"Bookshop"` et `server_info.version` est 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 vaut `None`.
* `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îne `instructions=` du serveur, ou `None` s’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](../protocol-versions.md)**.
!!! 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 {#listing-tools}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial002.py"
```
`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 :
```python
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 :
```json
{
"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.
!!! 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 {#calling-a-tool}
`call_tool(name, arguments)` exécute l’outil et vous renvoie un `CallToolResult`.
```python title="client.py" hl_lines="26-33"
--8<-- "docs_src/client/tutorial003.py"
```
Le `lookup_book` du serveur renvoie un `Book` Pydantic. Voici ce que voit le client :
```python
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-what-the-model-reads}
`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-what-your-application-reads}
`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](../servers/structured-output.md)**.
### `is_error` : si l’outil a échoué {#is_error-whether-the-tool-failed}
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
`ValueError`. L’appel revient pourtant normalement :
```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
```
Le message de l’exception 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. 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](../servers/handling-errors.md)** explique quand un serveur produit l’une ou l’autre.
## Ressources {#resources}
Les verbes des ressources vont par paires : deux façons de lister, une façon de lire.
```python title="client.py" hl_lines="22-31"
--8<-- "docs_src/client/tutorial004.py"
```
* `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 simple `str` et 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](subscriptions.md)** de cette section.
## Prompts {#prompts}
```python title="client.py" hl_lines="15-20"
--8<-- "docs_src/client/tutorial005.py"
```
`list_prompts()` vous dit ce que le serveur propose et ce dont chaque prompt a besoin :
```python
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` :
```python
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 {#completions}
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.
```python title="client.py" hl_lines="27-31"
--8<-- "docs_src/client/tutorial006.py"
```
* `ref` indique *quel* prompt ou modèle vous remplissez : un `PromptReference` ou un `ResourceTemplateReference`.
* `argument` vaut `{"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](../servers/completions.md)**.
## Pagination {#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.
```python title="client.py" hl_lines="22-30"
--8<-- "docs_src/client/tutorial007.py"
```
Cette boucle 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](../advanced/pagination.md)**.
## Dans les tests {#in-tests}
`Client(mcp)`, sans processus ni port, est déjà un banc de test pour votre serveur.
Il existe un drapeau du constructeur conçu pour cela : `Client(mcp, raise_exceptions=True)`. Il n’a d’effet que sur les connexions en mémoire, et **[Tests](../get-started/testing.md)** est la page qui l’explique et construit tout le modèle autour de lui.
## Récapitulatif {#recap}
* `Client(x)` se connecte en mémoire à un objet serveur, en Streamable HTTP à une chaîne d’URL, et à tout le reste via un transport.
* `async with` est tout le cycle de vie. À l’intérieur, `server_capabilities` et `protocol_version` sont déjà renseignés ; `server_info` et `instructions` le sont aussi lorsque le serveur les fournit.
* `list_tools()` vous donne le `name`, le `title`, la `description` et le `input_schema` de chaque outil.
* `call_tool()` renvoie `content` pour le modèle, `structured_content` pour votre code, et `is_error`. Un outil qui lève une exception est un résultat, pas une exception.
* `content` est une union de types de blocs ; restreignez le type avec `isinstance` avant de lire.
* `list_resources` / `list_resource_templates` / `read_resource`, `list_prompts` / `get_prompt` et `complete` complètent la liste des verbes.
* Chaque `list_*` accepte `cursor=` ; bouclez jusqu’à ce que `next_cursor` vaille `None`.
Ce qu’un serveur peut demander au *client*, et la façon d’y répondre, c’est **[Fonctions de rappel du client](callbacks.md)**.

View file

@ -0,0 +1,152 @@
---
translation:
sections: [c6899d3892bd9fa0, 79372cff3cc48a88, 63878d29e87c3e73, 13175843d3588af4, e7e2b9fd516f77de, 758f06399b513c1f, a05d7278487d610b]
tool: 1
---
# Clients OAuth {#oauth-clients}
Certains serveurs MCP sont protégés. Envoyez-leur une requête sans jeton et ils répondent `401 Unauthorized`.
**`OAuthClientProvider`** est le moyen d’obtenir ce jeton. Ce n’est pas du tout un objet MCP. C’est un `httpx2.Auth`, le hook standard de httpx2 pour « faire quelque chose à chaque requête ». Vous l’attachez à un `httpx2.AsyncClient`, vous confiez ce client au transport Streamable HTTP, et vous n’y pensez plus.
Cette page couvre le côté client. Pour que votre propre serveur exige un jeton, voyez **[Autorisation](../run/authorization.md)**.
## Le fournisseur {#the-provider}
```python title="client.py" hl_lines="44-54"
--8<-- "docs_src/oauth_clients/tutorial001.py"
```
Vous lui donnez quatre choses :
* `server_url` : le point de terminaison MCP auquel vous vous connectez. Le fournisseur découvre tout le reste à partir de lui.
* `client_metadata` : ce que vous saisiriez dans le formulaire « enregistrer une application » d’un serveur d’autorisation.
* `storage` : là où les jetons vivent entre deux exécutions.
* `redirect_handler` et `callback_handler` : les deux moments où un humain intervient.
Rien d’autre dans le fichier ne mentionne OAuth. `main()` ne voit jamais un jeton.
### Métadonnées du client {#client-metadata}
`OAuthClientMetadata` est le véritable document d’enregistrement de la [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591), sous forme de modèle Pydantic.
Vous définissez trois champs. Les valeurs par défaut remplissent le reste : `grant_types` vaut déjà `["authorization_code", "refresh_token"]` et `response_types` vaut déjà `["code"]`, ce qui correspond exactement au flux qu’exécute ce fournisseur.
!!! check
Comme c’est un modèle Pydantic, il valide **avant qu’un seul octet ne parte sur le réseau**.
Omettez `redirect_uris` et la construction échoue immédiatement avec une `ValidationError` qui
nomme le champ :
```text
redirect_uris
Field required [type=missing, input_value={'client_name': 'Bookshop Agent'}, input_type=dict]
```
Aucun navigateur ouvert, aucun enregistrement à moitié terminé laissé derrière sur le serveur d’autorisation.
### Stockage des jetons {#token-storage}
**`TokenStorage`** est un `Protocol` avec quatre méthodes asynchrones. Vous n’héritez de rien ; écrivez les méthodes et n’importe quelle classe devient un magasin de jetons :
* `get_tokens` / `set_tokens` conservent l’objet `OAuthToken` : jeton d’accès, jeton d’actualisation, expiration, portée.
* `get_client_info` / `set_client_info` conservent l’objet `OAuthClientInformationFull` que le serveur d’autorisation a émis lorsque le fournisseur vous a enregistré, y compris votre `client_id`.
La version en mémoire ci-dessus fonctionne. Elle oublie aussi tout quand le processus se termine, si bien que l’exécution suivante refait toute la procédure. Persistez-la dans un fichier ou dans le trousseau de votre plateforme et l’exécution suivante est silencieuse.
!!! tip
Stockez `client_info`, pas seulement les jetons. Le fournisseur s’enregistre dynamiquement la première fois qu’il
ne trouve aucun `client_info` stocké. Jetez-le et vous créez un nouvel enregistrement à chaque exécution.
### Les deux gestionnaires {#the-two-handlers}
Le flux du code d’autorisation a besoin d’un humain exactement une fois : quelqu’un doit se connecter et cliquer sur « autoriser ».
* **`redirect_handler`** est attendu (await) avec l’URL d’autorisation entièrement construite. Le `client_id`, le `redirect_uri`, le `state` et le défi PKCE y figurent déjà. Votre seul travail est d’y amener un navigateur. Une application de bureau appelle `webbrowser.open` ; ce fichier l’affiche.
* **`callback_handler`** est attendu ensuite. Il patiente jusqu’à ce que l’utilisateur revienne sur votre `redirect_uri` et renvoie les paramètres de requête de cette redirection sous la forme d’un `AuthorizationCodeResult`.
Un vrai client fait tourner un petit serveur HTTP local sur l’URI de redirection au lieu d’appeler `input()`. La forme est identique : recevoir la redirection, rendre `code`, `state` et `iss`.
!!! warning
Transmettez `state` et `iss` exactement tels qu’ils sont arrivés. Le fournisseur compare `state` à celui
qu’il a généré et `iss` à l’émetteur qu’il a découvert, et refuse toute divergence. Ce sont les défenses
contre le CSRF et contre la confusion de serveurs (mix-up).
### Dans le `Client` {#into-the-client}
Regardez `main()`. Le fournisseur va sur le **client httpx2**, le client httpx2 va dans `streamable_http_client(url, http_client=...)`, et ce transport va dans `Client`.
`streamable_http_client` n’a pas de mot-clé `auth=`. Tout ce qui relève du niveau HTTP (authentification, en-têtes, délais d’expiration, proxys) appartient au `httpx2.AsyncClient` que vous apportez. Cette superposition de couches est décrite dans **[Transports client](transports.md)**.
## Ce que le fournisseur fait pour vous {#what-the-provider-does-for-you}
La première fois que `Client` envoie une requête, le serveur répond `401`. Le fournisseur prend le relais :
1. **Découverte.** Il lit l’en-tête `WWW-Authenticate`, récupère les Protected Resource Metadata du serveur depuis `/.well-known/oauth-protected-resource`, apprend quel serveur d’autorisation protège cette ressource, et récupère les métadonnées de *ce* serveur-là.
2. **Enregistrement.** Rien dans le stockage ? Il vous enregistre dynamiquement avec votre `OAuthClientMetadata` et stocke le résultat.
3. **Autorisation.** Il génère la paire PKCE et un `state`, construit l’URL d’autorisation, attend votre `redirect_handler`, puis attend votre `callback_handler` pour obtenir le code.
4. **Échange.** Il échange le code contre un `OAuthToken`, le stocke, et rejoue votre requête d’origine avec `Authorization: Bearer ...`.
Après cela, il se fait discret. Les jetons sortent du stockage, un jeton d’accès expiré est actualisé avec le jeton d’actualisation, et ce n’est que lorsque rien de tout cela ne fonctionne qu’il relance le flux.
Vous n’avez rien écrit de tout cela. Il reste deux arguments nommés (`client_metadata_url` et `validate_resource_url`), et ce fichier n’a besoin d’aucun des deux. `client_metadata_url` est celui qui mérite d’être connu ; il a sa propre section plus bas.
### Essayer {#try-it}
La plupart des exemples de cette documentation se vérifient avec un `Client(server)` en mémoire. Pas celui-ci : tout l’intérêt du flux est un `401` HTTP, et il n’y a pas de HTTP entre un client en mémoire et son serveur.
Le dépôt fournit la version réelle. `examples/servers/simple-auth/` exécute un serveur d’autorisation autonome et un serveur MCP protégé ; `examples/clients/simple-auth-client/` est le client de cette page devenu une petite CLI. Son README donne les deux commandes : démarrez les serveurs, lancez le client contre eux, et vous voyez défiler les quatre étapes.
## Client ID Metadata Documents {#client-id-metadata-documents}
La révision 2026-07-28 de la spécification rend obsolète l’enregistrement dynamique des clients au profit des **Client ID Metadata Documents** (CIMD). Au lieu d’envoyer par POST un nouvel enregistrement à chaque serveur d’autorisation qu’il rencontre, votre client publie un unique document JSON le décrivant à une URL HTTPS stable, et cette URL *est* son `client_id`. Le serveur d’autorisation récupère le document ; le fournisseur n’y touche jamais.
Le SDK le parle déjà : passez l’URL dans `client_metadata_url=` quand vous construisez le fournisseur. Lorsque les métadonnées du serveur d’autorisation annoncent `client_id_metadata_document_supported: true`, le fournisseur saute entièrement la requête `/register` : l’URL entre dans le flux en tant que `client_id`, et il n’y a pas de `client_secret`. Lorsque le serveur ne l’annonce pas (la plupart ne le font pas encore), ou que vous ne passez jamais d’URL, le fournisseur se rabat **silencieusement** sur l’enregistrement dynamique, et tout ce qui précède fonctionne exactement comme décrit. Un `client_info` stocké l’emporte toujours sur les deux.
L’URL doit être en HTTPS avec un chemin autre que la racine ; tout le reste lève une `ValueError` à la construction, avant le moindre échange réseau. L’exemple fourni `examples/clients/simple-auth-client/` la reçoit via la variable d’environnement `MCP_CLIENT_METADATA_URL`.
## De machine à machine {#machine-to-machine}
Une tâche nocturne, une étape de CI, un autre service. Il n’y a pas de navigateur et personne pour cliquer sur « autoriser ». C’est le type d’octroi **client credentials** : vous détenez déjà un `client_id` et un `client_secret`, et le point de terminaison de jeton constitue tout le flux.
`ClientCredentialsOAuthProvider` est le même `httpx2.Auth`, l’humain en moins :
```python title="client.py" hl_lines="4 27-33"
--8<-- "docs_src/oauth_clients/tutorial002.py"
```
Ce qui a changé :
* Aucun `OAuthClientMetadata`, aucun gestionnaire. Vous passez `client_id` et `client_secret` ; le fournisseur construit autour d’eux un enregistrement `client_credentials` minimal et saute entièrement l’enregistrement dynamique.
* `scope` est une chaîne séparée par des espaces, le format qu’OAuth utilise sur la liaison.
* Tout ce qui se trouve en aval est identique : le même `TokenStorage`, le même `httpx2.AsyncClient(auth=...)`, le même `streamable_http_client`.
Par défaut, le secret voyage en authentification HTTP Basic sur la requête de jeton (`client_secret_basic`). Passez `token_endpoint_auth_method="client_secret_post"` pour le placer plutôt dans le corps du formulaire. Certains serveurs d’autorisation n’acceptent que l’une des deux méthodes.
!!! tip
Lisez `client_secret` depuis l’environnement ou un gestionnaire de secrets, jamais depuis le contrôle de version.
!!! info
Un fournisseur de plus se trouve dans `mcp.client.auth.extensions.client_credentials` :
**`PrivateKeyJWTOAuthProvider`**, pour les clients qui s’authentifient avec un JWT plutôt qu’avec un
secret partagé (`private_key_jwt`, la variante à paire de clés et identité de charge de travail). Il suit
le même schéma : construisez-en un, placez-le sur `auth=`. Le même module fournit
`SignedJWTParameters` et `static_assertion_provider`, deux utilitaires qui construisent son assertion.
Il existe une autre situation sans humain : le client appartient à une entreprise dont le fournisseur d’identité, et non l’utilisateur, décide quels serveurs MCP il peut atteindre. C’est un type d’octroi différent, avec son propre modèle de confiance et sa propre page, **[Assertion d’identité](identity-assertion.md)**.
## En cas d’échec {#when-it-fails}
Quand le flux OAuth tourne mal, le fournisseur lève une `OAuthFlowError` depuis `mcp.client.auth`. Elle a deux sous-classes. `OAuthRegistrationError` signifie que l’enregistrement n’a pas produit un client utilisable : le serveur d’autorisation a refusé de vous enregistrer, ou il vous a bien enregistré mais avec des identifiants que ce flux ne peut pas utiliser (par exemple une méthode d’authentification qu’il n’implémente pas). `OAuthTokenError` signifie qu’un jeton n’a pas pu être obtenu : le point de terminaison de jeton a dit non, ou une fiche client stockée porte une méthode d’authentification que ce client ne peut pas appliquer, ce qui est signalé pendant la construction de la requête de jeton plutôt qu’envoyé. Un seul `except OAuthFlowError:` couvre la découverte, l’enregistrement, l’autorisation et l’échange.
Tout n’est pas une erreur de flux. Le réseau peut toujours échouer ; ce sont des exceptions `httpx2` ordinaires et elles passent sans être modifiées.
## Récapitulatif {#recap}
* `OAuthClientProvider` est un `httpx2.Auth`. Placez-le sur un `httpx2.AsyncClient`, passez celui-ci à `streamable_http_client(url, http_client=...)`, et `Client` ne sait jamais qu’OAuth a eu lieu.
* Vous fournissez quatre choses : l’URL du serveur, un `OAuthClientMetadata`, un `TokenStorage` et la paire de gestionnaires redirect/callback.
* `TokenStorage` est un `Protocol` : quatre méthodes asynchrones, pas de classe de base. Persistez `client_info` en plus des jetons.
* La découverte, l’enregistrement (dynamique, ou via un **Client ID Metadata Document**), PKCE, les vérifications de `state` et `iss`, et l’actualisation des jetons sont l’affaire du fournisseur, pas la vôtre.
* `ClientCredentialsOAuthProvider` est la version sans humain : `client_id` + `client_secret`, pas de gestionnaires, pas de navigateur.
* Tout échec OAuth est une `OAuthFlowError` ; `OAuthRegistrationError` et `OAuthTokenError` en sont les sous-classes.
L’autre moitié de cette poignée de main, faire en sorte que votre *serveur* exige le jeton, se trouve dans **[Autorisation](../run/authorization.md)**.

View file

@ -0,0 +1,87 @@
---
translation:
sections: [09c857a25a9dc37a, 43bc6a76a243a50e, 0a716022a88768df, 4b7f78042bfcfff7, c112662e61b03315, 58974ba1f489a8b4, d18adbdbb835ea73]
tool: 1
---
# Groupes de sessions {#session-groups}
Un `Client` se connecte à un seul serveur. Les applications réelles en veulent souvent plusieurs (un serveur de recherche, un serveur de base de données, une API interne) et finissent par jongler avec une connexion et une liste d’outils pour chacun.
**`ClientSessionGroup`** est un objet unique qui détient de nombreuses connexions et fusionne tout ce qu’elles exposent en une seule vue.
## Deux serveurs {#two-servers}
Commencez par deux serveurs ordinaires. Ils n’ont rien à voir l’un avec l’autre, si bien que tous deux ont naturellement appelé leur outil `search` :
```python title="library_server.py" hl_lines="7"
--8<-- "docs_src/session_groups/tutorial001.py"
```
```python title="web_server.py" hl_lines="7"
--8<-- "docs_src/session_groups/tutorial002.py"
```
## Un groupe {#one-group}
Créez un `ClientSessionGroup` et appelez **`connect_to_server`** une fois par serveur :
```python title="client.py" hl_lines="10-12"
--8<-- "docs_src/session_groups/tutorial003.py"
```
* `connect_to_server` prend des paramètres de transport, pas un objet serveur : `StdioServerParameters` (depuis `mcp`) pour lancer un sous-processus, ou `StreamableHttpParameters` / `SseServerParameters` (depuis `mcp.client.session_group`) pour un serveur qui écoute déjà sur une URL.
* `group.tools` est un `dict[str, Tool]` regroupant les outils de tous les serveurs connectés. `group.resources` et `group.prompts` ont la même forme.
* `group.call_tool(name, arguments)` recherche le nom, trouve la session qui le possède et lui transmet l’appel. Vous n’indiquez jamais quel serveur.
!!! check
Placez `client.py` à côté des deux serveurs et exécutez-le. Le second `connect_to_server` refuse :
```text
mcp.shared.exceptions.MCPError: {'search'} already exist in group tools.
```
C’est une `MCPError`, levée avant que quoi que ce soit du second serveur ne soit enregistré. Un nom doit
être unique dans **tout** le groupe, et deux serveurs que vous ne contrôlez pas finiront tôt ou tard par entrer en collision.
## `component_name_hook` {#component_name_hook}
Vous corrigez cela au niveau du groupe, pas des serveurs. Passez une fonction de `(name, server_info)` et le groupe l’exécute sur chaque nom qu’il enregistre :
```python title="client.py" hl_lines="7-8 15"
--8<-- "docs_src/session_groups/tutorial004.py"
```
Relancez-le. `print(sorted(group.tools))` affiche maintenant les deux :
```text
['Library.search', 'Web.search']
```
* La **clé** est à vous. `by_server` l’a construite à partir de `server_info.name`, le nom avec lequel chaque `MCPServer(...)` a été construit.
* Le `Tool` à l’intérieur est intact : `group.tools["Web.search"].name` vaut toujours `"search"`, et c’est ce nom que `call_tool` envoie sur la liaison. Le préfixe ne quitte jamais votre processus.
* Cela ne concerne pas que les outils. La ressource `hours` de la bibliothèque est enregistrée sous le nom `Library.hours`.
!!! tip
Le hook s’exécute sur **chaque** nom de **chaque** serveur, pas seulement en cas de conflit : il n’existe pas de
mode « préfixe en cas de collision ». Choisissez un schéma et laissez-le s’appliquer partout.
## Ajouter et retirer des serveurs {#adding-and-removing-servers}
`connect_to_server` renvoie la `ClientSession` qu’il a ouverte. Conservez-la si vous voulez un jour vous séparer de ce serveur : `await group.disconnect_from_server(session)` retire ses outils, ressources et prompts du groupe.
Si vous détenez déjà une `ClientSession` connectée (`Client.session` en est une), passez-la à `await group.connect_with_session(server_info, session)` au lieu d’ouvrir un nouveau transport. Elle est agrégée de la même façon. Le groupe ne ferme jamais une session qu’il n’a pas ouverte. `server_info` nomme le serveur pour les préfixes de composants ; sur une connexion de génération 2026, `client.server_info` peut valoir `None` (l’identité est facultative), passez donc votre propre `Implementation(name=..., version=...)` dans ce cas.
## La poignée de main classique {#the-classic-handshake}
`ClientSessionGroup` est construit sur `ClientSession`, pas sur `Client`. Chaque `connect_to_server` exécute la poignée de main (handshake) `initialize` classique. Il n’envoie jamais la sonde `server/discover` décrite dans **[Versions du protocole](../protocol-versions.md)**. Tous les serveurs MCP comprennent cette poignée de main, donc cela ne vous coûte aucune compatibilité ; cela signifie seulement qu’un groupe emprunte le chemin plus ancien et plus lent vers un serveur qui pourrait faire mieux.
## Récapitulatif {#recap}
* `ClientSessionGroup` détient de nombreuses connexions serveur et fusionne leurs outils, ressources et prompts en un `dict` chacun.
* `connect_to_server(params)` par serveur. Il prend des paramètres de transport, jamais l’objet serveur ni l’URL que prend un `Client`.
* `group.call_tool(name, arguments)` achemine l’appel vers le serveur propriétaire à votre place.
* Les noms doivent être uniques dans tout le groupe ; deux serveurs dotés d’un outil `search` ne peuvent pas coexister tels quels.
* `component_name_hook=` réécrit chaque nom enregistré. La clé du dict change, pas le nom sur la liaison.
* `connect_with_session` ajoute une session que vous détenez déjà ; `disconnect_from_server` en retire une.
La poignée de main que parle un groupe (et celle, plus rapide, que préfère un `Client`) fait l’objet de **[Versions du protocole](../protocol-versions.md)**.

View file

@ -0,0 +1,91 @@
---
translation:
sections: [8f9558e57f29eee1, a88c587739e0465c, 46ebfd5b325ed041, 4d10b00b57ce4bd9, 2cdb0edd1f59b3e2]
tool: 1
---
# Abonnements {#subscriptions}
Le catalogue d’un serveur n’est pas figé. Des outils apparaissent à l’exécution, et le contenu derrière l’URI d’une ressource change. Un client l’apprend grâce à `client.listen(...)` : une seule requête `subscriptions/listen` dont la réponse *est* le flux. Elle reste ouverte et transporte les notifications de changement que le client a demandées.
Cette page couvre le côté client : ouvrir le flux, le surveiller en parallèle de votre traitement principal, et gérer la façon dont il se termine. Publier les changements, filtrer et servir la méthode relèvent du serveur ; c’est raconté dans **[Abonnements](../handlers/subscriptions.md)**, sous *Dans votre gestionnaire*. Les exemples de cette page dialoguent avec le serveur de tableau de sprint construit là-bas.
## Surveiller le flux {#watching-the-stream}
Un abonnement est un gestionnaire de contexte, un seul. Y entrer envoie la requête, avec vos arguments nommés comme filtre d’abonnement, puis attend la confirmation du serveur : le flux est donc actif au moment où le bloc commence.
```python title="client.py" hl_lines="15 18 28"
--8<-- "docs_src/subscriptions/tutorial003.py"
```
L’itération produit quatre événements typés : `ToolsListChanged`, `PromptsListChanged`, `ResourcesListChanged` et `ResourceUpdated(uri=...)`.
Un événement dit *ce qui* a changé, jamais *comment*. C’est pourquoi `follow_board` appelle `read_resource` et `list_tools` : l’événement est un signal pour récupérer à nouveau les données. Lisez `event.uri` plutôt que de supposer quelle ressource a bougé : un filtre peut nommer plusieurs URI, et un serveur peut signaler un changement sur une sous-ressource de l’un d’eux.
Les événements en double qui attendent d’être consommés se fondent en un seul, et une nouvelle récupération vous donne tout de même l’état courant. Seuls les événements identiques se fondent : deux `ResourceUpdated` pour des URI différents sont deux événements.
Deux autres propriétés de l’objet d’abonnement :
* `sub.honored` est le filtre que le serveur a confirmé : un `SubscriptionFilter` avec les champs que vous avez passés, lisibles comme attributs (`sub.honored.prompts_list_changed`). `MCPServer` honore tous les types d’événements que vous demandez, il vous renvoie donc votre requête telle quelle. Un serveur qui prend en charge moins de types en confirme moins, et un type confirmé peut malgré tout ne jamais se déclencher. Un serveur peut aussi refuser la requête entière plutôt que de la confirmer (voir [Décider qui peut surveiller](../handlers/subscriptions.md#deciding-who-may-watch) sur la page serveur), ce qui se manifeste comme l’erreur de la requête.
* `sub.subscription_id` est l’identifiant de la requête listen, celui qui est apposé sur chaque trame de ce flux. Plusieurs abonnements peuvent être ouverts en même temps, chacun démultiplexé par son propre identifiant.
## Surveiller sans bloquer {#watching-without-blocking}
`follow_board` tourne jusqu’à ce que le serveur ferme le flux, ce qui peut ne jamais arriver ; seule, elle monopolise donc votre programme. Les vrais clients veulent l’observateur *à côté* du traitement principal : un agent appelle des outils pendant qu’un observateur maintient à jour un cache ou une interface.
Ouvrez d’abord l’abonnement, puis démarrez l’observateur et poursuivez votre travail.
=== "asyncio"
```python title="app.py" hl_lines="18 20"
--8<-- "docs_src/subscriptions/tutorial004_asyncio.py"
```
=== "trio"
```python title="app.py" hl_lines="18 21"
--8<-- "docs_src/subscriptions/tutorial004_trio.py"
```
=== "anyio"
```python title="app.py" hl_lines="18 21"
--8<-- "docs_src/subscriptions/tutorial004_anyio.py"
```
!!! note
`app.py` importe `BOARD` et `read_board` depuis le premier exemple, que ce dépôt stocke sous
le nom `tutorial003.py`. Si vous enregistrez les fichiers affichés côte à côte sous les noms
`client.py` et `app.py`, écrivez plutôt `from client import BOARD, read_board`. L’exemple
`watch.py`, plus bas, importe `read_board` de la même façon.
L’ordre est tout l’enjeu. Rien n’est rejoué : un événement publié avant que votre flux n’existe est perdu. Entrer dans `client.listen(...)` attend la confirmation, si bien que chaque changement à partir de cet instant atteint votre observateur, et l’instantané que vous prenez dans le bloc ne peut en manquer aucun.
Les requêtes s’exécutent librement à côté d’un flux ouvert, depuis la tâche de l’observateur ou n’importe quelle autre, sur le même client. Comme les événements non consommés *en double* fusionnent, un traitement principal chargé peut produire une seule récupération plutôt que trois. Les événements qui diffèrent ne fusionnent pas : un filtre qui nomme de nombreux URI met en file un événement en attente par URI.
Pour arrêter de surveiller, sortez du bloc : il n’y a pas d’appel `unsubscribe`. Annuler la tâche qui possède le bloc le fait pour vous, et le SDK annule la requête listen comme le transport l’attend : en Streamable HTTP, en fermant le flux de cette requête. Un observateur qui tourne pendant toute la durée de vie de votre application ne revient jamais de lui-même ; annulez-le donc, ou la portée de son groupe de tâches, à l’arrêt.
## Les flux se terminent {#streams-end}
Un flux se termine de l’une de deux façons, toutes deux relevant du flot de contrôle ordinaire. Une fermeture propre côté serveur met fin à la boucle `async for` ; une coupure brutale lève `SubscriptionLost`.
La différence sert au diagnostic, elle ne change pas ce qu’il faut faire ensuite : le flux a disparu, rien n’a été rejoué, et un observateur toujours intéressé réécoute et récupère à nouveau.
```python title="watch.py" hl_lines="16 20"
--8<-- "docs_src/subscriptions/tutorial005.py"
```
Les serveurs ferment proprement les flux pour des raisons qui leur sont propres, notamment pour se délester d’un abonné dont l’arriéré a trop grossi ; une fin propre n’est donc pas un signal pour cesser de surveiller. Temporisez avant de réécouter.
`SubscriptionLost` a aussi une cause locale. Le client conserve au plus 1 024 événements non consommés, et un consommateur qui prend autant de retard perd l’abonnement plutôt que de grossir sans limite. Gardez le corps de la boucle `async for` court et faites le travail lent ailleurs.
`keep_following` n’intercepte que `SubscriptionLost`. Entrer dans `listen()` peut aussi lever `MCPError` (la connexion a échoué, ou le serveur ne sert pas la méthode), `TimeoutError` (aucune confirmation n’est arrivée) et `ListenNotSupportedError` (une connexion antérieure à 2026). Décidez lesquelles votre observateur devrait retenter : la dernière ne se résorbe jamais.
## Récapitulatif {#recap}
* Entrez dans `async with client.listen(...)` ; l’entrée attend la confirmation, donc rien de ce qui est publié ensuite n’est manqué.
* Itérez avec `async for event in sub`. Les événements sont des signaux pour récupérer à nouveau, jamais des charges utiles.
* Ouvrez l’abonnement, puis lancez l’observateur comme tâche, et les appels d’outils continuent de circuler à côté.
* Une fin propre arrête la boucle ; une coupure lève `SubscriptionLost`. Dans les deux cas : réécoutez, récupérez à nouveau, en temporisant d’abord.
* Sortir du bloc, c’est se désabonner.
Publier ces événements, restreindre le filtre et passer à l’échelle au-delà d’un seul processus relèvent du serveur : **[Abonnements](../handlers/subscriptions.md)**. Ces mêmes événements maintiennent aussi à jour un cache côté client, et **[Mise en cache](caching.md)** est la page suivante.

View file

@ -0,0 +1,128 @@
---
translation:
sections: [9cac816674181eb0, 0700f337babcd4dd, 2bde0dd58cdf00f5, ff7401df479af877, 3d0832f39b0d7059, d4bf7e4479637768, 05e20c0a798860e7]
tool: 1
---
# Transports côté client {#client-transports}
Chaque `Client` dialogue avec son serveur via un **transport** : ce qui achemine réellement les messages.
Vous n’en configurez jamais un séparément. `Client` prend un seul argument positionnel et déduit le transport de son type.
Le côté *serveur* de chacun (ce que fait `mcp.run()` et ce que vous déployez) est traité dans **[Exécuter votre serveur](../run/index.md)**.
## En mémoire {#in-memory}
Passez l’objet serveur lui-même :
```python title="client.py" hl_lines="14"
--8<-- "docs_src/client_transports/tutorial001.py"
```
Pas de sous-processus, pas de port, aucun octet sur une liaison. Le client et le serveur sont deux objets dans le même processus, et l’appel passe tout de même par la véritable couche protocolaire : `search_books` est listé, validé et invoqué exactement comme il le serait via HTTP.
Cela en fait deux choses à la fois :
* **Un banc de test.** Chaque exemple de cette documentation est exécuté de cette façon, et la page **[Tests](../get-started/testing.md)** construit tout son modèle autour de lui.
* **Une API d’intégration.** Une application qui construit le serveur n’a pas besoin d’un saut réseau pour appeler ses outils.
## Streamable HTTP {#streamable-http}
Passez une URL sous forme de chaîne et vous obtenez **Streamable HTTP**, le transport derrière lequel vous déployez :
```python title="client.py" hl_lines="5"
--8<-- "docs_src/client_transports/tutorial002.py"
```
C’est tout le client de production. `Client` enveloppe l’URL dans `streamable_http_client(...)` pour vous, par-dessus un `httpx2.AsyncClient` configuré comme MCP l’exige : `follow_redirects=True`, un délai d’expiration de 30 secondes pour connect/write/pool, et un délai de lecture de 300 secondes parce que le serveur peut garder un flux de réponse ouvert.
!!! check
Un `Client` que vous venez de construire n’est **pas** connecté. La construction ne fait que choisir le transport ;
c’est `async with` qui l’ouvre. Tentez d’accéder à la connexion avant d’y entrer et le SDK vous le signale :
```text
RuntimeError: Client must be used within an async context manager
```
Rien n’a été résolu, récupéré ni lancé quand vous avez écrit `Client("http://...")`. Cette ligne ne coûte rien.
### Fournir votre propre `httpx2.AsyncClient` {#bring-your-own-httpx2asyncclient}
Dès que vous avez besoin d’un en-tête `Authorization`, d’un cookie, d’un proxy, de mTLS ou d’un délai d’expiration différent, construisez le `httpx2.AsyncClient` vous-même et passez-le à `streamable_http_client` :
```python title="client.py" hl_lines="8-14"
--8<-- "docs_src/client_transports/tutorial003.py"
```
Deux points à remarquer :
* Le `httpx2.AsyncClient` vous appartient, donc c’est **vous** qui y entrez et en sortez. Le SDK ne ferme jamais un client qu’il n’a pas créé.
* `streamable_http_client(url, http_client=...)` renvoie un transport, et `Client(transport)` l’accepte comme n’importe quoi d’autre.
Une remarque sur TLS : `httpx2` vérifie les certificats par rapport au magasin de confiance du système d’exploitation (via
[`truststore`](https://pypi.org/project/truststore/)), et non par rapport à une liste d’autorités de certification embarquée. Dans un environnement sans
magasin d’autorités de certification système utilisable (certains conteneurs minimaux), définissez les variables d’environnement standard `SSL_CERT_FILE`/`SSL_CERT_DIR`
ou passez un `verify=ssl_context` explicite à votre `httpx2.AsyncClient`
(le contexte se trouve dans
[`httpx` et `httpx-sse` remplacés par `httpx2`](../migration.md#httpx-and-httpx-sse-replaced-by-httpx2)).
!!! warning
`streamable_http_client` acceptait autrefois `headers=` et `timeout=` directement. Ce n’est plus le cas :
ses seuls paramètres sont `url`, `http_client` et `terminate_on_close`. Utilisez `headers=` par
habitude et vous obtenez :
```text
TypeError: streamable_http_client() got an unexpected keyword argument 'headers'
```
Tout ce qui relève de HTTP se trouve désormais sur l’unique `httpx2.AsyncClient` que vous passez.
!!! info
`httpx2` conserve l’API familière de `httpx` ; si vous connaissez `httpx`, vous savez déjà comment gérer ici l’authentification,
les proxys, les hooks d’événements, les nouvelles tentatives et les limites de connexions. Le SDK n’ajoute rien par-dessus et ne retire
rien. C’est aussi là qu’OAuth se branche :
`httpx2.AsyncClient(auth=OAuthClientProvider(...))`. Tout ce flux est décrit dans **[Clients OAuth](oauth-clients.md)**.
## stdio {#stdio}
Un serveur **stdio** est un sous-processus. Le client le lance, écrit du JSON-RPC sur son stdin et lit du JSON-RPC depuis son stdout. C’est ainsi qu’un hôte de bureau exécute un serveur sur votre machine : un hôte *est* ce code plus une interface utilisateur, et **[Se connecter à un véritable hôte](../get-started/real-host.md)** montre la même relation vue du côté de l’hôte, sous forme de fichier de configuration.
Décrivez le processus avec `StdioServerParameters`, transformez-le en transport avec `stdio_client`, et passez *cela* à `Client` :
```python title="client.py" hl_lines="4-8 12"
--8<-- "docs_src/client_transports/tutorial004.py"
```
`Client` n’accepte pas l’objet de paramètres seul. `StdioServerParameters` est de la configuration ; `stdio_client(server)` est le transport qui sait lancer un processus à partir de celle-ci. Enveloppez toujours.
Quitter le bloc `async with` arrête aussi le sous-processus : fermeture de stdin, attente, arrêt forcé s’il traîne. Vous ne le nettoyez jamais vous-même.
!!! warning
Le processus enfant n’hérite **pas** de votre environnement. Il reçoit une liste d’autorisation minimale (`HOME`, `LOGNAME`,
`PATH`, `SHELL`, `TERM` et `USER` sous POSIX), de sorte que rien de sensible ne fuite vers un processus que vous n’avez peut-être
pas écrit.
Un serveur qui a besoin d’une clé d’API ne l’y trouvera pas. Passez-la explicitement avec `env=` ; ces
variables sont fusionnées par-dessus la liste d’autorisation. C’est ce que fait `BOOKSHOP_API_KEY` ci-dessus.
## SSE {#sse}
`sse_client(url)`, du module `mcp.client.sse`, est le transport HTTP que Streamable HTTP a remplacé. Enveloppez-le de la même manière, `Client(sse_client("http://localhost:8000/sse"))`, pour dialoguer avec un serveur qui le parle encore, et ne construisez rien de nouveau dessus.
## Le protocole `Transport` {#the-transport-protocol}
Pour `Client`, tout ce qui précède est une seule et même chose.
Un **transport** est n’importe quel gestionnaire de contexte asynchrone qui produit une paire `(read, write)` de flux de messages : formellement, le protocole `Transport` de `mcp.client`. `Client` résout son argument selon son type : un objet serveur se connecte dans le processus, une `str` devient `streamable_http_client(url)`, et tout le reste est ouvert directement comme transport. C’est cette dernière règle qui explique pourquoi `stdio_client(...)`, `streamable_http_client(...)` et `sse_client(...)` s’insèrent tous au même emplacement, et pourquoi vous pouvez écrire le vôtre.
## Récapitulatif {#recap}
* `Client(mcp)` (l’objet serveur) se connecte en mémoire. Utilisez-le pour les tests et pour l’intégration.
* `Client("http://.../mcp")` (une URL) se connecte via Streamable HTTP, le transport de production.
* Les en-têtes, l’authentification, les proxys et les délais d’expiration vont sur un `httpx2.AsyncClient` que vous passez à `streamable_http_client(url, http_client=...)`. Il n’y a pas de mot-clé `headers=`.
* stdio s’écrit `Client(stdio_client(StdioServerParameters(...)))`, jamais l’objet de paramètres seul.
* Le sous-processus reçoit un environnement sous liste d’autorisation, pas le vôtre ; `env=` s’y ajoute.
* Un transport est tout ce sur quoi vous pouvez faire `async with x as (read, write)`. `Client` transmet directement à ce protocole tout ce qui n’est ni un objet serveur ni une URL.
* Construire un `Client` choisit le transport. `async with` l’ouvre.
Une fois le transport ouvert, les deux côtés doivent s’accorder sur une version du protocole. En temps normal, vous n’y pensez jamais ; le jour où vous devez y penser, la page à consulter est **[Versions du protocole](../protocol-versions.md)**.

View file

@ -0,0 +1,97 @@
---
translation:
sections: [20541a40dbdd5980, 01262a123ad9501d, 429db5b574a2ac08, 56b2d49da412cb28, 6a1717123fe4513c]
tool: 1
---
# Fonctionnalités obsolètes {#deprecated-features}
La spécification 2026-07-28 retire cinq éléments. Le SDK les implémente toujours tous, et chacun d’eux porte désormais un **avertissement d’obsolescence**.
Le tableau ci-dessous nomme chaque fonctionnalité obsolète, la raison de sa disparition et le remplacement sur lequel vous appuyer.
## Ce qui est obsolète {#what-is-deprecated}
| Obsolète | Pourquoi | Ce que vous faites à la place |
|---|---|---|
| **Racines (roots)** : `ctx.session.list_roots()`, `client.send_roots_list_changed()`, le `list_roots_callback=` que vous passez à `Client(...)` | La [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577) retire la capacité. | Prenez les chemins comme arguments d’outil ordinaires ou comme URI de ressource, ou intégrez une `ListRootsRequest` dans un `InputRequiredResult` (voir **[Requêtes à plusieurs allers-retours (multi-round-trip)](handlers/multi-round-trip.md)**). |
| **Échantillonnage (sampling) à l’initiative du serveur** : `ctx.session.create_message()`, le `sampling_callback=` que vous passez à `Client(...)` | La SEP-2577 retire la capacité. | Renvoyez `InputRequiredResult` et laissez le client réessayer l’appel (voir **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)**). |
| **Journalisation par le protocole** : `ctx.log()`, `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`, `ctx.session.send_log_message()`, `client.set_logging_level()` | La SEP-2577 retire la capacité. Rien dans le protocole ne la remplace. | Un `import logging` ordinaire vers stderr (voir **[Journalisation](handlers/logging.md)**). |
| **`ping`** : `client.send_ping()` | **Supprimé** du protocole, pas simplement obsolète. Il n’y a pas de méthode `ping` en version 2026-07-28. | Rien. Cela ne fonctionne que sur une connexion `mode="legacy"`. |
| **Progression client->serveur** : `client.send_progress_notification()` | La version 2026-07-28 réserve la progression au sens serveur->client. | Rien à envoyer. Votre *serveur* signale sa progression avec `ctx.report_progress()` (voir **[Progression](handlers/progress.md)**). |
Trois choses ressortent de ce tableau :
* Les racines, l’échantillonnage et la journalisation vont ensemble. Une seule proposition, la **SEP-2577**, rend les trois capacités obsolètes d’un coup.
* L’échantillonnage et les racines partagent un problème plus profond : ce sont des endroits où un **serveur** envoie une **requête** au **client**. C’est toute cette direction que la version 2026-07-28 remplace par les **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)**. Ce sont les méthodes RPC autonomes (`sampling/createMessage`, `roots/list` et `elicitation/create` en mode push) qui disparaissent ; les types de charge utile `CreateMessageRequest` / `ListRootsRequest` / `ElicitRequest` survivent, intégrés dans `InputRequiredResult.input_requests`, et côté client ils aboutissent aux mêmes fonctions de rappel (callbacks).
* `ping` est l’exception. Le protocole ne le rend pas obsolète, il le supprime. La méthode du SDK avertit quand même (son message dit *removed*, pas *deprecated*) et l’appeler sur une connexion moderne répond par *« Method not found »*.
## L’obsolescence est indicative {#deprecated-is-advisory}
Rien ne casse aujourd’hui.
Chaque méthode ci-dessus continue de fonctionner sur toute session qui a négocié la version **2025-11-25 ou antérieure**. Fixez `mode="legacy"` sur le client et vous obtenez exactement le comportement d’avant 2026. Il n’y a aucun changement sur la liaison et la négociation des capacités est inchangée.
Ce qui change, c’est que vous obtenez un avertissement visible la première fois que chacune s’exécute :
```text
MCPDeprecationWarning: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
`MCPDeprecationWarning` hérite de `UserWarning`, **pas** de `DeprecationWarning`. C’est délibéré : le filtre par défaut de Python n’affiche `DeprecationWarning` que dans le code exécuté directement en tant que `__main__`, ce qui explique que les bibliothèques rendent des choses obsolètes sans que personne ne le remarque pendant deux ans. Celui-ci apparaît partout, sans option `-W`.
!!! warning
« Indicatif » s’arrête à la liaison. L’échantillonnage et les racines sont des *requêtes*
du serveur vers le client, et une session 2026-07-28 n’a aucun canal pour en transporter
une. Appelez `ctx.session.create_message()` dans un outil sur une connexion moderne :
l’avertissement se déclenche quand même, puis l’envoi échoue avec une erreur :
```text
Cannot send 'sampling/createMessage': this transport context has no back-channel
for server-initiated requests.
```
Deux signaux, dans cet ordre. Le `MCPDeprecationWarning` se déclenche dès que vous
appelez la méthode, sur n’importe quelle connexion. L’erreur est ce qui revient quand le
SDK tente ensuite l’envoi. Ces deux fonctionnalités ne marchent de bout en bout que sur
une connexion `mode="legacy"` dont le client a enregistré la fonction de rappel
correspondante.
## Faire taire l’avertissement {#silencing-the-warning}
Dans du nouveau code, ne le faites pas.
Mais un serveur que vous maintenez et qui sert réellement des clients d’avant 2026 a parfaitement droit à un journal silencieux. Filtrez la catégorie avant l’exécution du premier appel obsolète :
```python
import warnings
from mcp import MCPDeprecationWarning
warnings.filterwarnings("ignore", category=MCPDeprecationWarning)
```
C’est toute l’API. Il n’y a pas d’interrupteur par méthode, et vous n’en voulez pas : l’intérêt d’une catégorie unique, c’est qu’une ligne la fait taire et qu’une ligne la rétablit.
!!! check
Inversez le filtre et vous obtenez gratuitement un test de non-régression. Ajoutez
`"error::mcp.MCPDeprecationWarning"` au réglage `filterwarnings` de votre configuration
pytest et l’appel obsolète **lève une exception** au lieu d’avertir. Un outil nommé
`old_log` qui appelle encore `ctx.info()` cesse de passer et se met à signaler :
```text
Error executing tool old_log: The logging capability is deprecated as of 2026-07-28 (SEP-2577).
```
Une ligne de configuration pytest, et un appel obsolète ne peut plus jamais se glisser
de nouveau dans votre base de code sans faire échouer un test.
## Récapitulatif {#recap}
* La spécification 2026-07-28 rend obsolètes les **racines**, l’**échantillonnage** à l’initiative du serveur et la **journalisation** par le protocole (toutes via la [SEP-2577](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2577)), restreint la **progression** au sens serveur vers client et supprime **`ping`**.
* La colonne des remplacements vous oriente : **[Requêtes à plusieurs allers-retours](handlers/multi-round-trip.md)** pour l’échantillonnage et les racines, **[Journalisation](handlers/logging.md)** pour la journalisation, **[Progression](handlers/progress.md)** pour la progression. `ping` n’a besoin de rien du tout.
* L’obsolescence est indicative : aucun changement sur la liaison, tout continue de fonctionner sur les sessions d’avant 2026, et vous obtenez un `MCPDeprecationWarning` visible (un `UserWarning`, donc actif par défaut).
* L’échantillonnage et les racines ont en plus besoin d’un canal de retour (back-channel) qu’une session 2026-07-28 n’a pas. Sur une connexion moderne, ils avertissent puis lèvent une exception.
* `warnings.filterwarnings("ignore", category=MCPDeprecationWarning)` fait taire toute la catégorie ; `"error::mcp.MCPDeprecationWarning"` dans pytest la transforme en échec de test.
* Aucun nouveau code ne devrait s’appuyer sur l’une de ces fonctionnalités.
Toutes les autres pages de cette documentation enseignent l’API actuelle.

View file

@ -0,0 +1,144 @@
---
translation:
sections: [0d6c05bcbf836bf3, 59a7b14eeefc68c1, 7114d8d6daba203f, e8bbb56a98ba7bc9, 5138010f6159901c, f78da7c7c363d4c6, 220a939cab348686]
tool: 1
---
# Premiers pas {#first-steps}
La **[page d’accueil](../index.md)** va vite : écrire un serveur, l’exécuter, appeler un outil.
Cette page prend son temps, avec les trois choses qu’un serveur peut exposer, et un nom pour chaque notion rencontrée en chemin.
## Hôte, client et serveur {#host-client-and-server}
Trois mots que vous verrez sur chaque page à partir d’ici :
* Un **hôte** est l’application LLM : Claude, un IDE, un environnement d’exécution d’agents. C’est ce à quoi l’utilisateur parle.
* Un **client** vit à l’intérieur de l’hôte et parle MCP. L’hôte exécute un client par serveur auquel il est connecté.
* Un **serveur** est ce que vous construisez avec ce SDK. Il expose des choses aux clients. Il ne parle jamais directement au modèle.
Vous écrivez le serveur. Les hôtes sont le produit de quelqu’un d’autre. Le SDK vous fournit aussi un `Client`. Vous l’utiliserez pour tester vos serveurs, et il apparaît plus loin sur cette page.
## Les trois primitives {#the-three-primitives}
Un serveur expose exactement trois sortes de choses. Ce qui les distingue, c’est **qui décide de les utiliser** :
| Primitive | Contrôlée par | Ce que c’est | Exemple |
|----------------|-----------------|----------------------------------------------------------------------|------------------------------------------------|
| **Outils** | Le modèle | Une fonction que le modèle appelle pour agir | Un appel d’API, une écriture en base de données |
| **Ressources** | L’application | Des données que l’hôte charge dans le contexte du modèle | Le contenu d’un fichier, une réponse d’API |
| **Prompts** | L’utilisateur | Un modèle de message réutilisable que l’utilisateur invoque par son nom | Une commande slash, une entrée de menu |
« Contrôlée par » est tout l’intérêt de la distinction. Un outil s’exécute parce que le **modèle** a décidé de l’appeler. Une ressource est jointe parce que l’**application** a décidé que le modèle en avait besoin. Un prompt s’exécute parce que l’**utilisateur** l’a choisi.
!!! info
Si vous avez déjà construit une API web, vous avez l’essentiel de l’intuition : une **ressource** est un `GET`
(elle charge des données et ne modifie rien) et un **outil** est un `POST` (il effectue un travail et peut avoir
des effets de bord). Un **prompt** n’a pas d’équivalent HTTP ; il se rapproche d’une requête enregistrée que
l’utilisateur exécute par son nom.
## Un serveur, les trois à la fois {#one-server-all-three}
```python title="server.py" hl_lines="6 12 18"
--8<-- "docs_src/first_steps/tutorial001.py"
```
Trois fonctions ordinaires, trois décorateurs. Chaque décorateur constitue à lui seul tout l’enregistrement :
* `@mcp.tool()` fait de `add` un **outil**.
* `@mcp.resource("greeting://{name}")` fait de `greeting` un **modèle de ressource** (resource template) : le `{name}` dans l’URI est le paramètre de la fonction.
* `@mcp.prompt()` fait de `summarize` un **prompt**. La chaîne qu’il renvoie devient un message utilisateur.
Tout le reste (le nom, la description, le schéma des arguments), le SDK le lit dans la fonction elle-même : son nom, sa docstring, ses annotations de type. Vous n’avez rien déclaré de tout cela séparément.
!!! tip
Les deux moitiés du SDK ont deux chemins d’import : `from mcp import Client` et
`from mcp.server import MCPServer`. Il n’existe pas de `from mcp import MCPServer`.
### Essayer {#try-it}
Lancez-le avec le MCP Inspector :
```console
uv run mcp dev server.py
```
Ouvrez l’URL qu’il affiche. L’Inspector a un onglet par primitive ; parcourez-les dans l’ordre.
**Tools.** Une entrée : `add`, décrite comme *Add two numbers.* Le formulaire comporte un champ entier obligatoire pour `a` et un autre pour `b`. Remplissez-les, lancez l’appel, et le résultat est `3`. L’Inspector a construit ce formulaire à partir de `a: int, b: int`. Tous les autres clients font de même.
**Resources.** La liste *Resources* est vide. `greeting` se trouve sous **Resource Templates**, parce que `greeting://{name}` a un paramètre : il n’y a aucune ressource unique à lister tant que personne n’a fourni de `name`. Donnez-lui `World` et lisez-la :
```text
Hello, World!
```
**Prompts.** Une entrée : `summarize`, avec un seul argument obligatoire, `text`. Récupérez-le avec un peu de texte et vous recevez un message avec `role: user` et votre chaîne rendue comme contenu. Un prompt n’est rien d’autre que cela : une fonction qui construit des messages.
L’Inspector a exécuté votre serveur via **stdio**, l’un des transports qu’un serveur MCP peut parler. Vous n’en choisissez pas encore un ; **[Exécuter votre serveur](../run/index.md)** est la page consacrée à ce sujet.
## Capacités {#capabilities}
Vous avez vu trois onglets dans l’Inspector. Comment savait-il qu’il y en avait trois ?
Lorsqu’un client se connecte, le serveur déclare ses **capacités** (capabilities) : les familles de requêtes auxquelles il répondra. Le client utilise cette déclaration pour décider de ce qu’il peut même demander. Vous ne l’avez jamais écrite ; `MCPServer` la déclare pour vous.
Regardez par vous-même. Le `Client` du SDK accepte directement l’objet serveur et s’y connecte **en mémoire** (ni sous-processus, ni port) :
```python
import asyncio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
print(client.server_capabilities.model_dump(exclude_none=True))
asyncio.run(main())
```
```text
{'prompts': {'list_changed': True}, 'resources': {'subscribe': True, 'list_changed': True}, 'tools': {'list_changed': True}}
```
Ce dictionnaire, ce sont les **capacités** déclarées de votre serveur. C’est la première chose qu’apprend chaque client qui se connecte :
| Capacité | Le client peut désormais appeler |
|-------------|---------------------------------------------------------------|
| `tools` | `tools/list`, `tools/call` |
| `resources` | `resources/list`, `resources/templates/list`, `resources/read` |
| `prompts` | `prompts/list`, `prompts/get` |
`MCPServer` sert les trois primitives, donc les trois sont toujours déclarées.
Remarquez ce qui n’y figure pas. `completions` (la complétion automatique des arguments pour les modèles de ressources et les prompts) nécessite un gestionnaire que vous écrivez ; ce serveur n’en a pas, donc la capacité est absente et un client bien élevé ne demandera rien. C’est la règle pour tout ce qui est facultatif : enregistrez la chose et la capacité apparaît ; **[Complétions](../servers/completions.md)** le prouve.
!!! info
`Client(mcp)` est le même client en mémoire avec lequel chaque exemple de cette documentation est testé, et
c’est ainsi que vous testerez les vôtres. Il a droit à une page entière : **[Tester](testing.md)**.
## Ce que vous n’avez pas écrit {#what-you-did-not-write}
Reprenez cette page depuis le début. Vous avez écrit trois petites fonctions Python. Vous n’avez **pas** écrit :
* De JSON Schema. `a: int, b: int` *est* le schéma de `add`.
* De gestionnaire de requêtes. `tools/list`, `resources/read`, `prompts/get` : tous servis pour vous.
* De déclaration de capacités. `MCPServer` l’a faite pour vous.
* Une seule ligne de protocole. La négociation de version, l’encapsulation JSON-RPC, l’échange de capacités : tout cela s’est passé à l’intérieur de `mcp dev` et de `Client(mcp)`, et vous n’en avez rien vu.
Ce rapport est tout l’intérêt du SDK.
## Récapitulatif {#recap}
* Un **hôte** est l’application LLM, un **client** est sa moitié qui parle MCP, un **serveur** est ce que vous construisez.
* Les outils sont contrôlés par le **modèle**, les ressources par l’**application**, les prompts par l’**utilisateur**.
* Un décorateur par primitive : `@mcp.tool()`, `@mcp.resource(uri)`, `@mcp.prompt()`. Le nom, la description et le schéma viennent de la fonction.
* Un URI avec un `{param}` crée un **modèle** de ressource, listé séparément des ressources concrètes.
* Les **capacités** du serveur sont déclarées pour vous, et un client ne demande que ce qu’un serveur déclare.
* `Client(mcp)` se connecte à l’objet serveur en mémoire : votre banc d’essai dès le premier jour.
La suite, c’est **[Se connecter à un vrai hôte](real-host.md)** : ce serveur dans Claude Desktop ou un IDE, pour de vrai. Puis **[Tester](testing.md)** : une page, un client en mémoire, et vous n’aurez plus jamais à deviner si cela fonctionne. Ensuite, chaque primitive a droit à sa propre page, en commençant par celle que pilote le modèle : **[Outils](../servers/tools.md)**.

View file

@ -0,0 +1,57 @@
---
translation:
sections: [ed4a756b4c53c585, 97e2fb315b7fe398, 4d04f1c6f4bf6c1d, 577d73078fc62baf]
tool: 1
---
# Prise en main {#get-started}
Vous débutez avec MCP, ou avec ce SDK ? Commencez ici. Ces pages vous mènent de zéro à un
serveur fonctionnel et testé : [installez le SDK](installation.md), construisez votre
[premier serveur](first-steps.md), [connectez-le à un hôte réel](real-host.md) et
[testez-le](testing.md) avec un client en mémoire.
## Exécuter le code {#run-the-code}
Tous les blocs de code peuvent être copiés et utilisés tels quels : ce sont des fichiers complets et fonctionnels.
Pour suivre, collez un bloc dans un fichier `server.py` et ouvrez-le dans le MCP Inspector :
```console
uv run mcp dev server.py
```
Il est **FORTEMENT recommandé** d’écrire (ou de copier) le code, de le modifier et de l’exécuter localement. C’est en l’utilisant dans votre propre éditeur que vous en saisirez vraiment l’intérêt : le peu de code à écrire, l’autocomplétion, les vérifications de type qui détectent les erreurs avant même que vous n’exécutiez quoi que ce soit.
## Vous n’aurez pas à deviner {#you-will-not-be-guessing}
Chaque exemple de cette documentation est un fichier complet sous [`docs_src/`](https://github.com/modelcontextprotocol/python-sdk/tree/main/docs_src) dans le dépôt du SDK lui-même, et chacun d’eux est exécuté par la suite de tests du SDK via un **client en mémoire** :
```python
import pytest
from mcp import Client
from server import mcp
@pytest.mark.anyio
async def test_add() -> None:
async with Client(mcp) as client:
result = await client.call_tool("add", {"a": 1, "b": 2})
assert result.structured_content == {"result": 3}
```
Aucun sous-processus, aucun port, aucun transport. `Client(mcp)` se connecte directement à l’objet serveur.
Si une modification du SDK casse un exemple de l’une de ces pages, la CI passe au rouge avant la page. Le code que vous lisez ici est le code qui s’exécute.
Vous l’utiliserez vous-même dans [Tester](testing.md) ; c’est aussi ainsi que vous testez vos propres serveurs.
## Où aller ensuite {#where-to-go-next}
Une fois qu’un serveur tourne, le reste de cette documentation est une référence, pas un cours.
Chaque page se suffit à elle-même, alors allez directement à ce dont vous avez besoin :
* Ce qu’un serveur expose (outils, ressources, prompts), c’est **[Serveurs](../servers/index.md)**.
* Ce qui est disponible dans les fonctions que vous enregistrez, c’est **[Dans votre gestionnaire](../handlers/index.md)**.
* Le mettre à disposition des clients (stdio, HTTP, votre application FastAPI existante), c’est **[Exécuter votre serveur](../run/index.md)**.
* Construire l’autre côté, une application qui *utilise* des serveurs MCP, c’est **[Clients](../client/index.md)**.

Some files were not shown because too many files have changed in this diff Show more