Skip to content

Proposal: i18n via per-request language negotiation#42

Open
SamMorrowDrums wants to merge 4 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:sammorrowdrums/i18n-language-negotiation
Open

Proposal: i18n via per-request language negotiation#42
SamMorrowDrums wants to merge 4 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:sammorrowdrums/i18n-language-negotiation

Conversation

@SamMorrowDrums

@SamMorrowDrums SamMorrowDrums commented Jun 17, 2026

Copy link
Copy Markdown

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 requests
  • result._meta['io.modelcontextprotocol/contentLanguage'] (and error.data._meta[...]) on responses
  • Values are RFC 9110 Accept-Language / Content-Language strings (BCP 47 language-range lists with quality values), so no new grammar.

Why this is a transports concern

For Streamable HTTP, the proposal mirrors _meta into the standard Accept-Language / Content-Language HTTP headers:

  • Following the precedent set by SEP-2243 for Mcp-Method / Mcp-Name (RPC content surfaced as headers for infra observability and routing).
  • With strict byte-equality between header and _meta when both are present, rejected via a new HeaderMismatch JSON-RPC error (provisional -32005, HTTP 400). Header absence is tolerated (CDNs strip Accept-Language by default); _meta is the authoritative carrier.
  • Vary: Accept-Language is MUST on cacheable responses whose body depends on negotiated language.
  • SSE responses MAY omit the response header (headers flushed before body is known); _meta is still authoritative.

For stdio: same _meta fields, 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.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should try to expictly call out which fields these are, as I don't know that there is concencus.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.title on every type, ToolAnnotations.title, elicitation messages and form-schema property title/description, ProgressNotification.message, JSONRPCError.message).
  • MAY translate, with caveat — model-facing or dual-purpose fields (Tool.description and the other "hint to the model" descriptions, Prompt/Implementation descriptions, inputSchema/outputSchema property title/description, body content from tools/call / resources/read / prompts/get including MCP Apps UI resource bodies, LoggingMessageNotification.data where contractually user-facing).
  • MUST NOT translate — machine-interpreted values (name, URIs, MIME types, JSON Schema property keys, enum tokens, capability identifiers, _meta keys).

Copilot AI added 3 commits June 25, 2026 18:46
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 kurtisvg left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, supportive of voting on this to bring to the CMs.

Comment on lines +31 to +35
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.

@kurtisvg kurtisvg Jul 2, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines +108 to +109
honoring `acceptLanguage` **MUST** localize them when a localized form
is available.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was another PR that changed these error codes to 32020+, we should make sure this aligns with that.

Comment on lines +221 to +225
- 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).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants