Align 2026-07-28 wire with spec PR #3002: serverInfo in result _meta, clientInfo optional (#2513)

This commit is contained in:
Felix Weinberger 2026-07-20 16:09:55 +01:00 • committed by GitHub
commit f4137630c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 933 additions and 207 deletions

View file

@ -48,7 +48,7 @@ class ScriptedTransport implements Transport {
const discoverResult = (supportedVersions: string[]): DiscoverResult => ({
supportedVersions,
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 'persisted-server', version: '1.0.0' },
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'persisted-server', version: '1.0.0' } },
instructions: 'persisted instructions'
});
@ -149,7 +149,7 @@ describe('getDiscoverResult() round-trip', () => {
result: {
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'probed-server', version: '2.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'probed-server', version: '2.0.0' } }
}
});
}
@ -157,7 +157,7 @@ describe('getDiscoverResult() round-trip', () => {
const bootstrap = new Client({ name: 'bootstrap', version: '0' }, { versionNegotiation: { mode: 'auto' } });
await bootstrap.connect(bootstrapTransport);
const probed = bootstrap.getDiscoverResult();
expect(probed?.serverInfo).toEqual({ name: 'probed-server', version: '2.0.0' });
expect(bootstrap.getServerVersion()).toEqual({ name: 'probed-server', version: '2.0.0' });
expect(probed?.supportedVersions).toEqual([MODERN]);
await bootstrap.close();
// close() clears per-connection state.
@ -190,17 +190,19 @@ describe('getDiscoverResult() round-trip', () => {
cacheScope: 'public',
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'rediscovered', version: '3.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'rediscovered', version: '3.0.0' } }
}
});
}
});
const client = new Client({ name: 'c', version: '0' });
await client.connect(transport, { prior: modernPrior([MODERN]) });
expect(client.getDiscoverResult()?.serverInfo.name).toBe('persisted-server');
const metaName = (r?: { _meta?: Record<string, unknown> }) =>
(r?._meta?.['io.modelcontextprotocol/serverInfo'] as { name?: string } | undefined)?.name;
expect(metaName(client.getDiscoverResult())).toBe('persisted-server');
const fresh = await client.discover();
expect(fresh.serverInfo.name).toBe('rediscovered');
expect(client.getDiscoverResult()?.serverInfo.name).toBe('rediscovered');
expect(metaName(fresh)).toBe('rediscovered');
expect(metaName(client.getDiscoverResult())).toBe('rediscovered');
await client.close();
});
});
@ -348,4 +350,24 @@ describe('connect({ prior }) — malformed persisted blobs (runtime hardening)',
);
expect(transport.sent).toHaveLength(0);
});
test('a stale blob with a body serverInfo (not a spec field) connects with anonymous identity — the body is never read', async () => {
// The 2026-07-28 revision has no body serverInfo on DiscoverResult:
// identity travels in _meta (spec PR #3002). A persisted blob carrying
// the non-spec body member is still a schema-valid DiscoverResult
// (loose results tolerate unknown members), so validatePrior accepts
// it — but the identity read consults _meta only, so the connection is
// anonymous rather than adopting a field the spec does not define.
const transport = new ScriptedTransport();
const client = new Client({ name: 'worker', version: '0' });
const blob =
'{"kind":"modern","discover":{"supportedVersions":["2026-07-28"],"capabilities":{},' +
'"serverInfo":{"name":"stale-server","version":"0.4.0"}}}';
await client.connect(transport, { prior: JSON.parse(blob) as PriorDiscovery });
expect(transport.sent).toHaveLength(0);
expect(client.getProtocolEra()).toBe('modern');
expect(client.getServerVersion()).toBeUndefined();
await client.close();
});
});

View file

