SEP-2792: Reference implementation for per-request language negotiation - #2158
SEP-2792: Reference implementation for per-request language negotiation#2158SamMorrowDrums wants to merge 11 commits into
Conversation
…tiation Implements internationalization via per-request language negotiation as specified in SEP-2792. Changes include: SDK changes: - Add i18n helper module (packages/core/src/shared/i18n.ts) with: - ACCEPT_LANGUAGE_META / CONTENT_LANGUAGE_META constants - getAcceptLanguage / setAcceptLanguage helpers for request _meta - getContentLanguage / setContentLanguage helpers for response _meta - negotiateLanguage() using @formatjs/intl-localematcher (RFC 4647) - Client Streamable HTTP transport: mirrors _meta acceptLanguage to Accept-Language header; throws on header/body mismatch - Server Streamable HTTP transport: validates Accept-Language header vs _meta (400 on mismatch), copies header→_meta when only header present, mirrors Content-Language header from response _meta on JSON responses Examples: - examples/server/src/i18nExample.ts: server with get_greeting tool supporting en/fr/de via stdio and HTTP transports - examples/client/src/i18nClient.ts: client demonstrating three language scenarios (exact, fallback chain, no-match fallback) Tests: - Unit tests for all helpers and negotiateLanguage (quality values, subtag matching, fallback behavior) - HTTP integration tests: header mirroring, Content-Language on response, 400 on mismatch, agreement pass-through - stdio integration test: mid-session language switching on same connection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🦋 Changeset detectedLatest commit: 0ca56be The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@modelcontextprotocol/client
@modelcontextprotocol/codemod
@modelcontextprotocol/core
@modelcontextprotocol/server
@modelcontextprotocol/server-legacy
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
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>
1. Use -32001 HeaderMismatch error code (SEP-2243) for language header/body mismatch on both client and server sides. 2. Add error-response localization: getErrorContentLanguage/ setErrorContentLanguage helpers for error.data._meta, mirror Content-Language header from error responses, demonstrate in example server (empty name triggers localized error). 3. Remove batch handling from client Accept-Language extraction (MCP no longer permits JSON-RPC batches over Streamable HTTP). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Update: Review feedback addressed (commit 5a5f548)Following @pja-ant's review on the SEP (modelcontextprotocol/modelcontextprotocol#2792), three changes applied per SEP commit b5667477: 1.
|
…view The SEP-2792 strict mismatch rule is dropped per reviewer feedback: intermediaries (CloudFront, Fastly, Varnish) routinely strip or rewrite Accept-Language, making byte-equality enforcement unreliable. Changes: - Server: rename validateAndCopyAcceptLanguage → copyAcceptLanguageFromHeader; always use _meta as canonical, fall back to header only when _meta absent - Client: remove mismatch throw; always mirror _meta → header unconditionally - Delete HEADER_MISMATCH_ERROR_CODE constant (no longer used) - Replace mismatch integration test with positive 'stripped header' tests - Remove HEADER_MISMATCH_ERROR_CODE unit test Refs: SEP commit 63dc3e1a on SamMorrowDrums/modelcontextprotocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove header/body mismatch rejection (SEP commit
|
SEP-2792 now defines _meta[acceptLanguage] as the ONLY canonical carrier. Servers MUST NOT treat a bare Accept-Language header (without _meta) as an MCP language preference. This removes the copyAcceptLanguageFromHeader method entirely — the server transport no longer reads Accept-Language on the inbound path at all. Client still mirrors _meta → Accept-Language on outbound (best-effort hint). Server still mirrors _meta[contentLanguage] → Content-Language on response. Refs: SEP commit 8e1b6f4d on SamMorrowDrums/modelcontextprotocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove bare-header fallback (SEP commit
|
…dback Reverts the permissive no-enforcement design and reinstates strict byte-equality checks on both request and response paths: - HEADER_MISMATCH_ERROR_CODE reintroduced as -32005 (provisional; avoids -32001 conflict with Python/Kotlin REQUEST_TIMEOUT) - Server rejects with HTTP 400 / -32005 when Accept-Language header and _meta[acceptLanguage] are both present but not byte-identical - Server ignores bare Accept-Language header when _meta is absent - Server honors _meta when header is absent (CDN-strip tolerance) - Client checks response Content-Language header vs _meta[contentLanguage] on JSON responses; throws on byte-mismatch - Tests cover full byte-equality matrix: identical, different tag, lowercased, reordered ranges, extra spacing, q-value format diffs, header-absent, _meta-absent, response mismatch, error response, SSE, and stdio mid-session switch Refs: SEP-2792 commit 4e2a5be9 on SamMorrowDrums/modelcontextprotocol Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reinstate byte-equality mismatch rule (SEP commit
|
Adds per-request language negotiation per SEP-2792: - i18n helpers/constants in core-internal - Server Accept-Language byte-equality validation - Content-Language/Vary/Cache-Control response headers - Client Accept-Language mirroring + response mismatch detection - Comprehensive tests (byte-equality matrix, stdio, error path) - Example server+client with en/fr/de translations Uses HEADER_MISMATCH_ERROR_CODE = -32020. Refs: transports-wg PR modelcontextprotocol#42, commit c85169e8
- Client: Accept-Language header emission, Content-Language mismatch check - Server: Accept-Language byte-equality validation, i18n response headers - InboundClassification: validateAcceptLanguageHeader function
SEP-2792 Reference Implementation — Complete Rewrite on Current MainThis PR has been completely rebuilt on the current What's implemented:Core (
Server (
Client (
Tests (14 passing):
Example (
Architecture notes:
Known limitations (draft):
Refs: SEP-2792 (transports-wg PR #42, commit c85169e8) |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f2236498-f7bf-43f1-9ef1-d54fd21665ae
Align per-request metadata, HTTP mirroring, caching, MRTR flows, and runnable examples with the current SEP and SDK v2 architecture. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f2236498-f7bf-43f1-9ef1-d54fd21665ae
Validate input-required responses through the composed modern result schema and retain the full parsed result for manual callers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: f2236498-f7bf-43f1-9ef1-d54fd21665ae
Summary
Reference implementation for SEP-2792: Per-Request Language Negotiation, aligned to the authoritative specification at
ce12e646and the current TypeScript SDK v2 architecture.This remains a draft while SEP-2792 is Draft. It implements language-preference propagation and reporting; it does not automatically translate SDK content or infer timezone, currency, formatting, collation, units, or other locale behavior.
Public API
Both
@modelcontextprotocol/clientand@modelcontextprotocol/serverre-export:ACCEPT_LANGUAGE_META/CONTENT_LANGUAGE_METAgetAcceptLanguage,getRawAcceptLanguage,setAcceptLanguagegetContentLanguage,getErrorContentLanguage,getMessageContentLanguage,setContentLanguage,setErrorContentLanguageisValidAcceptLanguage,negotiateLanguage,languageHeaderValueConflictsRequestOptions.acceptLanguagemakes the preference available on every request, including methods without explicit params and the connect-timeserver/discover/initializeexchange. It is request-scoped and is preserved across MRTR retry legs without becoming connection/session state.Implementation helpers live once in
@modelcontextprotocol/core-internal; obsolete duplicate v1packages/corehelpers and old example locations were removed.Protocol and transport behavior
params._meta["io.modelcontextprotocol/acceptLanguage"]._meta["io.modelcontextprotocol/contentLanguage"], includingerror.data._metaandInputRequiredResult._meta.Accept-Languagegrammar validation plus weighted, wildcard-aware, case-insensitive RFC 4647 lookup. Malformed canonical values are treated as absent after mirror agreement and do not error by themselves.contentLanguage.Accept-Languagefield line. Servers use Fetch's complete combined field value (repeated lines combine with", "), tolerate a missing mirror, ignore a bare header, and return HTTP 400 /HeaderMismatch-32020on exact disagreement before grammar validation.Content-Language; conflicting duplicated values are malformed. SSE remains canonical per message, omits a stream-wide header by default, and clients enforce agreement if a server does provide one.Vary: Accept-Language; stripped-header responses areCache-Control: privatewhen a shared HTTP cache cannot observe the canonical variant.Caching and MRTR
The client response cache keys cacheable list/resource results by the exact request preference in addition to method/params/server/principal partition. Distinct preferences never share an entry even when they select the same language, and invalidations remove every language variant. Existing
ttlMsandcacheScopesemantics remain unchanged.The runnable
examples/i18nstory uses a deployment approval workflow: stabledeploy_release/environmentidentifiers with localized discovery title, progress text, elicitation message/schema labels, result/error display, default fallback, preference changes/omission, stdio carriage, HTTP mirroring, and currentinputRequired/elicitation/createretry behavior. Its matrix is modern-only because legacy stateless HTTP cannot carry the MRTR return path.Validation
pnpm install --frozen-lockfile✅pnpm build:all✅pnpm test:all✅pnpm -r typecheck✅core-internal,client,server,node,examples) ✅pnpm run run:examples— 77/77 legs passed ✅pnpm check:allcompletes snippet sync and all typechecks, then the clean current-main tree reports pre-existingimport/no-extraneous-dependenciesmanifest classification errors in untouchedtest/helpers,test/conformance, andtest/e2eworkspaces. Those unrelated manifests are not changed by this PR.Release and limitations
@modelcontextprotocol/clientand@modelcontextprotocol/server.@formatjs/intl-localematcherapproach was removed in favor of the small standards-compatible implementation.2.0.0packages do not contain this API.AI disclosure
Authored with GitHub Copilot assistance.
Co-authored-by: Copilot App 223556219+Copilot@users.noreply.github.com