What I found
mediaTypeEssence (packages/core-internal/src/shared/mediaType.ts) has a guard for a Content-Type value that is unparseable because it carries a joining comma — Headers.get() joins repeated headers with ', ', so a client that sends Content-Type twice, or a proxy that appends one, produces a value like application/json, application/json. The guard returns no essence for that, which is the documented intent. But it only looks for the comma in the parameter tail:
const essence = (header.split(';', 1)[0] ?? '').trim().toLowerCase();
if (essence === '' || header.slice(essence.length).includes(',')) {
With no parameters there is no tail, so the comma sits inside the media-type segment and header.slice(essence.length) starts after it:
Before / after
| header |
before |
after |
application/json, application/json |
'application/json, application/json' |
undefined |
text/plain, text/html |
'text/plain, text/html' |
undefined |
application/json; charset=utf-8, text/plain |
undefined |
undefined |
TEXT/PLAIN; a=b, c=d |
undefined |
undefined |
application/json; |
'application/json' |
'application/json' |
application/json ; a=1 |
'application/json' |
'application/json' |
So the two shapes of the same ambiguity behave differently, and the first one returns a string that is not a media type at all.
packages/core-internal/test/shared/mediaType.test.ts already documents the intended behaviour — the test is named "yields no essence for joined duplicate headers, with or without parameters" and its comment says "Without parameters the comma lands in the first segment; with parameters it hides in the tail — both must behave the same" — but its assertion pins the value the guard actually returns.
Impact, honestly stated
No call site changes behaviour today. Every use of the essence compares it against a known media type, and the bogus string fails those comparisons exactly as undefined does. What the fix changes is that the returned value matches the function's documented contract ("the lowercased type/subtype pair, without parameters") and that the two joined-duplicate shapes agree, which is what the existing test's name and comment already describe.
A second, related looseness I did not propose changing: for an unparseable value whose first segment is not a valid type at all (garbage, application/json extra), the fallback returns that raw segment as the "essence". The lenient fallback is deliberate for sloppy parameter sections; tightening the type side is a separate policy call, so I would leave it alone unless you want it.
Suggested change
Check the whole value instead of the text after the essence, so a comma anywhere in an unparseable value is ambiguous:
if (essence === '' || header.includes(',')) {
A comma inside a quoted parameter value (text/plain; foo="a,b") is unaffected, because that value parses and never reaches the fallback. The existing assertion in the test named above would need to change from the bogus string to undefined, which is what its own name and comment say it should be.
I have this implemented and tested on a branch (fix/content-type-joined-duplicate-essence on feiiiiii5/typescript-sdk): 1469/1469 in core-internal, 525/525 in server, typecheck/eslint/prettier clean, plus a patch changeset. I have not been able to open the PR from my side — GitHub is currently refusing createPullRequest for my account on this repository (gh pr create reports "does not have the correct permissions to execute CreatePullRequest", POST /pulls returns 404, while gh pr create --dry-run resolves the head/base fine, and issues and comments work). If that clears, the branch is ready; if you would rather take the change directly, the diff is small enough to describe in a comment.
Steps to reproduce
import { mediaTypeEssence } from '@modelcontextprotocol/core-internal';
mediaTypeEssence('application/json, application/json');
// 'application/json, application/json' — expected undefined
What I found
mediaTypeEssence(packages/core-internal/src/shared/mediaType.ts) has a guard for aContent-Typevalue that is unparseable because it carries a joining comma —Headers.get()joins repeated headers with', ', so a client that sendsContent-Typetwice, or a proxy that appends one, produces a value likeapplication/json, application/json. The guard returns no essence for that, which is the documented intent. But it only looks for the comma in the parameter tail:With no parameters there is no tail, so the comma sits inside the media-type segment and
header.slice(essence.length)starts after it:Before / after
application/json, application/json'application/json, application/json'undefinedtext/plain, text/html'text/plain, text/html'undefinedapplication/json; charset=utf-8, text/plainundefinedundefinedTEXT/PLAIN; a=b, c=dundefinedundefinedapplication/json;'application/json''application/json'application/json ; a=1'application/json''application/json'So the two shapes of the same ambiguity behave differently, and the first one returns a string that is not a media type at all.
packages/core-internal/test/shared/mediaType.test.tsalready documents the intended behaviour — the test is named "yields no essence for joined duplicate headers, with or without parameters" and its comment says "Without parameters the comma lands in the first segment; with parameters it hides in the tail — both must behave the same" — but its assertion pins the value the guard actually returns.Impact, honestly stated
No call site changes behaviour today. Every use of the essence compares it against a known media type, and the bogus string fails those comparisons exactly as
undefineddoes. What the fix changes is that the returned value matches the function's documented contract ("the lowercasedtype/subtypepair, without parameters") and that the two joined-duplicate shapes agree, which is what the existing test's name and comment already describe.A second, related looseness I did not propose changing: for an unparseable value whose first segment is not a valid type at all (
garbage,application/json extra), the fallback returns that raw segment as the "essence". The lenient fallback is deliberate for sloppy parameter sections; tightening the type side is a separate policy call, so I would leave it alone unless you want it.Suggested change
Check the whole value instead of the text after the essence, so a comma anywhere in an unparseable value is ambiguous:
A comma inside a quoted parameter value (
text/plain; foo="a,b") is unaffected, because that value parses and never reaches the fallback. The existing assertion in the test named above would need to change from the bogus string toundefined, which is what its own name and comment say it should be.I have this implemented and tested on a branch (
fix/content-type-joined-duplicate-essenceonfeiiiiii5/typescript-sdk): 1469/1469 incore-internal, 525/525 inserver,typecheck/eslint/prettierclean, plus a patch changeset. I have not been able to open the PR from my side — GitHub is currently refusingcreatePullRequestfor my account on this repository (gh pr createreports "does not have the correct permissions to execute CreatePullRequest",POST /pullsreturns 404, whilegh pr create --dry-runresolves the head/base fine, and issues and comments work). If that clears, the branch is ready; if you would rather take the change directly, the diff is small enough to describe in a comment.Steps to reproduce