FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

SEP-2792: Per-Request User Language Preference by SamMorrowDrums · Pull Request #2792 · modelcontextprotocol/modelcontextprotocol · GitHub

SEP-2792: Per-Request User Language Preference - #2792

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

SEP-2792: Per-Request User Language Preference#2792
SamMorrowDrums wants to merge 29 commits into
modelcontextprotocol:mainfrom
SamMorrowDrums:sammorrowdrums/sep-i18n-language-negotiation

Conversation

SamMorrowDrums commented May 26, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Summary

SEP-2792 defines transport-neutral propagation of a user's natural-language preference on each MCP request:

  • params._meta["io.modelcontextprotocol/acceptLanguage"] reuses the HTTP Accept-Language grammar and semantics, including weighted language ranges and wildcard matching under RFC 4647.
  • Response _meta["io.modelcontextprotocol/contentLanguage"] reports the language actually used with Content-Language semantics, including server-default fallback.
  • The preference is request-scoped: clients can change it on any request, and servers cannot bind or reuse it as connection or session state.
  • This is language preference only, not a locale, timezone, currency, calendar, measurement, formatting, or translation-infrastructure system.

Core behavior

Clients with a known applicable preference should send it on every request. Servers may ignore it; participating servers use RFC-compatible matching, fall back without error when no language matches, and report the actual selected language.

The display-oriented translation guidance is intentionally narrow: BaseMetadata.title on current title-bearing types, ToolAnnotations.title, elicitation messages and form property titles/descriptions, and progress messages are in the SHOULD category. Error.message and other natural-language/model-facing/returned content are MAY, with an explicit warning that translation can change model behavior. Machine-interpreted names, methods, URIs, schema keys, enum/const values, discriminators, protocol identifiers, error codes, _meta keys, and opaque tokens remain stable.

Explicit method or tool language arguments SHOULD take precedence for the content they control; the negotiated preference remains fallback context and MAY provide a fallback when the explicit argument is absent. It is available before discovery or tool selection.

HTTP and caching

_meta is canonical across transports. Streamable HTTP clients mirror the preference to Accept-Language; JSON responses mirror the selected language to Content-Language. SSE and notifications retain per-message _meta reporting.

Mirror agreement is exact equality of the field values, not semantic Accept-Language equivalence and not raw wire bytes. The SEP spells out the comparison: take the decoded _meta string unnormalized, take the HTTP field value after normal field parsing (RFC 9110 §5.5), join repeated field lines in received order with comma SP (RFC 9110 §5.3), then compare character for character. Tag casing, comma spacing, range order, and q-value spelling are therefore significant. A present mismatch is rejected with 400 Bad Request and HeaderMismatch (-32020), while a stripped request header can be tolerated — the asymmetry agreed in review. Intermediaries must preserve a present mirrored field exactly or remove it.

Language-dependent responses remain cacheable. MCP/client caches include acceptLanguage in the cache key when representations vary, existing ttlMs/cacheScope semantics remain intact, and HTTP variants use Vary: Accept-Language. Shared caches that cannot observe the language signal must use a visible equivalent key or keep the response private.

Examples

The proposal leads with localized tools/list discovery: stable tool name, localized display title, and no model or tool argument involvement. It follows with localized elicitation UI using a stable form field key, then shows an explicit language argument overriding the fallback preference for an official document edition.

Transport WG review

The Transport WG proposal is modelcontextprotocol/transports-wg#42. The vote passed with 3 Accept and 3 Accept with Changes. The requested changes were:

  1. Better examples showing why a tool-call argument cannot replace the mechanism.
  2. Clear justification for core specification semantics rather than an extension.

Both are addressed in this revision through discovery/elicitation examples and dedicated rationale sections.

Reference implementation and prior art

AI disclosure

This SEP was authored with assistance from GitHub Copilot.

