Skip to content

Commit d1c0b95

Browse files
Align SEP-2575 checks with spec PR #3002: serverInfo in result _meta, clientInfo optional (#403)
Spec PR #3002 (part of the final 2026-07-28 revision) moved serverInfo from the DiscoverResult body to _meta['io.modelcontextprotocol/serverInfo'] and demoted the request envelope's clientInfo from required to SHOULD. The checks still asserted the old shape, failing SDKs that follow the final revision. - Vendor schema types at spec commit 71e30695. - server-stateless: drop the missing-clientInfo rejection case and assert the opposite (a clientInfo-less request must be served, new check sep-2575-request-meta-client-info-optional); discover no longer requires body serverInfo; new SHOULD-level check sep-2575-server-identifies-in-result-meta (WARNING when the _meta identity is absent, SKIPPED when discover itself failed). - request-metadata (client side): the clientInfo SHOULD moves out of the MUST check into its own id sep-2575-client-sends-client-info (WARNING on absence), so requirement levels never blend in one check id. - Mock servers and the everything-server example emit the _meta identity and accept clientInfo-less requests.
1 parent ce25103 commit d1c0b95

10 files changed

Lines changed: 237 additions & 120 deletions

File tree

examples/servers/typescript/everything-server.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,11 +1266,11 @@ app.post('/mcp', async (req, res) => {
12661266

12671267
// Per-Request Metadata Integrity Checks (Fields verification).
12681268
// A request missing any required `_meta` field is malformed: -32602 and,
1269-
// on HTTP, status 400 Bad Request.
1269+
// on HTTP, status 400 Bad Request. `clientInfo` is a SHOULD since spec
1270+
// PR #3002 and is never required.
12701271
if (
12711272
!meta ||
12721273
!meta['io.modelcontextprotocol/protocolVersion'] ||
1273-
!meta['io.modelcontextprotocol/clientInfo'] ||
12741274
!meta['io.modelcontextprotocol/clientCapabilities']
12751275
) {
12761276
return res.status(400).json({
@@ -1372,7 +1372,13 @@ app.post('/mcp', async (req, res) => {
13721372
// served on this path, so the capability must be declared too.
13731373
resources: {}
13741374
},
1375-
serverInfo: { name: 'everything-stateless-server', version: '1.0.0' }
1375+
// Spec PR #3002: server identity lives in the result `_meta`.
1376+
_meta: {
1377+
'io.modelcontextprotocol/serverInfo': {
1378+
name: 'everything-stateless-server',
1379+
version: '1.0.0'
1380+
}
1381+
}
13761382
}
13771383
});
13781384
}

src/mock-server/mock-server.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,10 @@ describe('createServerStateless', () => {
252252
);
253253
expect(status).toBe(200);
254254
expect(body.result.supportedVersions).toEqual(STATELESS_SPEC_VERSIONS);
255-
expect(body.result.serverInfo.name).toBe('conformance-mock-server');
255+
// Spec PR #3002: server identity lives in the result `_meta`.
256+
expect(body.result._meta['io.modelcontextprotocol/serverInfo'].name).toBe(
257+
'conformance-mock-server'
258+
);
256259
} finally {
257260
await srv.close();
258261
}

src/mock-server/stateless.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
/**
22
* Stateless mock server: 2026-x lifecycle (SEP-2575).
33
*
4-
* No initialize handshake. Validates `_meta` (protocolVersion / clientInfo /
5-
* clientCapabilities) and the `MCP-Protocol-Version` header on every request,
4+
* No initialize handshake. Validates `_meta` (protocolVersion /
5+
* clientCapabilities — `clientInfo` is a SHOULD since spec PR #3002 and is
6+
* never required) and the `MCP-Protocol-Version` header on every request,
67
* serves `server/discover`, and routes other methods to the supplied handlers.
78
* Implemented with raw express so it can front-run SDK support.
89
*/
@@ -14,9 +15,13 @@ import type { MockServer, RequestHandlers } from './index';
1415
import { STATELESS_SPEC_VERSIONS } from '../connection/select';
1516
import { capabilitiesFromHandlers } from './stateful';
1617

18+
/**
19+
* The required per-request `_meta` keys. `io.modelcontextprotocol/clientInfo`
20+
* is deliberately absent: spec PR #3002 demoted it to SHOULD, so a request
21+
* without it is valid.
22+
*/
1723
const META_KEYS = [
1824
'io.modelcontextprotocol/protocolVersion',
19-
'io.modelcontextprotocol/clientInfo',
2025
'io.modelcontextprotocol/clientCapabilities'
2126
] as const;
2227

@@ -156,7 +161,14 @@ export function validateStatelessRequest(
156161
result: withRequiredDraftResultFields(method, {
157162
supportedVersions,
158163
capabilities,
159-
serverInfo: { name: 'conformance-mock-server', version: '1.0.0' }
164+
// Spec PR #3002: server identity lives in the result `_meta`, not
165+
// the DiscoverResult body.
166+
_meta: {
167+
'io.modelcontextprotocol/serverInfo': {
168+
name: 'conformance-mock-server',
169+
version: '1.0.0'
170+
}
171+
}
160172
})
161173
}
162174
};

src/scenarios/client/request-metadata.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const STATUS_SEVERITY: Record<CheckStatus, number> = {
3232
export const DECLARED_CHECK_IDS = [
3333
'sep-2575-http-client-sends-version-header',
3434
'sep-2575-client-populates-meta',
35+
'sep-2575-client-sends-client-info',
3536
'sep-2575-http-version-header-matches-meta',
3637
'sep-2575-client-declares-roots-capability',
3738
'sep-2575-client-declares-sampling-capability',
@@ -161,17 +162,19 @@ export class RequestMetadataScenario implements Scenario {
161162

162163
// 2. "Every client request MUST include the following
163164
// io.modelcontextprotocol/* fields in _meta: protocolVersion,
164-
// clientInfo, clientCapabilities."
165+
// clientCapabilities." clientInfo is a SHOULD since spec PR #3002 and
166+
// is tracked by its own check below, so requirement levels never blend
167+
// in one check id.
165168
const hasClientInfo = meta?.['io.modelcontextprotocol/clientInfo'];
166169
const hasCapabilities =
167170
meta?.['io.modelcontextprotocol/clientCapabilities'];
168-
const metaIsValid = metaVersion && hasClientInfo && hasCapabilities;
171+
const metaIsValid = metaVersion && hasCapabilities;
169172

170173
this.addOrUpdateCheck({
171174
id: 'sep-2575-client-populates-meta',
172175
name: 'ClientPopulatesMeta',
173176
description:
174-
'Client populates _meta on every request with all three required fields',
177+
'Client populates _meta on every request with the required fields (protocolVersion, clientCapabilities)',
175178
status: metaIsValid ? 'SUCCESS' : 'FAILURE',
176179
timestamp: new Date().toISOString(),
177180
specReferences: [
@@ -183,6 +186,24 @@ export class RequestMetadataScenario implements Scenario {
183186
details: { method: request.method, meta }
184187
});
185188

189+
// 2b. clientInfo SHOULD (spec PR #3002): absence is a WARNING, never a
190+
// FAILURE — a dedicated id so adoption is trackable on its own.
191+
this.addOrUpdateCheck({
192+
id: 'sep-2575-client-sends-client-info',
193+
name: 'ClientSendsClientInfo',
194+
description:
195+
"Client SHOULD include io.modelcontextprotocol/clientInfo in every request's _meta (spec PR #3002)",
196+
status: hasClientInfo ? 'SUCCESS' : 'WARNING',
197+
timestamp: new Date().toISOString(),
198+
specReferences: [
199+
{
200+
id: 'SEP-2575',
201+
url: 'https://modelcontextprotocol.io/specification/draft/basic/index#meta'
202+
}
203+
],
204+
details: { method: request.method, meta }
205+
});
206+
186207
// 3. "The header value MUST match the io.modelcontextprotocol/protocolVersion
187208
// field carried in the request body's _meta." Only comparable when both
188209
// are present; absence is already covered by the two checks above, so

src/scenarios/server/stateless.ts

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,14 @@ export class ServerStatelessScenario implements ClientScenario {
2929
**Server Implementation Requirements:**
3030
3131
**Endpoints**:
32-
- \`server/discover\`: Returns supportedVersions, capabilities, and serverInfo metadata.
32+
- \`server/discover\`: Returns supportedVersions and capabilities; SHOULD identify itself via \`_meta['io.modelcontextprotocol/serverInfo']\` (spec PR #3002).
3333
- \`tools/call\`: Implement structural test tools like \`test_missing_capability\` requiring explicit capabilities in \`_meta\`.
3434
3535
**Grouped Specification Requirements**:
3636
37-
1. **Per-Request _meta Validation (5 Checks)**
38-
- Rejects requests missing \`_meta\` or lacking structural required internal subfields (\`protocolVersion\`, \`clientInfo\`, \`clientCapabilities\`) with a JSON-RPC \`-32602 Invalid params\` error signature and an HTTP status code \`400 Bad Request\`.
37+
1. **Per-Request _meta Validation (4 Checks)**
38+
- Rejects requests missing \`_meta\` or lacking structural required internal subfields (\`protocolVersion\`, \`clientCapabilities\`) with a JSON-RPC \`-32602 Invalid params\` error signature and an HTTP status code \`400 Bad Request\`.
39+
- Serves requests whose \`_meta\` omits \`clientInfo\` (a SHOULD since spec PR #3002 — servers MUST NOT require it).
3940
2. **Discovery & Capabilities (3 Checks)**
4041
- Implements \`server/discover\` mapping exact mandatory protocol elements.
4142
- Dynamically checks prompt capability declaration constraints, validates that active RPC handlers match advertised discovery capacities.
@@ -332,20 +333,8 @@ export class ServerStatelessScenario implements ClientScenario {
332333
},
333334
rpcId: 102
334335
},
335-
{
336-
slug: 'missing-client-info',
337-
description:
338-
'Rejects request with _meta missing io.modelcontextprotocol/clientInfo',
339-
params: {
340-
_meta: {
341-
'io.modelcontextprotocol/protocolVersion':
342-
validMeta['io.modelcontextprotocol/protocolVersion'],
343-
'io.modelcontextprotocol/clientCapabilities':
344-
validMeta['io.modelcontextprotocol/clientCapabilities']
345-
}
346-
},
347-
rpcId: 103
348-
},
336+
// No 'missing-client-info' case: spec PR #3002 demoted clientInfo to
337+
// SHOULD, so its absence is valid (asserted positively below).
349338
{
350339
slug: 'missing-client-capabilities',
351340
description:
@@ -411,6 +400,45 @@ export class ServerStatelessScenario implements ClientScenario {
411400
);
412401
}
413402

403+
// Positive companion (spec PR #3002): clientInfo is a SHOULD — a request
404+
// whose _meta omits it MUST be served, not rejected.
405+
const noClientInfoProbe = await sendRpc(
406+
'server/discover',
407+
{
408+
_meta: {
409+
'io.modelcontextprotocol/protocolVersion':
410+
validMeta['io.modelcontextprotocol/protocolVersion'],
411+
'io.modelcontextprotocol/clientCapabilities':
412+
validMeta['io.modelcontextprotocol/clientCapabilities']
413+
}
414+
},
415+
undefined,
416+
105
417+
).catch(() => null);
418+
const noClientInfoData: any = noClientInfoProbe?.data;
419+
await runCheck(
420+
'sep-2575-request-meta-client-info-optional',
421+
'RequestMetaClientInfoOptional',
422+
'Serves requests whose _meta omits io.modelcontextprotocol/clientInfo (clientInfo is a SHOULD).',
423+
() => {
424+
if (!noClientInfoProbe)
425+
return { error: 'clientInfo-less probe failed completely' };
426+
if (noClientInfoData?.error) {
427+
return {
428+
error: `Expected a result, got error ${noClientInfoData.error.code}: ${noClientInfoData.error.message}`,
429+
details: { response: noClientInfoData }
430+
};
431+
}
432+
if (noClientInfoProbe.res?.status !== 200) {
433+
return {
434+
error: `Expected HTTP 200, got status code ${noClientInfoProbe.res?.status}`,
435+
details: { response: noClientInfoData }
436+
};
437+
}
438+
return { details: { response: noClientInfoData } };
439+
}
440+
);
441+
414442
// ==========================================
415443
// 2. Discovery & Capabilities (4 Checks)
416444
// ==========================================
@@ -448,10 +476,12 @@ export class ServerStatelessScenario implements ClientScenario {
448476
() => {
449477
if (discoverRpcError)
450478
return { error: `Discovery failed: ${discoverRpcError.message}` };
479+
// Mandatory body fields per the final revision (spec PR #3002 removed
480+
// body serverInfo — identity is a SHOULD in the result _meta, checked
481+
// separately below).
451482
if (
452483
!discoverResult?.supportedVersions ||
453-
!discoverResult?.capabilities ||
454-
!discoverResult?.serverInfo
484+
!discoverResult?.capabilities
455485
) {
456486
return {
457487
error: 'Missing mandatory fields in discover response setup',
@@ -462,6 +492,38 @@ export class ServerStatelessScenario implements ClientScenario {
462492
}
463493
);
464494

495+
await runCheck(
496+
'sep-2575-server-identifies-in-result-meta',
497+
'ServerIdentifiesInResultMeta',
498+
"Servers SHOULD identify themselves via _meta['io.modelcontextprotocol/serverInfo'] on responses (spec PR #3002).",
499+
() => {
500+
if (discoverRpcError)
501+
// Discover itself failed (already a FAILURE on the implements-
502+
// discover check): nothing is known about identity, so the gap
503+
// must not read as an intentional absence.
504+
return {
505+
skipped: true,
506+
details: {
507+
note: `Prerequisite missing: ${discoverRpcError.message}`
508+
}
509+
};
510+
const metaServerInfo =
511+
discoverResult?._meta?.['io.modelcontextprotocol/serverInfo'];
512+
if (!metaServerInfo?.name || !metaServerInfo?.version) {
513+
const bodyServerInfo = discoverResult?.serverInfo;
514+
return {
515+
// SHOULD-level: WARNING, never FAILURE.
516+
warning: true,
517+
error: bodyServerInfo
518+
? 'serverInfo found only in the pre-#3002 result body, not in _meta'
519+
: 'No serverInfo in the discover result _meta',
520+
details: { result: discoverResult }
521+
};
522+
}
523+
return { details: { serverInfo: metaServerInfo } };
524+
}
525+
);
526+
465527
await runCheck(
466528
'sep-2575-server-declares-prompts-in-discover',
467529
'ServerDeclaresPromptsInDiscover',

src/spec-types/2025-03-26.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1234,9 +1234,7 @@ export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult;
12341234

12351235
/* Server messages */
12361236
export type ServerRequest =
1237-
| PingRequest
1238-
| CreateMessageRequest
1239-
| ListRootsRequest;
1237+
PingRequest | CreateMessageRequest | ListRootsRequest;
12401238

12411239
export type ServerNotification =
12421240
| CancelledNotification

src/spec-types/2025-06-18.ts

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
* @category JSON-RPC
77
*/
88
export type JSONRPCMessage =
9-
| JSONRPCRequest
10-
| JSONRPCNotification
11-
| JSONRPCResponse
12-
| JSONRPCError;
9+
JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError;
1310

1411
/** @internal */
1512
export const LATEST_PROTOCOL_VERSION = "2025-06-18";
@@ -1134,11 +1131,7 @@ export interface Annotations {
11341131
* @category Content
11351132
*/
11361133
export type ContentBlock =
1137-
| TextContent
1138-
| ImageContent
1139-
| AudioContent
1140-
| ResourceLink
1141-
| EmbeddedResource;
1134+
TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;
11421135

11431136
/**
11441137
* Text provided to or from an LLM.
@@ -1490,10 +1483,7 @@ export interface ElicitRequest extends Request {
14901483
* @category `elicitation/create`
14911484
*/
14921485
export type PrimitiveSchemaDefinition =
1493-
| StringSchema
1494-
| NumberSchema
1495-
| BooleanSchema
1496-
| EnumSchema;
1486+
StringSchema | NumberSchema | BooleanSchema | EnumSchema;
14971487

14981488
/**
14991489
* @category `elicitation/create`
@@ -1586,18 +1576,12 @@ export type ClientNotification =
15861576

15871577
/** @internal */
15881578
export type ClientResult =
1589-
| EmptyResult
1590-
| CreateMessageResult
1591-
| ListRootsResult
1592-
| ElicitResult;
1579+
EmptyResult | CreateMessageResult | ListRootsResult | ElicitResult;
15931580

15941581
/* Server messages */
15951582
/** @internal */
15961583
export type ServerRequest =
1597-
| PingRequest
1598-
| CreateMessageRequest
1599-
| ListRootsRequest
1600-
| ElicitRequest;
1584+
PingRequest | CreateMessageRequest | ListRootsRequest | ElicitRequest;
16011585

16021586
/** @internal */
16031587
export type ServerNotification =

0 commit comments

Comments
 (0)