@ -44,7 +44,7 @@ const discoverBody = {
cacheScope: 'public',
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'modern-server', version: '1.0.0' },
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'modern-server', version: '1.0.0' } },
instructions: 'modern instructions'
};
@ -78,7 +78,7 @@ describe('Client.discover()', () => {
const advertisement = await client.discover();
expect(advertisement.supportedVersions).toEqual([MODERN]);
expect(advertisement.serverInfo).toEqual({ name: 'modern-server', version: '1.0.0' });
expect(advertisement._meta?.['io.modelcontextprotocol/serverInfo']).toEqual({ name: 'modern-server', version: '1.0.0' });
expect(advertisement.instructions).toBe('modern instructions');
await client.close();
@ -99,3 +99,49 @@ describe('Client.discover()', () => {
await client.close();
});
});
describe('server identity from a DiscoverResult (#3002: _meta only)', () => {
const base = { resultType: 'complete', ttlMs: 0, cacheScope: 'public', supportedVersions: [MODERN], capabilities: {} };
const metaIdentity = { name: 'meta-server', version: '2.0.0' };
async function connectAgainst(result: Record<string, unknown>): Promise<Client> {
const transport = new ScriptedTransport((message, t) => {
if (isJSONRPCRequest(message) && message.method === 'server/discover') {
t.reply({ jsonrpc: '2.0', id: message.id, result });
}
});
const client = new Client({ name: 'c', version: '0' }, { versionNegotiation: { mode: { pin: MODERN } } });
await client.connect(transport);
return client;
}
test('serverInfo in _meta yields the identity', async () => {
const client = await connectAgainst({ ...base, _meta: { 'io.modelcontextprotocol/serverInfo': metaIdentity } });
expect(client.getServerVersion()).toEqual(metaIdentity);
await client.close();
});
test('a stray body serverInfo is ignored — identity comes from _meta only', async () => {
const client = await connectAgainst({ ...base, serverInfo: { name: 'body-only', version: '0.9.0' } });
expect(client.getServerVersion()).toBeUndefined();
await client.close();
});
test('absent identity → undefined, connection still established (serverInfo is a SHOULD)', async () => {
const client = await connectAgainst(base);
expect(client.getServerVersion()).toBeUndefined();
await client.close();
});
test('a MALFORMED _meta serverInfo still connects modern through the real transport path (regression: the neutral-schema classification guard must not drop the frame)', async () => {
// Exercises the full inbound pipeline — transport.onmessage →
// isJSONRPCResultResponse (neutral JSONRPCResultResponseSchema) →
// probe classification — not a pre-built ProbeOutcome. A strict
// neutral serverInfo key made this connect fail before the frame
// reached the lenient wire schema.
const client = await connectAgainst({ ...base, _meta: { 'io.modelcontextprotocol/serverInfo': 'utterly-bogus' } });
expect(client.getNegotiatedProtocolVersion()).toBe(MODERN);
expect(client.getServerVersion()).toBeUndefined();
await client.close();
});
});

View file

@ -47,7 +47,7 @@ async function scriptedServerSide(era: 'modern' | 'legacy', answerToolsList = tr
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'scripted-modern-server', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted-modern-server', version: '1.0.0' } }
}
});
} else {

View file

@ -45,7 +45,7 @@ async function scriptedModernServer(respondToToolCall: (request: JSONRPCRequest,
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'scripted-mrtr-server', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted-mrtr-server', version: '1.0.0' } }
}
});
return;

View file

@ -36,7 +36,7 @@ async function scriptedModern(onListen?: (id: number | string, filter: unknown,
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true }, prompts: { listChanged: true } },
serverInfo: { name: 'scripted', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted', version: '1' } }
}
});
}
@ -73,7 +73,7 @@ async function scriptedModernNoAck() {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true }, prompts: { listChanged: true } },
serverInfo: { name: 'scripted', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted', version: '1' } }
}
});
}
@ -249,7 +249,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: {},
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}
@ -278,7 +278,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: {},
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}
@ -340,7 +340,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: {},
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}
@ -419,7 +419,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: {},
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}
@ -554,7 +554,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}
@ -679,7 +679,7 @@ describe('Client.listen()', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 's', version: '1' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1' } }
}
});
}

View file

@ -72,7 +72,7 @@ async function scriptedModernServer(pages: Tool[][], rejectFirstCall = false): P
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 'scripted', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted', version: '1.0.0' } }
}
});
} else if (r.method === 'tools/list') {

View file

@ -36,7 +36,7 @@ async function scriptedServerSide(eras: 'modern' | 'legacy') {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: {} },
serverInfo: { name: 'scripted-modern-server', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted-modern-server', version: '1.0.0' } }
}
});
} else {

View file

@ -31,7 +31,7 @@ function classify(outcome: ProbeOutcome, context: Partial<ProbeClassifierContext
const discoverResult = (supportedVersions: string[]) => ({
supportedVersions,
capabilities: { tools: {} },
serverInfo: { name: 'fixture-server', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'fixture-server', version: '1.0.0' } }
});
/** The deployed-fleet 400 body for a JSON-RPC error (server streamableHttp `createJsonErrorResponse`). */
@ -57,7 +57,7 @@ describe('row: DiscoverResult with version overlap → modern, select from suppo
expect(verdict.kind).toBe('modern');
if (verdict.kind === 'modern') {
expect(verdict.discover.capabilities).toEqual({ tools: {} });
expect(verdict.discover.serverInfo.name).toBe('fixture-server');
expect(verdict.discover._meta?.['io.modelcontextprotocol/serverInfo']).toEqual({ name: 'fixture-server', version: '1.0.0' });
}
});
});

View file

