Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1266,11 +1266,11 @@ app.post('/mcp', async (req, res) => {

// Per-Request Metadata Integrity Checks (Fields verification).
// A request missing any required `_meta` field is malformed: -32602 and,
// on HTTP, status 400 Bad Request.
// on HTTP, status 400 Bad Request. `clientInfo` is a SHOULD since spec
// PR #3002 and is never required.
if (
!meta ||
!meta['io.modelcontextprotocol/protocolVersion'] ||
!meta['io.modelcontextprotocol/clientInfo'] ||
!meta['io.modelcontextprotocol/clientCapabilities']
) {
return res.status(400).json({
Expand Down Expand Up @@ -1372,7 +1372,13 @@ app.post('/mcp', async (req, res) => {
// served on this path, so the capability must be declared too.
resources: {}
},
serverInfo: { name: 'everything-stateless-server', version: '1.0.0' }
// Spec PR #3002: server identity lives in the result `_meta`.
_meta: {
'io.modelcontextprotocol/serverInfo': {
name: 'everything-stateless-server',
version: '1.0.0'
}
}
}
});
}
Expand Down
5 changes: 4 additions & 1 deletion src/mock-server/mock-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,10 @@ describe('createServerStateless', () => {
);
expect(status).toBe(200);
expect(body.result.supportedVersions).toEqual(STATELESS_SPEC_VERSIONS);
expect(body.result.serverInfo.name).toBe('conformance-mock-server');
// Spec PR #3002: server identity lives in the result `_meta`.
expect(body.result._meta['io.modelcontextprotocol/serverInfo'].name).toBe(
'conformance-mock-server'
);
} finally {
await srv.close();
}
Expand Down
20 changes: 16 additions & 4 deletions src/mock-server/stateless.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/**
* Stateless mock server: 2026-x lifecycle (SEP-2575).
*
* No initialize handshake. Validates `_meta` (protocolVersion / clientInfo /
* clientCapabilities) and the `MCP-Protocol-Version` header on every request,
* No initialize handshake. Validates `_meta` (protocolVersion /
* clientCapabilities — `clientInfo` is a SHOULD since spec PR #3002 and is
* never required) and the `MCP-Protocol-Version` header on every request,
* serves `server/discover`, and routes other methods to the supplied handlers.
* Implemented with raw express so it can front-run SDK support.
*/
Expand All @@ -14,9 +15,13 @@ import type { MockServer, RequestHandlers } from './index';
import { STATELESS_SPEC_VERSIONS } from '../connection/select';
import { capabilitiesFromHandlers } from './stateful';

/**
* The required per-request `_meta` keys. `io.modelcontextprotocol/clientInfo`
* is deliberately absent: spec PR #3002 demoted it to SHOULD, so a request
* without it is valid.
*/
const META_KEYS = [
'io.modelcontextprotocol/protocolVersion',
'io.modelcontextprotocol/clientInfo',
'io.modelcontextprotocol/clientCapabilities'
] as const;

Expand Down Expand Up @@ -156,7 +161,14 @@ export function validateStatelessRequest(
result: withRequiredDraftResultFields(method, {
supportedVersions,
capabilities,
serverInfo: { name: 'conformance-mock-server', version: '1.0.0' }
// Spec PR #3002: server identity lives in the result `_meta`, not
// the DiscoverResult body.
_meta: {
'io.modelcontextprotocol/serverInfo': {
name: 'conformance-mock-server',
version: '1.0.0'
}
}
})
}
};
Expand Down
27 changes: 24 additions & 3 deletions src/scenarios/client/request-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const STATUS_SEVERITY: Record<CheckStatus, number> = {
export const DECLARED_CHECK_IDS = [
'sep-2575-http-client-sends-version-header',
'sep-2575-client-populates-meta',
'sep-2575-client-sends-client-info',
'sep-2575-http-version-header-matches-meta',
'sep-2575-client-declares-roots-capability',
'sep-2575-client-declares-sampling-capability',
Expand Down Expand Up @@ -161,17 +162,19 @@ export class RequestMetadataScenario implements Scenario {

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

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

// 2b. clientInfo SHOULD (spec PR #3002): absence is a WARNING, never a
// FAILURE — a dedicated id so adoption is trackable on its own.
this.addOrUpdateCheck({
id: 'sep-2575-client-sends-client-info',
name: 'ClientSendsClientInfo',
description:
"Client SHOULD include io.modelcontextprotocol/clientInfo in every request's _meta (spec PR #3002)",
status: hasClientInfo ? 'SUCCESS' : 'WARNING',
timestamp: new Date().toISOString(),
specReferences: [
{
id: 'SEP-2575',
url: 'https://modelcontextprotocol.io/specification/draft/basic/index#meta'
}
],
details: { method: request.method, meta }
});

// 3. "The header value MUST match the io.modelcontextprotocol/protocolVersion
// field carried in the request body's _meta." Only comparable when both
// are present; absence is already covered by the two checks above, so
Expand Down
100 changes: 81 additions & 19 deletions src/scenarios/server/stateless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ export class ServerStatelessScenario implements ClientScenario {
**Server Implementation Requirements:**

**Endpoints**:
- \`server/discover\`: Returns supportedVersions, capabilities, and serverInfo metadata.
- \`server/discover\`: Returns supportedVersions and capabilities; SHOULD identify itself via \`_meta['io.modelcontextprotocol/serverInfo']\` (spec PR #3002).
- \`tools/call\`: Implement structural test tools like \`test_missing_capability\` requiring explicit capabilities in \`_meta\`.

**Grouped Specification Requirements**:

1. **Per-Request _meta Validation (5 Checks)**
- 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\`.
1. **Per-Request _meta Validation (4 Checks)**
- 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\`.
- Serves requests whose \`_meta\` omits \`clientInfo\` (a SHOULD since spec PR #3002 — servers MUST NOT require it).
2. **Discovery & Capabilities (3 Checks)**
- Implements \`server/discover\` mapping exact mandatory protocol elements.
- Dynamically checks prompt capability declaration constraints, validates that active RPC handlers match advertised discovery capacities.
Expand Down Expand Up @@ -332,20 +333,8 @@ export class ServerStatelessScenario implements ClientScenario {
},
rpcId: 102
},
{
slug: 'missing-client-info',
description:
'Rejects request with _meta missing io.modelcontextprotocol/clientInfo',
params: {
_meta: {
'io.modelcontextprotocol/protocolVersion':
validMeta['io.modelcontextprotocol/protocolVersion'],
'io.modelcontextprotocol/clientCapabilities':
validMeta['io.modelcontextprotocol/clientCapabilities']
}
},
rpcId: 103
},
// No 'missing-client-info' case: spec PR #3002 demoted clientInfo to
// SHOULD, so its absence is valid (asserted positively below).
{
slug: 'missing-client-capabilities',
description:
Expand Down Expand Up @@ -411,6 +400,45 @@ export class ServerStatelessScenario implements ClientScenario {
);
}

// Positive companion (spec PR #3002): clientInfo is a SHOULD — a request
// whose _meta omits it MUST be served, not rejected.
const noClientInfoProbe = await sendRpc(
'server/discover',
{
_meta: {
'io.modelcontextprotocol/protocolVersion':
validMeta['io.modelcontextprotocol/protocolVersion'],
'io.modelcontextprotocol/clientCapabilities':
validMeta['io.modelcontextprotocol/clientCapabilities']
}
},
undefined,
105
).catch(() => null);
const noClientInfoData: any = noClientInfoProbe?.data;
await runCheck(
'sep-2575-request-meta-client-info-optional',
'RequestMetaClientInfoOptional',
'Serves requests whose _meta omits io.modelcontextprotocol/clientInfo (clientInfo is a SHOULD).',
() => {
if (!noClientInfoProbe)
return { error: 'clientInfo-less probe failed completely' };
if (noClientInfoData?.error) {
return {
error: `Expected a result, got error ${noClientInfoData.error.code}: ${noClientInfoData.error.message}`,
details: { response: noClientInfoData }
};
}
if (noClientInfoProbe.res?.status !== 200) {
return {
error: `Expected HTTP 200, got status code ${noClientInfoProbe.res?.status}`,
details: { response: noClientInfoData }
};
}
return { details: { response: noClientInfoData } };
}
);

// ==========================================
// 2. Discovery & Capabilities (4 Checks)
// ==========================================
Expand Down Expand Up @@ -448,10 +476,12 @@ export class ServerStatelessScenario implements ClientScenario {
() => {
if (discoverRpcError)
return { error: `Discovery failed: ${discoverRpcError.message}` };
// Mandatory body fields per the final revision (spec PR #3002 removed
// body serverInfo — identity is a SHOULD in the result _meta, checked
// separately below).
if (
!discoverResult?.supportedVersions ||
!discoverResult?.capabilities ||
!discoverResult?.serverInfo
!discoverResult?.capabilities
) {
return {
error: 'Missing mandatory fields in discover response setup',
Expand All @@ -462,6 +492,38 @@ export class ServerStatelessScenario implements ClientScenario {
}
);

await runCheck(
'sep-2575-server-identifies-in-result-meta',
'ServerIdentifiesInResultMeta',
"Servers SHOULD identify themselves via _meta['io.modelcontextprotocol/serverInfo'] on responses (spec PR #3002).",
() => {
if (discoverRpcError)
// Discover itself failed (already a FAILURE on the implements-
// discover check): nothing is known about identity, so the gap
// must not read as an intentional absence.
return {
skipped: true,
details: {
note: `Prerequisite missing: ${discoverRpcError.message}`
}
};
const metaServerInfo =
discoverResult?._meta?.['io.modelcontextprotocol/serverInfo'];
if (!metaServerInfo?.name || !metaServerInfo?.version) {
const bodyServerInfo = discoverResult?.serverInfo;
return {
// SHOULD-level: WARNING, never FAILURE.
warning: true,
error: bodyServerInfo
? 'serverInfo found only in the pre-#3002 result body, not in _meta'
: 'No serverInfo in the discover result _meta',
details: { result: discoverResult }
};
}
return { details: { serverInfo: metaServerInfo } };
}
);

await runCheck(
'sep-2575-server-declares-prompts-in-discover',
'ServerDeclaresPromptsInDiscover',
Expand Down
4 changes: 1 addition & 3 deletions src/spec-types/2025-03-26.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1234,9 +1234,7 @@ export type ClientResult = EmptyResult | CreateMessageResult | ListRootsResult;

/* Server messages */
export type ServerRequest =
| PingRequest
| CreateMessageRequest
| ListRootsRequest;
PingRequest | CreateMessageRequest | ListRootsRequest;

export type ServerNotification =
| CancelledNotification
Expand Down
26 changes: 5 additions & 21 deletions src/spec-types/2025-06-18.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@
* @category JSON-RPC
*/
export type JSONRPCMessage =
| JSONRPCRequest
| JSONRPCNotification
| JSONRPCResponse
| JSONRPCError;
JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError;

/** @internal */
export const LATEST_PROTOCOL_VERSION = "2025-06-18";
Expand Down Expand Up @@ -1134,11 +1131,7 @@ export interface Annotations {
* @category Content
*/
export type ContentBlock =
| TextContent
| ImageContent
| AudioContent
| ResourceLink
| EmbeddedResource;
TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource;

/**
* Text provided to or from an LLM.
Expand Down Expand Up @@ -1490,10 +1483,7 @@ export interface ElicitRequest extends Request {
* @category `elicitation/create`
*/
export type PrimitiveSchemaDefinition =
| StringSchema
| NumberSchema
| BooleanSchema
| EnumSchema;
StringSchema | NumberSchema | BooleanSchema | EnumSchema;

/**
* @category `elicitation/create`
Expand Down Expand Up @@ -1586,18 +1576,12 @@ export type ClientNotification =

/** @internal */
export type ClientResult =
| EmptyResult
| CreateMessageResult
| ListRootsResult
| ElicitResult;
EmptyResult | CreateMessageResult | ListRootsResult | ElicitResult;

/* Server messages */
/** @internal */
export type ServerRequest =
| PingRequest
| CreateMessageRequest
| ListRootsRequest
| ElicitRequest;
PingRequest | CreateMessageRequest | ListRootsRequest | ElicitRequest;

/** @internal */
export type ServerNotification =
Expand Down
Loading
Loading