Copy link
Copy Markdown
Contributor Author

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).

  1. Fully opt-in on both sides. Clients MAY send acceptLanguage; servers MAY ignore it entirely, with no capability flag or handshake change. Nothing in the SEP requires any existing implementation to change.
  2. No reinvention. The motivation leans hard on the fact that BCP 47, RFC 4647 matching, Accept-Language / Content-Language, CDN Vary behavior, and every framework-level i18n library (Intl.LocaleMatcher, golang.org/x/text/language, ICU, Babel, gettext, framework localization modules) already exist and are battle-tested. The SEP standardizes only the carrier; the value is the exact HTTP Accept-Language syntax verbatim, so it goes straight into existing matchers.
  3. Addresses the docs(spec): add internationalization guidance for Streamable HTTP #2355 pushback head-on.
    • @pja-ant on transport parity: language is defined as a transport-agnostic _meta field first. stdio and any future transport get full i18n with zero transport changes. HTTP gets the standard headers as a strict mirror, not the source of truth.
    • @kurtisvg on "put it in _meta and mirror to the header like we're doing for some other values": that is exactly the model, applying the SEP-2243 header-mirroring pattern and reusing its strict-mismatch rule verbatim.
  4. Per-request, not handshake-bound. Citing SEP-2575: the field is sent on every request, so language can change mid-conversation without renegotiation. This avoids reintroducing the kind of session state SEP-2575 is removing, and supports the casual agent-loop notion of a "session" where the user might switch UI language between turns.

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 _meta helpers (wrapping @formatjs/intl-localematcher, no bespoke matcher), Streamable HTTP header/_meta mirroring with the strict-mismatch rule, stdio pass-through, an en/fr/de example server + client across both transports, and unit + integration tests, including the per-request mid-session language switch proof on stdio. All builds, typechecks, lints and tests pass.

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 <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums force-pushed the sammorrowdrums/sep-i18n-language-negotiation branch from ead3ab8 to 958a569 Compare May 27, 2026 08:00
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

