Proposal: i18n via per-request language negotiation#42
Conversation
Mirrors SEP-2792 (modelcontextprotocol#2792) into the Transports WG proposals/ directory for review of the transport-touching aspects: Streamable HTTP Accept-Language / Content-Language mirroring, byte-equality enforcement, Vary semantics, and per-request statelessness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| interface and that the human user reads. Typical user-facing fields | ||
| (non-exhaustive) include: | ||
|
|
||
| - `title` and `description` on tools, resources, prompts, and server |
There was a problem hiding this comment.
one of the pieces for feedback was more clarification on what should change.
Does this include descriptions for parameters in a tools schema for example?
There was a problem hiding this comment.
Good question — addressed in 254b291. The Scope section is rewritten as three explicit buckets (MUST / MAY / MUST NOT translate), anchored to the schema's own framing of each field.
To your specific example: yes, property description and title inside Tool.inputSchema / outputSchema are included, but explicitly as MAY, with a stated caveat that translating them changes what the model sees and can affect tool-selection and agent behaviour. The Codex implementation (codex-rs/tools/src/mcp_tool.rs, codex-rs/codex-mcp/src/rmcp_client.rs) wires these straight into the function definition sent to the model, which is the reason for the caveat rather than a blanket MUST.
Tool.description itself is in the same MAY bucket for the same reason — the schema describes it as "a hint to the model".
| - Error `message` strings intended for surfacing in a UI | ||
| - Notification messages (e.g. `logging/message`) that may be rendered to | ||
| the user | ||
| - Any other field the host explicitly displays to the user without |
There was a problem hiding this comment.
I think we should try to expictly call out which fields these are, as I don't know that there is concencus.
There was a problem hiding this comment.
Done in 254b291. The open-ended "any other field the host displays" bullet is gone. The replacement is an enumerated list across three buckets:
- MUST translate — fields the schema classifies as display-only (
BaseMetadata.titleon every type,ToolAnnotations.title, elicitation messages and form-schema propertytitle/description,ProgressNotification.message,JSONRPCError.message). - MAY translate, with caveat — model-facing or dual-purpose fields (
Tool.descriptionand the other "hint to the model" descriptions,Prompt/Implementationdescriptions,inputSchema/outputSchemapropertytitle/description, body content fromtools/call/resources/read/prompts/getincluding MCP Apps UI resource bodies,LoggingMessageNotification.datawhere contractually user-facing). - MUST NOT translate — machine-interpreted values (
name, URIs, MIME types, JSON Schema property keys, enum tokens, capability identifiers,_metakeys).
Mirrors changes from modelcontextprotocol#2792. Addresses @kurtisvg review comments on this PR: - Schema-property descriptions: explicit MAY with a caveat that translation alters what the model sees, with a concrete Codex reference instead of handwaving. - Open-ended "any other displayed field" bullet replaced with an enumerated MUST/MAY/MUST NOT list anchored to the spec's own classification of each field. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Resolve duplicate MUST-NOT-translate list (Scope is canonical). - Fix broken Scope anchor. - Resolve notifications open question into the Specification. - Make error.message localization choice explicit. - Update SEP-2243/2575 link targets to merged seps/ files. - Trim Reference Implementation paragraph. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the parallel localizedMessage field in favor of translating error.message directly. error.code remains the machine-interpreted identifier; error.message is the display string. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
kurtisvg
left a comment
There was a problem hiding this comment.
Overall, supportive of voting on this to bring to the CMs.
| that are intended for display in user interfaces. The specification today | ||
| provides no mechanism for a client to request these strings in a particular | ||
| language, or for a server that supports multiple languages to advertise which | ||
| one it returned. This forces ad-hoc solutions and discourages MCP servers | ||
| from investing in i18n at all. |
There was a problem hiding this comment.
Can we provide a concrete example of where i18n would be useful here? I don't work with models in other langauges, so I'm unclear how they handle different languages today.
Are there benefits to keeping some stuff as the same language?
| honoring `acceptLanguage` **MUST** localize them when a localized form | ||
| is available. |
There was a problem hiding this comment.
nit: this looks like maybe a "SHOULD" -- as "when a localized form is available seems to be an example where it wouldn't localize it.
| must reconfigure them to leave `Accept-Language` either verbatim or | ||
| absent. | ||
|
|
||
| ### Why `-32005` rather than `-32001` |
There was a problem hiding this comment.
There was another PR that changed these error codes to 32020+, we should make sure this aligns with that.
| - A server that selected a language in response to `acceptLanguage`, or that | ||
| is aware of the language of the user-facing content it returned, **MUST** | ||
| include `io.modelcontextprotocol/contentLanguage` in `result._meta` on a | ||
| successful response, or in `error.data._meta` on an error response (see | ||
| [Error responses](#error-responses) below). |
There was a problem hiding this comment.
I think this should by limited to "selected alanguage in response to acceptLanguage" -- the "if a server is aware" it a bit unclear what that means. I think if it was sent, the server is aware.
Bringing this to the Transports WG for review and comment before bringing to the core maintainers.
Upstream SEP: modelcontextprotocol/modelcontextprotocol#2792 (now draft, pending WG input).
Reference implementation: modelcontextprotocol/typescript-sdk#2158 — full client + server, stdio and Streamable HTTP, byte-equality enforcement, 11-case integration matrix.
What this proposes
A single, transport-agnostic way for clients to express language preference per request, and for servers to indicate the language used per response:
params._meta['io.modelcontextprotocol/acceptLanguage']on requestsresult._meta['io.modelcontextprotocol/contentLanguage'](anderror.data._meta[...]) on responsesAccept-Language/Content-Languagestrings (BCP 47 language-range lists with quality values), so no new grammar.Why this is a transports concern
For Streamable HTTP, the proposal mirrors
_metainto the standardAccept-Language/Content-LanguageHTTP headers:Mcp-Method/Mcp-Name(RPC content surfaced as headers for infra observability and routing)._metawhen both are present, rejected via a newHeaderMismatchJSON-RPC error (provisional-32005, HTTP 400). Header absence is tolerated (CDNs stripAccept-Languageby default);_metais the authoritative carrier.Vary: Accept-Languageis MUST on cacheable responses whose body depends on negotiated language._metais still authoritative.For stdio: same
_metafields, no transport changes.Why per-request, not session
Aligns with the recent stateless-by-default direction: language can change mid-conversation (user retries in another language, agent serves multiple end-users) without renegotiation. There is no
initialize-time language state.