SEP-2792: Internationalization via Per-Request Language Negotiation#2792
Conversation
|
Hi @kurtisvg, @pja-ant, tagging you both directly given your feedback on #2355, which is the seed for this SEP (indeed I believe we discussed at MCP Dev Summit that I would look at converting this to a SEP in order to include the metadata for all transports).
Scope is intentionally narrow: user-facing strings (titles, descriptions, UI-bound errors, user-visible notifications) are the primary target; servers MAY also translate body content. Reference implementation is up as a draft on the TypeScript SDK: modelcontextprotocol/typescript-sdk#2158. It includes the core Would either of you be willing to sponsor? |
Adds a transport-agnostic, fully opt-in i18n mechanism for MCP using _meta['io.modelcontextprotocol/acceptLanguage'] on requests and _meta['io.modelcontextprotocol/contentLanguage'] on responses, mirrored into the standard HTTP Accept-Language / Content-Language headers on the Streamable HTTP transport with a strict-mismatch rule consistent with SEP-2243. Per-request scope (no handshake-bound state) aligns with SEP-2575 and supports mid-conversation language switching. Reuses BCP 47, RFC 4647 language-range matching, and existing ecosystem libraries verbatim, no bespoke matcher or schema. Supersedes modelcontextprotocol#2355. Proposes subsuming the locale aspect of SEP-1809. Reference implementation: modelcontextprotocol/typescript-sdk#2158 (en/fr/de server + client, stdio and Streamable HTTP, unit and integration tests including mid-session language switch on stdio). Co-authored-by: Copilot <[email protected]>
ead3ab8 to
958a569
Compare
Co-authored-by: Copilot <[email protected]>
pja-ant
left a comment
There was a problem hiding this comment.
Clean, well-scoped, and I think the shape is right: per-request language in _meta, mirrored to the HTTP headers, no session state. Lines up with SEP-2243 and SEP-2575, reuses BCP 47 / Accept-Language rather than inventing anything. No objections to the design — and the scary-sounding mismatch-rule concerns don't hold up, since the 400 only fires when both the header and body _meta are present and disagree (and _meta isn't intermediary-rewritable).
Two things to fix first:
- Drop the Batching section. We removed JSON-RPC batching — one message per POST, no batch type in the schema — so the "union of language ranges" MUST describes a wire shape the spec forbids. (And a union header would mismatch every per-message
_meta, tripping the very 400 it's trying to avoid.) contentLanguageon errors has nowhere to live.Erroris{ code, message, data? }— no_meta— and your own illustrative schema only adds_metatoResult. Pick a home (add_metatoError, or useerror.data) and spell it out, since localized errors are a primary use case.
Minor: name the mismatch code explicitly (-32001 HeaderMismatch, now in the draft spec — the TS impl drifted to SendFailed); and "reused unchanged" undersells it — you're extending the 2243 rule to a standard header, worth a line in Security Implications.
Structure's all there, file/docs are right. Just needs a sponsor and (for Final) a conformance scenario per SEP-2484.
- Drop Batching section (JSON-RPC batching is removed from MCP) - Define error-response localization via error.data._meta - Name mismatch error explicitly as -32001 HeaderMismatch - Reframe SEP-2243 relationship as extending its rule and error code - Add SEP-2484 conformance scenario outline Co-authored-by: Copilot <[email protected]>
|
Feedback applied @pja-ant - thank you for the thorough review. Changes in b5667477:
The TS SDK reference implementation (modelcontextprotocol/typescript-sdk#2158) is being updated in lockstep: switching the placeholder |
Co-authored-by: Copilot <[email protected]>
Accept-Language/Content-Language are routinely stripped, normalized, or rewritten by intermediaries (CloudFront default behavior, Fastly accept.language_lookup, Varnish vmod_accept). RFC 9110 also lacks a canonical byte-equality form for Accept-Language. Applying SEP-2243's hard-fail rule to these headers would error out exactly the edge-i18n deployments we cite as motivating. - _meta is now canonical; headers are best-effort hints (SHOULD mirror) - Remove -32001 HeaderMismatch reject path entirely - Reframe Why-mirror rationale around expected intermediary rewriting - Replace mismatch-attack security note with header-tampering-expected - Replace conformance scenario 4 (reject mismatch) with one asserting that a stripped/rewritten Accept-Language MUST NOT cause rejection - Update reference-implementation summary to drop mismatch claims Co-authored-by: Copilot <[email protected]>
|
Feedback applied @pja-ant - and you're right, this is much cleaner.
This also sidesteps the -32001 vs SDK conventions discussion entirely, since we no longer need a code at all. |
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
The PoC implementation notes (build status, deferred mismatch rule rationale, SSE vs JSON header behavior) belong in the PR description, not in the SEP itself. Keep the SEP focused on the protocol design. Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
| - **Edge i18n services** that route requests to language-specific backends. | ||
| - **Observability tools** that segment usage by locale. | ||
|
|
||
| This SEP therefore says clients SHOULD mirror `_meta[acceptLanguage]` into |
There was a problem hiding this comment.
I think this should be clients MUST add the header, while servers MAY allow the request if the header is missing but MUST reject the request if the header is present and not matching.
Per WG feedback (Kurtis), revert the bare-header-tolerated design and restore strict byte-equality between _meta and the corresponding HTTP header on both request (Accept-Language) and response (Content-Language) sides, with header absence on requests tolerated for CDN compatibility. Use error code -32005 for HeaderMismatch instead of -32001, which is already in conflicting use across SDKs (REQUEST_TIMEOUT in Python and Kotlin vs HeaderMismatch in Go and C#); the code is provisional pending SEP-2243 / SEP-2678 / PR modelcontextprotocol#2642 schema-level reservation work. Add a Normalization footgun section covering Fastly, Varnish, CloudFront and reverse-proxy header rewriting; consolidate operator-config detail in that section so Security and Backward Compatibility just link to it. Co-authored-by: Copilot <[email protected]>
- Split the bundled 'header absent / bare header' bullet into two distinct rules so each cell of the (_meta-present, header-present) matrix has its own normative line. - Trim the Streamable HTTP intro to one opinionated paragraph. - Drop the trailing 'no new attack surface' platitude from Security. - Promote 'caches must Vary' from a reminder to a MUST. - Replace 'Tentative answer' hedging in Open Questions with explicit proposed resolutions. - Reference Implementation point 5 reads as a factual list rather than a results pitch. - Add [RFC 9111] reference link. Co-authored-by: Copilot <[email protected]>
The MUST NOT rule lives in the Specification section that immediately follows; the cross-reference adds no information. Co-authored-by: Copilot <[email protected]>
The bullet list and coordination note restated the preceding paragraph. Co-authored-by: Copilot <[email protected]>
The detailed bullet list duplicated the linked PR's description and will rot. Keep the SEP terse; the PR is the source of truth. Co-authored-by: Copilot <[email protected]>
The Vary requirement now lives normatively in the Response section. Security bullet shortened to a pointer. The Open Questions entry is resolved (MUST) and dropped. Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Drop the bold leading-phrases on 6 and 7 so all seven scenarios use the same plain-bullet style. Co-authored-by: Copilot <[email protected]>
Addresses @kurtisvg feedback on transports-wg#42: - Replaces the open-ended "any field the host displays" catch-all with an explicit, enumerated list anchored to the schema's own classification of each field (display-only vs "hint to the model"). - Makes the consequence of translating model-facing fields (Tool.description, inputSchema descriptions, etc.) explicit, with a concrete example from the Codex implementation, instead of handwaving. - Calls out MCP Apps UI resource bodies (SEP-1865) by name. - Defers schema property descriptions (dual-purpose model + human) to MAY with a stated caveat rather than a separate SEP. Co-authored-by: Copilot <[email protected]>
…8n-language-negotiation
- Resolve merge with main. - Drop duplicate (and now-stale) MUST NOT translate list from the acceptLanguage subsection; the Scope section is the single source of truth. - Fix broken anchor to the renamed Scope heading. - Resolve the open question on notifications into the Specification: notifications carrying user-facing text MUST set params._meta[contentLanguage]. Open Questions section removed. - Make the error.message localization choice explicit (server may localize message inline or carry the translated text in a structured error.data field). - Update SEP-2243 and SEP-2575 link targets from PRs to the merged seps/ files. - Trim the Reference Implementation paragraph. Co-authored-by: Copilot <[email protected]>
Bring PR modelcontextprotocol#2792 up to date with upstream main (163 commits) and replace seps/2792-i18n-language-negotiation.md with the Transports WG-reviewed authoritative source at SamMorrowDrums/transports-wg@c85169e (proposals/XXXX-i18n-language-negotiation.md). Highlights vs previous PR head (94cc51c): - HeaderMismatch error code is now the settled `-32020` (matches the reservation now live in schema/draft/schema.ts on main), replacing the provisional `-32005`. - Exact HTTP field-value comparison algorithm: comparison happens on the decoded JSON string vs. the parsed HTTP field-value (per RFC 9110 §5.5), so surrounding OWS is stripped and repeated field lines are joined with `, ` before byte-comparison. Explicit worked examples for OWS, case, q-value zero-padding, and multi-line fields added to Conformance. - Corrected RFC/citation semantics throughout (RFC 9110 §5.3, §5.5, §12.5.4, §12.5.5; RFC 9111 §4.1; RFC 5646 §2.1.1) and clarified language-matching reuse (server hands the value verbatim to any RFC 4647 matcher; no bespoke matching defined here). - error.message localized in place (no parallel field), with the contract that error.code remains the machine-interpreted identifier and clients MUST NOT branch on error.message text. - Notifications rule promoted from Open Questions into Specification. - Scope restated as three explicit MUST / MAY-with-caveat / MUST-NOT buckets, anchored to the schema's own field-level classification. Regenerated docs/seps/2792-i18n-language-negotiation.mdx and refreshed docs/seps/index.mdx and docs/docs.json via `npm run generate:seps`. `npm run check:seps` clean. Supersedes stash "sep-2792 error.message edit, pending WG outcome".
These four SEPs (1330, 1577, 2322, 2663) arrived from upstream/main with pre-existing prettier drift in their TypeScript code fences (missing leading `|` in union types). The Markdown Format Check CI job on this PR fails on them even though the changes are unrelated to SEP-2792. Apply `prettier --write` to satisfy CI; content is unchanged (union-type formatting only). No mdx regeneration needed — the rendered mdx already matches. Also verified: - npm run check:seps: clean - npm run check:docs:format: clean
Prettier 3.9.5 (pinned in package.json) formats these differently than 3.8.3 did (which was in this workspace's stale node_modules when the previous commit ran). CI runs `npm ci` so it uses 3.9.5 and rejects the un-reformatted files. Apply `npm run format:docs` so both md and mdx match the pinned prettier version. Verified: - npm run check:docs:format: clean - npm run generate:seps: clean - npm run check:seps: clean
Summary
A transport-agnostic i18n mechanism for MCP:
_meta['io.modelcontextprotocol/acceptLanguage']on a request, value matches the HTTPAccept-Languagefield syntax verbatim (BCP 47 ranges + quality values, per RFC 9110 §12.5.4)._meta['io.modelcontextprotocol/contentLanguage']on a response, one or more BCP 47 tags indicating the language(s) actually returned (per RFC 9110 §8.5).acceptLanguage; servers MAY ignore it. Nothing in this SEP requires any existing client or server to change behavior.Accept-Language/Content-Languageheaders using the payload/header agreement rule SEP-2243 established forMcp-Method/Mcp-Name. Comparison is against the parsed HTTP field-value (RFC 9110 §5.5, so surrounding OWS is stripped and repeated field lines are joined with,before comparison); the server tolerates header absence (CDN strip) but rejects on mismatch.Scope of translation
The SEP defines three explicit buckets, anchored to the schema's own field-level classification:
acceptLanguage:BaseMetadata.titleon every type that extends it,ToolAnnotations.title,ElicitRequestFormParams.message/ElicitRequestURLParams.message, elicitation form-schematitle/description,ProgressNotification.message,JSONRPCError.messagewhen intended for display.Tool.description,Resource.description,ResourceTemplate.description,Prompt.description,PromptArgument.description,Implementation.description, property-leveltitle/descriptioninsideTool.inputSchema/outputSchema, body content oftools/call/resources/read/prompts/get(including MCP Apps UI-resource bodies),LoggingMessageNotification.datawhen contractually user-facing.BaseMetadata.name, URIs and URI templates, MIME types, JSON Schema property keys, enum tokens, capability identifiers, errorcodevalues, method names,_metakeys.Motivation
Converts the docs-only proposal in #2355 into a cross-transport SEP, addressing reviewer feedback there:
_metafirst, with HTTP headers as a mirror.Motivating precedents
Accept-Language/Content-Language.initializeas a place for persistent negotiated state; per-request language preference is the natural fit.request.params._meta) establishes_metaas the carrier for per-request metadata.io.modelcontextprotocol/vendor prefix.Design choices worth flagging
,to form the field value. Comparison is then byte-equality on the decoded JSON string vs. that field value. This is unambiguous, matches what any RFC-compliant HTTP stack already yields, and stays a one-line check. Trade-off: operators using header-rewriting CDN features (Fastlyaccept.language_lookup, Varnishvmod_accept) need a one-time configuration change; a dedicated Normalization footgun section spells this out.Accept-Language. Rejecting on absence would lock out operators behind such a CDN even when the caller didn't intend to negotiate. Tolerating absence preserves the routing guarantee for callers that do supply the header and falls back to_metacleanly otherwise.-32020, settled, matching the schema-level reservation now onmain(schema/draft/schema.ts,HEADER_MISMATCH = -32020). Earlier revisions of this PR carried a provisional-32005pending SEP-2243, SEP-2678, and #2642 — that reservation is now in the schema and the SEP tracks it.error.messageis localized in place. JSON-RPC 2.0 defineserror.codeas the machine-interpreted identifier anderror.messageas the display string; a parallellocalizedMessagefield would just duplicate that. Clients MUST NOT branch onerror.messagetext; dispatch is oncode. When the server localizederror.message, it MUST also seterror.data._meta['io.modelcontextprotocol/contentLanguage'].Relationship to SEP-1809
SEP-1809 proposed a
clientContextobject ontools/callcarryingpreferredLanguagesand other client context. This SEP is deliberately narrower and orthogonal: it standardizes language negotiation only (nothing else about client context), covers every request (not justtools/call), and reuses IETF machinery unchanged. SEP-1809 can proceed independently for non-language client-context fields if the WG wants; the language field itself is subsumed here.Transports WG review
Cross-posted to and reviewed in modelcontextprotocol/transports-wg#42. All WG feedback (RFC-citation cleanup, exact-comparison algorithm,
-32020alignment,error.messagein-place localization) is folded into the head of this PR.Reference implementation
modelcontextprotocol/typescript-sdk#2158 (draft) exercises every conformance scenario in the SEP over both Streamable HTTP and stdio, including the exact-field-value comparison edge cases.
AI disclosure
This SEP was authored with assistance from GitHub Copilot.