pja-ant left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

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.)
  • contentLanguage on errors has nowhere to live. Error is { code, message, data? } — no _meta — and your own illustrative schema only adds _meta to Result. Pick a home (add _meta to Error, or use error.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 <223556219+Copilot@users.noreply.github.com>

SamMorrowDrums commented May 27, 2026
edited
Loading

Copy link
Copy Markdown
Contributor Author

Feedback applied @pja-ant - thank you for the thorough review.

Changes in b5667477:

  • Dropped the Batching section. You're right, batching is gone from the spec and the union-of-ranges MUST described a forbidden wire shape.
  • Localized errors have a home. Added an Error responses subsection using error.data._meta['io.modelcontextprotocol/contentLanguage'] (the JSON-RPC data extension point, so no schema change to Error), with an example and the mirror rule for the Content-Language HTTP response header.
  • Named the mismatch code explicitly: -32001 HeaderMismatch throughout Specification, Rationale, and Security Implications.
  • Reframed the SEP-2243 relationship as extending the rule and the error code to the standard Accept-Language / Content-Language surface, with a corresponding line in Security Implications (the broader intermediary surface is exactly what makes the extension worth calling out).
  • Added a Conformance section per SEP-2484 outlining six scenarios required for Final, including header/body mismatch returning -32001, error-response localization, and per-request switching on stdio.

The TS SDK reference implementation (modelcontextprotocol/typescript-sdk#2158) is being updated in lockstep: switching the placeholder SdkErrorCode.SendFailed to -32001 HeaderMismatch, implementing error.data._meta localization with HTTP Content-Language mirroring on errors, and demonstrating both in the example server/client with tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
pja-ant changed the title SEP: Internationalization via Per-Request Language Negotiation SEP-2792: Internationalization via Per-Request Language Negotiation May 28, 2026
Comment thread seps/2792-i18n-language-negotiation.md Outdated
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 <223556219+Copilot@users.noreply.github.com>

Copy link
Copy Markdown
Contributor Author

Feedback applied @pja-ant - and you're right, this is much cleaner.

63dc3e1a drops the strict header/body mismatch rule for Accept-Language / Content-Language entirely. The new model:

  • _meta is canonical; if present, the server uses it and ignores the header.
  • HTTP headers are a best-effort mirror (SHOULD, not MUST), intended as a hint for caches/CDNs/observability, with the explicit acknowledgement that intermediaries may strip, normalize, or rewrite them. CloudFront's default behaviour and Fastly's accept.language_lookup / Varnish's vmod_accept are called out by name in the rationale.
  • No reject path, no -32001, no header/body equality contract to violate (which conveniently also sidesteps RFC 9110's lack of a canonical serialization for Accept-Language).
  • Security Implications now treats intermediary header rewriting as expected behaviour rather than as an attack.
  • Conformance scenario 4 (mismatch returns 400) is replaced with a scenario asserting that a stripped/rewritten Accept-Language MUST NOT cause rejection.
  • Reference-implementation summary updated to drop mismatch claims; the TS SDK PoC (SEP-2792: Reference implementation for per-request language negotiation typescript-sdk#2158) is being updated in lockstep (removing the server reject branch, the client throw, and the -32001 constant; adding a stripped-header positive test).

This also sidesteps the -32001 vs SDK conventions discussion entirely, since we no longer need a code at all.

SamMorrowDrums and others added 2 commits May 29, 2026 16:12
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums requested a review from pja-ant May 29, 2026 14:38
SamMorrowDrums added the draft SEP proposal with a sponsor. label May 29, 2026
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread seps/2792-i18n-language-negotiation.md Outdated
SamMorrowDrums and others added 6 commits June 1, 2026 23:01
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 <223556219+Copilot@users.noreply.github.com>
- 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 <223556219+Copilot@users.noreply.github.com>
The MUST NOT rule lives in the Specification section that immediately
follows; the cross-reference adds no information.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The bullet list and coordination note restated the preceding paragraph.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
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 <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums and others added 2 commits June 1, 2026 23:32
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop the bold leading-phrases on 6 and 7 so all seven scenarios use
the same plain-bullet style.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
localden added the SEP label Jun 8, 2026
localden added proposal SEP proposal without a sponsor. and removed draft SEP proposal with a sponsor. labels Jun 8, 2026
SamMorrowDrums marked this pull request as draft June 17, 2026 20:34
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 <223556219+Copilot@users.noreply.github.com>
SamMorrowDrums and others added 3 commits June 29, 2026 15:15
- 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 <223556219+Copilot@users.noreply.github.com>
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".
SamMorrowDrums added draft SEP proposal with a sponsor. and removed draft SEP proposal with a sponsor. labels Jul 21, 2026
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
SamMorrowDrums changed the title SEP-2792: Internationalization via Per-Request Language Negotiation SEP-2792: Per-Request User Language Preference Aug 14, 2026
SamMorrowDrums and others added 4 commits August 14, 2026 19:47
Narrow SEP-2792 to request-scoped language preference propagation, precise display-field translation guidance, HTTP mirroring, and language-aware caching.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: f2236498-f7bf-43f1-9ef1-d54fd21665ae
SamMorrowDrums marked this pull request as ready for review August 14, 2026 22:03
SamMorrowDrums added in-review SEP proposal ready for review. transport Related to MCP transports and removed proposal SEP proposal without a sponsor. labels Aug 14, 2026
The simplified draft referred to "the transport's equality rule" without
saying what it was, so two conformant implementations could reach different
verdicts on the same request and the conformance scenario could not be
written. Define it.

Mirror agreement is exact equality of the two exposed field values, and is
distinct from RFC 4647 language matching, which stays semantic. Spell out the
comparison: take the decoded _meta string unnormalized, take the HTTP field
value after normal field parsing (RFC 9110 5.5), join repeated field lines in
received order with comma SP (RFC 9110 5.3), then compare character for
character with no parsing or re-serialization. Require an HTTP API that
exposes all field line values, and require senders to emit a single field
line.

Also make agreement evaluation order explicit relative to grammar validation,
extend the response mirror to error responses and SSE streams, note that a
present mirrored field must be preserved exactly or removed by
intermediaries, and add the exact-match conformance cases.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f2236498-f7bf-43f1-9ef1-d54fd21665ae
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in-review SEP proposal ready for review. SEP transport Related to MCP transports

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

5 participants


Back | FazBrowse Home | New Git URL