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

View file

@ -28,10 +28,17 @@ client: []
# --- Same gaps as the 2025 baseline (fail identically when forced to 2026-07-28) ---
# (empty: SEP-2468/2352/2350/837 burned by the auth bundle; SEP-2106 burned earlier)
server: []
server:
# --- Carried-forward scenarios (also run by the 2025 legs) ---
# (empty: json-schema-2020-12 burned by the SEP-2106 fixture;
# (json-schema-2020-12 burned by the SEP-2106 fixture;
# sep-2164-resource-not-found burned by the spec#2907 error-code renumber +
# alpha.5 referee; server-stateless burned by conformance#376 at alpha.9 —
# the referee's `requiredCapabilities` assertion now matches the schema's
# `ClientCapabilities` object, which is what this SDK emits.)
# alpha.5 referee.)
#
# --- spec PR #3002 — referee pinned at alpha.9 asserts the OLD shape ---
# Same three failing checks as the 2025-leg baseline entry
# (sep-2575-request-meta-invalid-missing-client-info, the
# missing-client-info iteration of sep-2575-http-server-meta-invalid-400,
# and sep-2575-server-implements-discover which requires body serverInfo):
# this SDK follows the final revision. Remove when the pin bumps to a
# conformance release that incorporates #3002 (alpha.10+).
- server-stateless

View file

@ -23,6 +23,17 @@ client: []
# last referee-side gap — conformance#361 callback-iss — closed at alpha.6)
server:
# --- spec PR #3002 — referee pinned at alpha.9 asserts the OLD shape ---
# The alpha.9 `server-stateless` scenario still enforces the pre-#3002
# spec: clientInfo required in the envelope, serverInfo a DiscoverResult
# body field. This SDK follows the final revision (clientInfo optional;
# identity in the result _meta), so exactly three checks fail:
# - sep-2575-request-meta-invalid-missing-client-info
# - sep-2575-http-server-meta-invalid-400 (the missing-client-info iteration)
# - sep-2575-server-implements-discover (requires body serverInfo)
# Remove this entry when the pin bumps to a conformance release that
# incorporates #3002 (alpha.10+).
- server-stateless
# --- SEP-2663 (io.modelcontextprotocol/tasks) — server SDK does not implement the tasks extension ---
# Extension-tagged scenarios; selected only by `--suite all` (the alpha.9 referee
# has no server-side `--suite extensions`). The active/draft/2026 legs never select

View file

@ -366,7 +366,7 @@ function withLocalDiscoverResponse(serverInfo: { name: string; version: string }
// list/read/get calls reach the real mock; callers that
// only use tools are unaffected by the extra entries.
capabilities: { tools: { listChanged: true }, resources: {}, prompts: {} },
serverInfo
_meta: { 'io.modelcontextprotocol/serverInfo': serverInfo }
}
},
{ status: 200, headers: { 'Content-Type': 'application/json' } }

View file

