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
61 changed files with 933 additions and 207 deletions

View file

@ -0,0 +1,17 @@
---
'@modelcontextprotocol/core-internal': minor
'@modelcontextprotocol/core': minor
'@modelcontextprotocol/client': minor
'@modelcontextprotocol/server': minor
---
Align the 2026-07-28 wire with the final revision (spec PR #3002): `serverInfo` moves from the `DiscoverResult` body to the result `_meta`, and the per-request envelope's `clientInfo` demotes from required to SHOULD.
Before this change the SDK shipped the pre-#3002 shape in both directions: the client hard-rejected a conforming server's `DiscoverResult` (missing body `serverInfo` failed parse, so the probe misclassified the server as legacy and attempted an `initialize` handshake against it — a hard connect failure against a modern-only server such as go-sdk v1.7.0-pre.3), and the server rejected conforming clients that omit `clientInfo`.
Now:
- The 2026 wire schemas are the final revision exactly: no body `serverInfo` on `DiscoverResult`, envelope `clientInfo` optional (a present-but-malformed value still fails validation).
- Servers stamp `_meta['io.modelcontextprotocol/serverInfo']` on every 2026-era response (spec SHOULD; a handler-authored value wins, the 2025-era wire is untouched). This includes the entry-built `subscriptions/listen` graceful-close results — the spec's `SubscriptionsListenResultMeta` extends `ResultMetaObject`.
- Clients keep sending `clientInfo` and read server identity from the discover result's `_meta` only. A server that stamps no identity is anonymous: `getServerVersion()` is `undefined` and the response cache partitions under a per-connection surrogate. A malformed `_meta` serverInfo value is treated as absent on receive (the spec marks the field self-reported, unverified, and display-only).
- Breaking type changes: `DiscoverResult` no longer declares `serverInfo`; `RequestMetaEnvelope`'s `clientInfo` is optional. New public constant `SERVER_INFO_META_KEY` (`'io.modelcontextprotocol/serverInfo'`).

View file

@ -44,7 +44,7 @@ The recorded value is the server's whole advertisement — supported versions, c
cacheScope: 'private',
supportedVersions: [ '2026-07-28' ],
capabilities: { tools: { listChanged: true } },
serverInfo: { name: 'gateway-target', version: '1.0.0' },
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'gateway-target', version: '1.0.0' } },
resultType: 'complete'
}
```

View file

@ -26,6 +26,7 @@ below.
- [Auth on 2026-07-28](#auth-on-2026-07-28)
- [Per-era wire codecs](#per-era-wire-codecs)
- [Wire-only members hidden from public types](#wire-only-members-hidden-from-public-types)
- [Server identity in result `_meta`; `clientInfo` demoted to SHOULD](#server-identity-in-result-_meta-clientinfo-demoted-to-should)
- [Multi-round-trip requests](#multi-round-trip-requests)
- [Legacy shim for `input_required`](#legacy-shim-for-input_required)
- [`subscriptions/listen`](#subscriptionslisten)
@ -417,6 +418,35 @@ the instance's negotiated era.
---
## Server identity in result `_meta`; `clientInfo` demoted to SHOULD
The final 2026-07-28 revision (spec PR #3002) moved server identity out of the
`DiscoverResult` body: servers identify themselves via
`_meta['io.modelcontextprotocol/serverInfo']` (constant `SERVER_INFO_META_KEY`) on
every response, and the per-request envelope's `clientInfo` is a SHOULD instead of a
requirement.
What the SDK does:
- **Server.** Every 2026-era response gets the `_meta` serverInfo stamp (a
handler-authored value wins; 2025-era responses are untouched) — including the
entry-built `subscriptions/listen` graceful-close results, whose `_meta` carries
the identity next to the subscription id. Requests without `clientInfo` are
served; a present-but-malformed value is still rejected.
- **Client.** `clientInfo` is still sent on every request (the spec SHOULD).
`getServerVersion()` reads the discover result's `_meta`. A server that stamps no
identity is simply anonymous: the connection works, `getServerVersion()` is
`undefined`, and the response cache partitions under a per-connection surrogate.
- **Types.** `DiscoverResult` has no `serverInfo` member, and `RequestMetaEnvelope`'s
`clientInfo` is optional. Code that read `discover.serverInfo` moves to
`client.getServerVersion()` (or the `_meta` key directly).
`serverInfo`/`clientInfo` are self-reported and intended for display, logging, and
debugging — do not use them for behavior or security decisions. A malformed `_meta`
serverInfo value is treated as absent on receive, per the same clause.
---
## Multi-round-trip requests
The 2026-07-28 revision removes the server→client JSON-RPC request channel. Servers

View file

@ -38,7 +38,9 @@ check.equal(bootstrap.getNegotiatedProtocolVersion(), '2026-07-28');
const discovered = bootstrap.getDiscoverResult();
check.ok(discovered, 'bootstrap connect populated getDiscoverResult()');
check.deepEqual(discovered?.serverInfo, { name: 'gateway-target', version: '1.0.0' });
// Server identity: getServerVersion() resolves it from the discover result's
// `_meta['io.modelcontextprotocol/serverInfo']` (spec PR #3002).
check.deepEqual(bootstrap.getServerVersion(), { name: 'gateway-target', version: '1.0.0' });
// The probe was the only request so far; the request_count call is the
// second. (createMcpHandler builds one server instance per request.)

View file

@ -61,6 +61,7 @@ import {
isJSONRPCErrorResponse,
isJSONRPCRequest,
isModernProtocolVersion,
isSpecType,
legacyProtocolVersions,
ListChangedOptionsBaseSchema,
mergeCapabilities,
@ -74,6 +75,7 @@ import {
scanXMcpHeaderDeclarations,
SdkError,
SdkErrorCode,
SERVER_INFO_META_KEY,
SUBSCRIPTION_ID_META_KEY,
SUPPORTED_MODERN_PROTOCOL_VERSIONS
} from '@modelcontextprotocol/core-internal';
@ -84,6 +86,18 @@ import { ClientResponseCache, InMemoryResponseCacheStore, MAX_CACHE_TTL_MS } fro
import type { ResolvedVersionNegotiation, VersionNegotiationOptions } from './versionNegotiation';
import { detectProbeEnvironment, detectProbeTransportKind, negotiateEra, resolveVersionNegotiation } from './versionNegotiation';
/**
* The server identity a `DiscoverResult` carries in
* `_meta['io.modelcontextprotocol/serverInfo']` (a spec SHOULD — absent means
* the server offered no identity). Runtime-validated with the spec-type
* guard because `connect({ prior })` accepts caller-supplied values that
* never saw a wire parse.
*/
function serverInfoFromDiscover(discover: DiscoverResult): Implementation | undefined {
const fromMeta = discover._meta?.[SERVER_INFO_META_KEY];
return isSpecType.Implementation(fromMeta) ? fromMeta : undefined;
}
/**
* Elicitation default application helper. Applies defaults to the `data` based on the `schema`.
*
@ -1122,7 +1136,7 @@ export class Client extends Protocol<ClientContext> {
}
this._serverCapabilities = result.discover.capabilities;
this._serverVersion = result.discover.serverInfo;
this._serverVersion = serverInfoFromDiscover(result.discover);
this._cache.setServerIdentity(this._deriveServerIdentity(transport));
this._instructions = result.discover.instructions;
this._discoverResult = result.discover;
@ -1251,7 +1265,7 @@ export class Client extends Protocol<ClientContext> {
this._discoverResult = discover;
this._serverCapabilities = discover.capabilities;
this._serverVersion = discover.serverInfo;
this._serverVersion = serverInfoFromDiscover(discover);
this._cache.setServerIdentity(this._deriveServerIdentity(transport));
this._instructions = discover.instructions;
this._negotiatedProtocolVersion = version;
@ -1275,7 +1289,10 @@ export class Client extends Protocol<ClientContext> {
}
/**
* After initialization has completed, this will be populated with information about the server's name and version.
* The connected server's self-reported name and version, when it
* identified itself: required on the legacy `initialize` result; a spec
* SHOULD in the discover result's `_meta` on 2026-07-28, so a successful
* modern connect against an anonymous server leaves this `undefined`.
*/
getServerVersion(): Implementation | undefined {
return this._serverVersion;
@ -1283,16 +1300,22 @@ export class Client extends Protocol<ClientContext> {
/**
* The connected server's identity for response-cache partitioning. The
* `serverInfo` `name@version` pair when available (the spec requires it on
* both `initialize` and `server/discover`); falls back to the transport's
* `sessionId` otherwise. The value itself is server-controlled — the
* collision-safety of the storage partition comes from
* `serverInfo` `name@version` pair when available (required on
* `initialize`; a SHOULD in the discover result's `_meta` since spec PR
* #3002); falls back to the transport's `sessionId`, then to a
* per-connection surrogate. The surrogate matters since #3002 made
* identity optional: without it, two identity-less servers reached over
* sessionId-less transports would share the cache's pre-connect `''`
* partition and read each other's entries — no stable identity means no
* cross-connection cache reuse. The value itself is server-controlled —
* the collision-safety of the storage partition comes from
* {@linkcode ClientResponseCache}'s JSON-array encoding around it, not
* from any character it does or does not contain.
*/
private _deriveServerIdentity(transport: Transport): string {
const v = this._serverVersion;
return v === undefined ? (transport.sessionId ?? '') : `${v.name}@${v.version}`;
if (v !== undefined) return `${v.name}@${v.version}`;
return transport.sessionId ?? `anonymous:${Date.now()}-${Math.random().toString(36).slice(2)}`;
}
/**

View file

@ -299,13 +299,13 @@ export class ClientResponseCache {
*/
private _toolOutputValidatorIndex?: { stamp: number; byName: Map<string, unknown> };
/**
* The connected server's identity (`serverInfo.name@version`, or a
* transport-supplied surrogate). Set by the `Client` immediately after a
* successful connect; `''` is the pre-connect sentinel. Every storage
* partition is derived from this (see `_partitionFor`), so two
* clients sharing one store but connected to different servers never
* collide on `tools/list` and a server cannot read another server's
* `'public'` entries.
* The connected server's identity (`serverInfo.name@version`, the
* transport's `sessionId`, or a client-generated per-connection
* surrogate). Set by the `Client` immediately after a successful connect;
* `''` is the pre-connect sentinel. Every storage partition is derived
* from this (see `_partitionFor`), so two clients sharing one store but
* connected to different servers never collide on `tools/list` and a
* server cannot read another server's `'public'` entries.
*/
private _serverIdentity = '';
@ -347,10 +347,15 @@ export class ClientResponseCache {
/**
* Record the connected server's identity. Called by `Client` immediately
* after a successful connect (`serverInfo.name@version`, or the
* transport's `sessionId` when `serverInfo` is unavailable). Every
* partition derived after this call is scoped to this identity; entries
* written under the pre-connect `''` sentinel are no longer reachable.
* after a successful connect: `serverInfo.name@version` when the server
* identified itself, else the transport's `sessionId`, else a
* client-generated per-connection surrogate (`serverInfo` is a spec
* SHOULD on 2026-07-28, so anonymous servers exist). Surrogate-keyed
* partitions are NOT stable across reconnects — no identity means no
* cross-connection cache reuse, and a shared long-lived store should
* bound its own size accordingly. Every partition derived after this
* call is scoped to this identity; entries written under the pre-connect
* `''` sentinel are no longer reachable.
*/
setServerIdentity(identity: string): void {
this._serverIdentity = identity;

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. */

View file

@ -121,6 +121,7 @@ export const SPEC_SCHEMA_NAMES: ReadonlySet<string> = new Set([
'ResourceTemplateSchema',
'ResourceUpdatedNotificationParamsSchema',
'ResourceUpdatedNotificationSchema',
'ResultMetaObjectSchema',
'ResultSchema',
'RoleSchema',
'RootSchema',

View file

@ -92,6 +92,7 @@ export {
PARSE_ERROR,
PROTOCOL_VERSION_META_KEY,
RELATED_TASK_META_KEY,
SERVER_INFO_META_KEY,
SUBSCRIPTION_ID_META_KEY,
SUPPORTED_PROTOCOL_VERSIONS,
TRACEPARENT_META_KEY,

Some files were not shown because too many files have changed in this diff Show more