@ -53,6 +53,20 @@ const DEPLOYED_SESSION_REQUIRED_BODY = JSON.stringify({
error: { code: -32_000, message: 'Bad Request: Server not initialized' }
});
/**
* The exact discover result a go-sdk v1.7.0-pre.3 server answers the probe
* with (spec PR #3002 final 2026-07-28 shape): `serverInfo` lives in the
* result `_meta`, the body field is gone.
*/
const GO_V17_DISCOVER_RESULT = {
resultType: 'complete',
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'server', version: 'v0.0.1' } },
ttlMs: 0,
cacheScope: 'public',
supportedVersions: ['2026-07-28', '2025-11-25', '2025-06-18', '2025-03-26', '2024-11-05'],
capabilities: { logging: {} }
};
interface CorpusRow {
name: string;
outcome: ProbeOutcome;
@ -78,7 +92,7 @@ const CORPUS: CorpusRow[] = [
name: 'T9: DiscoverResult with no mutual version + fallback available → legacy (initialize on the same connection)',
outcome: {
kind: 'result',
result: { supportedVersions: ['2027-01-01'], capabilities: {}, serverInfo: { name: 's', version: '1' } }
result: { supportedVersions: ['2027-01-01'], capabilities: {} }
},
expected: 'legacy'
},
@ -86,7 +100,7 @@ const CORPUS: CorpusRow[] = [
name: 'T9: DiscoverResult with no mutual version + NO fallback (pin / modern-only) → typed error, never initialize',
outcome: {
kind: 'result',
result: { supportedVersions: ['2027-01-01'], capabilities: {}, serverInfo: { name: 's', version: '1' } }
result: { supportedVersions: ['2027-01-01'], capabilities: {} }
},
context: { fallbackAvailable: false },
expected: 'error'
@ -164,6 +178,26 @@ const CORPUS: CorpusRow[] = [
name: 'wire-real: -32601 method-not-found → legacy fallback',
outcome: { kind: 'rpc-error', code: -32_601, message: 'Method not found' },
expected: 'legacy'
},
// --- Wire-real shape C: the #3002 final-revision DiscoverResult (go v1.7.0-pre.3).
// Regression: before the #3002 alignment the wire schema required body
// `serverInfo`, so this conforming response failed parse and misclassified
// legacy — the client then attempted `initialize` against a modern server.
{
name: 'wire-real: go v1.7.0-pre.3 DiscoverResult (#3002 shape, serverInfo in _meta) → modern',
outcome: { kind: 'result', result: GO_V17_DISCOVER_RESULT },
expected: 'modern'
},
// A malformed _meta serverInfo is display-only material and must not
// demote a conforming DiscoverResult to legacy (receiver leniency: the
// wire schema drops the bad value instead of failing the parse).
{
name: 'recognizer: DiscoverResult with a malformed _meta serverInfo → still modern',
outcome: {
kind: 'result',
result: { ...GO_V17_DISCOVER_RESULT, _meta: { 'io.modelcontextprotocol/serverInfo': 'bogus' } }
},
expected: 'modern'
}
];
@ -179,7 +213,7 @@ describe('T9/T11 merged probe fixture corpus (probe classifier)', () => {
const verdict = classifyProbeOutcome(
{
kind: 'result',
result: { supportedVersions: [MODERN], capabilities: {}, serverInfo: { name: 's', version: '1' } }
result: { supportedVersions: [MODERN], capabilities: {} }
},
baseContext
);

View file

@ -345,7 +345,7 @@ async function scriptedModernServer(pages: Tool[][], opts: ScriptOptions = {}):
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { tools: { listChanged: true }, prompts: {}, resources: {} },
serverInfo: opts.serverInfo ?? { name: 'scripted', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': opts.serverInfo ?? { name: 'scripted', version: '1.0.0' } }
}
});
} else if (r.method === 'tools/list') {
@ -1210,7 +1210,7 @@ describe('Client honours cacheHints (SEP-2549)', () => {
resultType: 'complete',
supportedVersions: [MODERN],
capabilities: { resources: {} },
serverInfo: { name: 'scripted', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted', version: '1.0.0' } }
}
});
} else if (r.method === 'resources/read') {

View file

@ -27,7 +27,7 @@ async function connectedPair() {
resultType: 'complete',
supportedVersions: ['2026-07-28'],
capabilities: { tools: {} },
serverInfo: { name: 's', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 's', version: '1.0.0' } }
}
});
} else if (r.method === 'tools/list') {

View file

@ -118,7 +118,7 @@ class ScriptedTransport implements Transport {
const discoverResult = (supportedVersions: string[]) => ({
supportedVersions,
capabilities: {},
serverInfo: { name: 'scripted-modern-server', version: '1.0.0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'scripted-modern-server', version: '1.0.0' } }
});
/** A scripted dual-era server: answers server/discover with a DiscoverResult and initialize like a 2025 server. */