@ -1365,7 +1365,24 @@ export const REQUIREMENTS: Record<string, Requirement> = {
},
'protocol:meta:result-to-client': {
source: 'https://modelcontextprotocol.io/specification/2025-11-25/basic#_meta',
behavior: "_meta returned in a handler's result is delivered intact to the requesting client."
behavior:
"_meta returned in a handler's result is delivered intact to the requesting client. On a 2026-07-28 connection the delivered _meta additionally carries the SDK-stamped io.modelcontextprotocol/serverInfo key (spec PR #3002: servers SHOULD identify themselves on every response); on 2025-era connections nothing is ever added."
},
'protocol:meta:server-identity': {
source: 'https://modelcontextprotocol.io/specification/draft/server/discover',
behavior:
"On the 2026-07-28 revision the server identifies itself via _meta['io.modelcontextprotocol/serverInfo'] on its responses (spec PR #3002), and the client resolves getServerVersion() from the discover result's _meta.",
transports: ['entryModern'],
addedInSpecVersion: '2026-07-28',
note: 'entryModern is the only arm that negotiates a real 2026-07-28 connection (the other arms run the legacy handshake), and the body also POSTs a raw discover via wired.fetch to assert the wire-level _meta stamp.'
},
'protocol:envelope:client-info-optional': {
source: 'https://modelcontextprotocol.io/specification/draft/basic#meta',
behavior:
'A 2026-07-28 request whose per-request _meta envelope omits io.modelcontextprotocol/clientInfo is served normally — clientInfo is a SHOULD (spec PR #3002), never a validation requirement.',
transports: ['entryModern'],
addedInSpecVersion: '2026-07-28',
note: "The wired SDK client always sends clientInfo (the spec SHOULD), so the omission can only be put on the wire as a raw POST through wired.fetch — which needs the entry arm's in-process endpoint."
},
'protocol:request-id:unique': {
entryExclusions: [{ arm: 'entryModern', reason: 'method-not-in-modern-registry' }],

View file

@ -35,7 +35,7 @@ import {
import { expect, vi } from 'vitest';
import { z } from 'zod/v4';
import { tapWire, wire } from '../helpers/index';
import { modernEnvelopeMeta, tapWire, wire } from '../helpers/index';
import { verifies } from '../helpers/verifies';
import type { TestArgs } from '../types';
@ -1128,8 +1128,71 @@ verifies('protocol:meta:result-to-client', async ({ transport }: TestArgs) => {
const result = await client.callTool({ name: 'metered_call', arguments: {} });
expect(result.content).toEqual([{ type: 'text', text: 'metered' }]);
// The _meta the handler attached to its result reaches the requesting client unchanged.
expect(result._meta).toEqual(resultMeta);
// The _meta the handler attached to its result reaches the requesting
// client unchanged. On a 2026-era connection (the entryModern arm) the
// encode seam additionally stamps the server's identity (spec PR #3002:
// `_meta` serverInfo SHOULD on every response); on 2025-era connections
// nothing is ever added (never-stamp).
const expectedMeta =
client.getNegotiatedProtocolVersion() === '2026-07-28'
? { ...resultMeta, 'io.modelcontextprotocol/serverInfo': { name: 's', version: '0' } }
: resultMeta;
expect(result._meta).toEqual(expectedMeta);
});
verifies('protocol:meta:server-identity', async ({ transport }: TestArgs) => {
const makeServer = () => {
const s = new Server({ name: 'identity-server', version: '4.2.0' }, { capabilities: { tools: {} } });
s.setRequestHandler('tools/list', () => ({ tools: [] }));
return s;
};
const client = newClient();
await using wired = await wire(transport, makeServer, client);
// Client-side resolution: getServerVersion() reads the discover _meta.
expect(client.getServerVersion()).toEqual({ name: 'identity-server', version: '4.2.0' });
// Wire-side: a raw discover response carries the _meta stamp.
const response = await wired.fetch!(wired.url!, {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2026-07-28',
'mcp-method': 'server/discover'
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'server/discover', params: { _meta: modernEnvelopeMeta() } })
});
expect(response.status).toBe(200);
const body = (await response.json()) as { result: { _meta?: Record<string, unknown> } };
expect(body.result._meta?.['io.modelcontextprotocol/serverInfo']).toEqual({ name: 'identity-server', version: '4.2.0' });
});
verifies('protocol:envelope:client-info-optional', async ({ transport }: TestArgs) => {
const makeServer = () => {
const s = new Server({ name: 's', version: '0' }, { capabilities: { tools: {} } });
s.setRequestHandler('tools/list', () => ({ tools: [] }));
return s;
};
const client = newClient();
await using wired = await wire(transport, makeServer, client);
const meta = modernEnvelopeMeta();
delete meta['io.modelcontextprotocol/clientInfo'];
const response = await wired.fetch!(wired.url!, {
method: 'POST',
headers: {
'content-type': 'application/json',
accept: 'application/json, text/event-stream',
'mcp-protocol-version': '2026-07-28',
'mcp-method': 'tools/list'
},
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list', params: { _meta: meta } })
});
expect(response.status).toBe(200);
const body = (await response.json()) as { result?: { tools?: unknown[] }; error?: unknown };
expect(body.error).toBeUndefined();
expect(body.result?.tools).toEqual([]);
});
verifies('protocol:request-id:unique', async ({ transport }: TestArgs) => {
@ -1611,7 +1674,7 @@ class LoopbackTransport implements Transport {
this.respond(message.id, {
supportedVersions: [this.serverProtocolVersion],
capabilities: { tools: {} },
serverInfo: { name: 'loopback-server', version: '3.1.4' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'loopback-server', version: '3.1.4' } }
});
break;
}

View file

@ -63,7 +63,7 @@ function makeResponder(toolCallBody: unknown) {
return {
supportedVersions: ['2026-07-28'],
capabilities: { tools: {} },
serverInfo: { name: 'raw-input-required-server', version: '0' }
_meta: { 'io.modelcontextprotocol/serverInfo': { name: 'raw-input-required-server', version: '0' } }
};
}
if (request.method === 'tools/call') return toolCallBody;