test(conformance): bump referee to 0.2.0-alpha.9; arm SEP-2575 diagnostic fixtures; fix post-dispatch -32021 HTTP status (#2399)
This commit is contained in:
parent
1ea960b5bb
commit
3c7ddafa05
13 changed files with 211 additions and 75 deletions
6
.changeset/post-dispatch-32021-http-400.md
Normal file
6
.changeset/post-dispatch-32021-http-400.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
'@modelcontextprotocol/server': patch
|
||||
'@modelcontextprotocol/core-internal': patch
|
||||
---
|
||||
|
||||
Return HTTP 400 for a `MissingRequiredClientCapabilityError` (`-32021`) produced after dispatch. The spec mandates `400 Bad Request` for this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — the `input_required` gate rejecting an embedded request whose required capability the caller did not declare — surfaced in-band on HTTP 200. The JSON-RPC error body is unchanged, every other error code (including a handler relaying a downstream peer's `-32020`/`-32022`) keeps the origin-keyed in-band behavior, and the mapping only applies while the response is uncommitted: an exchange that already streamed — or one hosted with `responseMode: 'sse'`, which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream.
|
||||
|
|
@ -157,6 +157,18 @@ fetch-shaped handler.
|
|||
> `toNodeHandler(handler)` and add the `@modelcontextprotocol/node` import.
|
||||
> `NodeIncomingMessageLike` / `NodeServerResponseLike` are now exported from
|
||||
> `@modelcontextprotocol/node`, not `@modelcontextprotocol/server`.
|
||||
>
|
||||
> Also: a `MissingRequiredClientCapabilityError` (`-32021`) produced **after** dispatch
|
||||
> — the `input_required` gate refusing an embedded request whose capability the caller
|
||||
> did not declare — now answers HTTP **400** (earlier alphas surfaced it in-band on
|
||||
> 200). The spec mandates 400 for this error wherever it arises; the JSON-RPC body is
|
||||
> unchanged. This applies to a handler-thrown `-32021` too: a proxy relaying a
|
||||
> downstream server's `-32021` should translate it (its `requiredCapabilities`
|
||||
> describes the downstream hop's envelope) rather than rethrow the bare error. Every
|
||||
> other handler-produced code (including a relayed `-32020`/`-32022`)
|
||||
> keeps the in-band 200, and an exchange whose response stream is already open — the
|
||||
> handler streamed first, or `responseMode: 'sse'` — keeps its committed 200 and
|
||||
> carries the error in-stream.
|
||||
|
||||
### Server over stdio / long-lived connections: `serveStdio`
|
||||
|
||||
|
|
|
|||
|
|
@ -371,7 +371,9 @@ export const INBOUND_VALIDATION_LADDER: readonly InboundValidationRungDescriptor
|
|||
* the ladder (or a pre-handler protocol gate) produced. Errors produced by
|
||||
* request handlers — whatever their code — stay in-band on HTTP 200, and are
|
||||
* never mapped to an HTTP status by this table; in particular `-32603` and
|
||||
* domain-specific codes never become a blanket 500.
|
||||
* domain-specific codes never become a blanket 500. The single exception is
|
||||
* `MissingRequiredClientCapability` (-32021) — see
|
||||
* {@linkcode httpStatusForErrorCode}.
|
||||
*
|
||||
* `-32602` (invalid params) deliberately has NO entry: the only invalid-params
|
||||
* rejection that maps to HTTP 400 is the classifier's own envelope rung
|
||||
|
|
@ -390,11 +392,24 @@ export const LADDER_ERROR_HTTP_STATUS: Readonly<Record<number, number>> = {
|
|||
/**
|
||||
* The HTTP status to answer a JSON-RPC error with, keyed on the error's
|
||||
* origin. `in-band` errors (anything produced by a request handler) are
|
||||
* always HTTP 200 — the JSON-RPC error response is the payload, not an HTTP
|
||||
* failure. `ladder` errors map through {@linkcode LADDER_ERROR_HTTP_STATUS}.
|
||||
* HTTP 200 — the JSON-RPC error response is the payload, not an HTTP
|
||||
* failure — with ONE exception: `MissingRequiredClientCapability` (-32021),
|
||||
* whose 400 the spec mandates on the error itself with no origin condition,
|
||||
* and which the SDK genuinely produces after dispatch (the `input_required`
|
||||
* capability gate). A handler relaying some downstream peer's `-32020`/`-32022`
|
||||
* is NOT that peer's spec error and stays in-band like every other handler
|
||||
* code. `ladder` errors map through {@linkcode LADDER_ERROR_HTTP_STATUS}.
|
||||
*
|
||||
* The per-request transport intentionally does NOT delegate to this function:
|
||||
* its `?? 400` ladder fallback is only correct for entry-gate codes known to
|
||||
* the table, and would wrongly map dispatch-window errors outside it (a
|
||||
* window `-32602` must stay in-band on 200). The transport indexes the table
|
||||
* directly; keep the two in agreement when editing either.
|
||||
*/
|
||||
export function httpStatusForErrorCode(code: number, origin: 'ladder' | 'in-band'): number {
|
||||
if (origin === 'in-band') return 200;
|
||||
if (origin === 'in-band') {
|
||||
return code === ProtocolErrorCode.MissingRequiredClientCapability ? 400 : 200;
|
||||
}
|
||||
return LADDER_ERROR_HTTP_STATUS[code] ?? 400;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,13 @@
|
|||
*
|
||||
* - errors produced by the validation ladder or a pre-handler protocol gate
|
||||
* map through the table (`-32601` → 404; the small mandated 400 set);
|
||||
* - everything a request handler produces — whatever its code, including
|
||||
* `-32603`, `-32602` and domain-specific codes — stays in-band on HTTP 200,
|
||||
* never a blanket 500;
|
||||
* - everything a request handler produces — including `-32603`, `-32602` and
|
||||
* domain-specific codes — stays in-band on HTTP 200, never a blanket 500;
|
||||
* - EXCEPT `-32021` (MissingRequiredClientCapability): the spec mandates its
|
||||
* 400 per-error with no origin condition, and the `input_required`
|
||||
* capability gate genuinely emits it after dispatch — so it alone is
|
||||
* status-mapped wherever it arises. A handler relaying a downstream peer's
|
||||
* `-32020`/`-32022` is not that peer's spec error and stays in-band;
|
||||
* - `-32602` deliberately has no table entry: the classifier's envelope rung
|
||||
* carries its own HTTP 400 and is the only invalid-params rejection that
|
||||
* maps to 400.
|
||||
|
|
@ -45,7 +49,7 @@ describe('the status matrix — pinned cells', () => {
|
|||
expect(httpStatusForErrorCode(row.code, 'ladder')).toBe(row.status);
|
||||
});
|
||||
|
||||
test('every code stays in-band on HTTP 200 when handler-originated — including internal errors and domain codes', () => {
|
||||
test('every code except -32021 stays in-band on HTTP 200 when handler-originated — including internal errors and domain codes', () => {
|
||||
const handlerCodes = [
|
||||
ProtocolErrorCode.InternalError,
|
||||
ProtocolErrorCode.InvalidParams,
|
||||
|
|
@ -61,6 +65,16 @@ describe('the status matrix — pinned cells', () => {
|
|||
}
|
||||
});
|
||||
|
||||
test('-32021 is the single code-keyed exception: its spec-mandated 400 applies wherever it arises', () => {
|
||||
expect(httpStatusForErrorCode(ProtocolErrorCode.MissingRequiredClientCapability, 'in-band')).toBe(400);
|
||||
expect(httpStatusForErrorCode(ProtocolErrorCode.MissingRequiredClientCapability, 'ladder')).toBe(400);
|
||||
// The relay contract for the OTHER two spec-defined HTTP errors is
|
||||
// origin-keyed: a handler-relayed -32020/-32022 is not this server's
|
||||
// spec error and stays in-band.
|
||||
expect(httpStatusForErrorCode(HEADER_MISMATCH_ERROR_CODE, 'in-band')).toBe(200);
|
||||
expect(httpStatusForErrorCode(ProtocolErrorCode.UnsupportedProtocolVersion, 'in-band')).toBe(200);
|
||||
});
|
||||
|
||||
test('-32603 never becomes a blanket 500: handler-originated internal errors are in-band', () => {
|
||||
expect(LADDER_ERROR_HTTP_STATUS[ProtocolErrorCode.InternalError]).toBeUndefined();
|
||||
expect(httpStatusForErrorCode(ProtocolErrorCode.InternalError, 'in-band')).toBe(200);
|
||||
|
|
@ -71,12 +85,19 @@ describe('the status matrix — pinned cells', () => {
|
|||
expect(httpStatusForErrorCode(ProtocolErrorCode.InvalidParams, 'in-band')).toBe(200);
|
||||
});
|
||||
|
||||
test('the table is exactly the mandated set (no silent growth)', () => {
|
||||
expect(
|
||||
Object.keys(LADDER_ERROR_HTTP_STATUS)
|
||||
.map(Number)
|
||||
.sort((a, b) => a - b)
|
||||
).toEqual([-32_700, -32_601, -32_600, -32_022, -32_021, -32_020].sort((a, b) => a - b));
|
||||
test('the table is exactly the mandated set, keys and values (no silent growth)', () => {
|
||||
// The parse-error and invalid-request rows joined the table when the
|
||||
// status matrix was completed alongside the cache fill / capability
|
||||
// gate work; they were previously carried only by the classifier's own
|
||||
// httpStatus on the rejection outcomes (same 400, now table-visible).
|
||||
expect(LADDER_ERROR_HTTP_STATUS).toEqual({
|
||||
[-32_700]: 400,
|
||||
[-32_601]: 404,
|
||||
[-32_600]: 400,
|
||||
[-32_022]: 400,
|
||||
[-32_021]: 400,
|
||||
[-32_020]: 400
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -20,13 +20,7 @@
|
|||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { InboundHttpRequest, InboundLadderRejection } from '../../src/shared/inboundClassification';
|
||||
import {
|
||||
classifyInboundRequest,
|
||||
httpStatusForErrorCode,
|
||||
INBOUND_VALIDATION_LADDER,
|
||||
LADDER_ERROR_HTTP_STATUS,
|
||||
modernOnlyStrictRejection
|
||||
} from '../../src/shared/inboundClassification';
|
||||
import { classifyInboundRequest, INBOUND_VALIDATION_LADDER, modernOnlyStrictRejection } from '../../src/shared/inboundClassification';
|
||||
import { CLIENT_CAPABILITIES_META_KEY, CLIENT_INFO_META_KEY, PROTOCOL_VERSION_META_KEY } from '../../src/types/constants';
|
||||
|
||||
const MODERN_REVISION = '2026-07-28';
|
||||
|
|
@ -397,37 +391,6 @@ describe('the validation ladder as data', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('HTTP status mapping for ladder-originated errors (origin-keyed)', () => {
|
||||
test('the table maps exactly the ladder-originated codes', () => {
|
||||
// The parse-error and invalid-request rows joined the table when the
|
||||
// status matrix was completed alongside the cache fill / capability
|
||||
// gate work; they were previously carried only by the classifier's own
|
||||
// httpStatus on the rejection outcomes (same 400, now table-visible).
|
||||
expect(LADDER_ERROR_HTTP_STATUS).toEqual({
|
||||
[-32_700]: 400,
|
||||
[-32_601]: 404,
|
||||
[-32_600]: 400,
|
||||
[-32_022]: 400,
|
||||
[-32_021]: 400,
|
||||
[-32_020]: 400
|
||||
});
|
||||
});
|
||||
|
||||
test('the table never maps invalid params: the classifier envelope short-circuit is the only -32602 -> 400 source', () => {
|
||||
expect(Object.keys(LADDER_ERROR_HTTP_STATUS)).not.toContain(String(-32_602));
|
||||
expect(httpStatusForErrorCode(-32_602, 'in-band')).toBe(200);
|
||||
});
|
||||
|
||||
test('handler-originated errors stay in-band on HTTP 200, whatever their code', () => {
|
||||
for (const code of [-32_603, -32_602, -32_601, -32_022, -32_002, -32_000, 1234]) {
|
||||
expect(httpStatusForErrorCode(code, 'in-band')).toBe(200);
|
||||
}
|
||||
});
|
||||
|
||||
test('ladder-originated codes map to their HTTP statuses', () => {
|
||||
expect(httpStatusForErrorCode(-32_601, 'ladder')).toBe(404);
|
||||
expect(httpStatusForErrorCode(-32_022, 'ladder')).toBe(400);
|
||||
expect(httpStatusForErrorCode(-32_021, 'ladder')).toBe(400);
|
||||
expect(httpStatusForErrorCode(-32_020, 'ladder')).toBe(400);
|
||||
});
|
||||
});
|
||||
// The error→HTTP-status policy (LADDER_ERROR_HTTP_STATUS / httpStatusForErrorCode)
|
||||
// is pinned in errorHttpStatusMatrix.test.ts — the single test surface for that
|
||||
// module. This file pins the classifier's cells only.
|
||||
|
|
|
|||
|
|
@ -669,9 +669,11 @@ export function createMcpHandler(factory: McpServerFactory, options: CreateMcpHa
|
|||
// Pre-dispatch capability gate: a request to a method whose processing
|
||||
// structurally requires a client capability the request's validated
|
||||
// envelope did not declare is refused here, before any instance is
|
||||
// constructed or dispatched. Answering at the entry pins the
|
||||
// spec-mandated HTTP 400 for this error; a handler-time emission would
|
||||
// surface in-band on HTTP 200.
|
||||
// constructed or dispatched. Answering at the entry short-circuits
|
||||
// before factory construction and pins the spec-mandated HTTP 400 for
|
||||
// this error unconditionally; a handler-time -32021 (the
|
||||
// input_required gate) also maps to 400 at the per-request transport,
|
||||
// but only while no response has been committed.
|
||||
if (route.messageKind === 'request') {
|
||||
const required = requiredClientCapabilitiesForRequest(route.message.method);
|
||||
if (required !== undefined) {
|
||||
|
|
|
|||
|
|
@ -27,10 +27,14 @@
|
|||
* stream, and request-header validation (which belongs to middleware). The
|
||||
* exchange is single-use; serving another request requires a new transport
|
||||
* (and, in the per-request serving model, a fresh server instance).
|
||||
*
|
||||
* Consumers wiring this transport into a custom entry (instead of
|
||||
* `createMcpHandler`) inherit the spec's per-error HTTP mandate with it: a
|
||||
* terminal `MissingRequiredClientCapabilityError` (-32021) MUST answer 400,
|
||||
* which this transport applies whenever the response is still uncommitted.
|
||||
*/
|
||||
import type {
|
||||
AuthInfo,
|
||||
JSONRPCErrorResponse,
|
||||
JSONRPCMessage,
|
||||
JSONRPCNotification,
|
||||
JSONRPCRequest,
|
||||
|
|
@ -45,6 +49,7 @@ import {
|
|||
isJSONRPCRequest,
|
||||
isJSONRPCResultResponse,
|
||||
LADDER_ERROR_HTTP_STATUS,
|
||||
ProtocolErrorCode,
|
||||
SdkError,
|
||||
SdkErrorCode
|
||||
} from '@modelcontextprotocol/core-internal';
|
||||
|
|
@ -252,13 +257,27 @@ export class PerRequestHTTPServerTransport implements Transport {
|
|||
// validation ladder, the era registry gate and handoff check, a
|
||||
// missing handler — are answered with the mapped HTTP status from
|
||||
// the ladder table. Handler-produced errors, whatever their code,
|
||||
// stay in-band on HTTP 200. Ladder rejections keep that mapped
|
||||
// status in every response mode (the SSE upgrade is deferred to
|
||||
// the first actual send), so a forced-`sse` exchange still
|
||||
// answers pre-dispatch rejections as plain HTTP errors.
|
||||
// stay in-band on HTTP 200 — except
|
||||
// MissingRequiredClientCapability (-32021), whose 400 the spec
|
||||
// mandates per-error with no origin condition and whose only
|
||||
// SDK-produced post-window source (the input_required capability
|
||||
// gate) cannot fire any earlier — a handler-minted -32021 gets
|
||||
// the same 400; a handler RELAYING a downstream peer's
|
||||
// -32020/-32022 is not that peer's spec error and stays in-band.
|
||||
// Must agree with httpStatusForErrorCode (core-internal), which
|
||||
// is deliberately NOT called here: its `?? 400` ladder fallback
|
||||
// would wrongly map window codes outside the table.
|
||||
// The mapping applies only while no response has been committed:
|
||||
// once the stream is open — the handler streamed first, or the
|
||||
// exchange is forced-`sse` (which settles its 200 at dispatch
|
||||
// end) — the status is on the wire and the error rides the
|
||||
// stream. Pre-dispatch ladder rejections always precede the
|
||||
// forced-`sse` upgrade, so they keep their mapped status in every
|
||||
// response mode.
|
||||
const errorCode = isJSONRPCErrorResponse(message) ? message.error.code : undefined;
|
||||
const ladderStatus =
|
||||
this._dispatchWindowOpen && isJSONRPCErrorResponse(message)
|
||||
? LADDER_ERROR_HTTP_STATUS[(message as JSONRPCErrorResponse).error.code]
|
||||
errorCode !== undefined && (this._dispatchWindowOpen || errorCode === ProtocolErrorCode.MissingRequiredClientCapability)
|
||||
? LADDER_ERROR_HTTP_STATUS[errorCode]
|
||||
: undefined;
|
||||
if (ladderStatus !== undefined && this._sse === undefined) {
|
||||
this.settleResponse(Response.json(message, { status: ladderStatus, headers: { 'Content-Type': 'application/json' } }));
|
||||
|
|
|
|||
|
|
@ -199,6 +199,9 @@ describe('HTTP status mapping', () => {
|
|||
});
|
||||
|
||||
it('keeps a handler-thrown unsupported-protocol-version error in-band on HTTP 200', async () => {
|
||||
// A handler relaying a downstream peer's -32022 is not THIS server
|
||||
// rejecting the caller's version; like the -32601 relay above it must
|
||||
// not be re-mapped just because the ladder table maps that code.
|
||||
const { server } = modernServer({
|
||||
toolsCallHandler: async () => {
|
||||
throw new ProtocolError(-32_022, 'Unsupported protocol version: 2099-01-01');
|
||||
|
|
@ -210,6 +213,41 @@ describe('HTTP status mapping', () => {
|
|||
expect(errorOf(await response.json())?.code).toBe(-32_022);
|
||||
});
|
||||
|
||||
it('maps a post-dispatch -32021 (MissingRequiredClientCapability) to HTTP 400: the spec mandates that status per-error', async () => {
|
||||
const { server } = modernServer({
|
||||
toolsCallHandler: async () => {
|
||||
throw new ProtocolError(-32_021, 'Missing required client capabilities: sampling', {
|
||||
requiredCapabilities: { sampling: {} }
|
||||
});
|
||||
}
|
||||
});
|
||||
const transport = await connectedTransport(server);
|
||||
const response = await transport.handleMessage(toolsCall());
|
||||
expect(response.status).toBe(400);
|
||||
// The spec shape: `requiredCapabilities` is a ClientCapabilities
|
||||
// OBJECT, never an array.
|
||||
expect(errorOf(await response.json())).toMatchObject({ code: -32_021, data: { requiredCapabilities: { sampling: {} } } });
|
||||
});
|
||||
|
||||
it('leaves a post-dispatch -32021 on the already-open HTTP 200 stream when the handler streamed first', async () => {
|
||||
// Once the lazy SSE upgrade has happened, the 200 is committed — and
|
||||
// the error must still REACH the client as the stream's terminal
|
||||
// frame rather than being swallowed by the status-mapping arm.
|
||||
const { server } = modernServer({
|
||||
toolsCallHandler: async ctx => {
|
||||
await ctx.mcpReq.notify({ method: 'notifications/progress', params: { progressToken: 'capability-test', progress: 1 } });
|
||||
throw new ProtocolError(-32_021, 'Missing required client capabilities: sampling');
|
||||
}
|
||||
});
|
||||
const transport = await connectedTransport(server);
|
||||
const response = await transport.handleMessage(toolsCall());
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.headers.get('content-type')).toBe('text/event-stream');
|
||||
const frames = (await response.text()).split('\n\n').filter(frame => frame.includes('data: '));
|
||||
const terminal = frames.at(-1)!;
|
||||
expect(JSON.parse(terminal.split('data: ')[1]!)).toMatchObject({ id: 1, error: { code: -32_021 } });
|
||||
});
|
||||
|
||||
it('keeps handler-produced invalid-params errors in-band on HTTP 200 (never status-mapped)', async () => {
|
||||
const { server } = modernServer({
|
||||
toolsCallHandler: async () => {
|
||||
|
|
|
|||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
|
@ -1855,8 +1855,8 @@ importers:
|
|||
specifier: workspace:^
|
||||
version: link:../../packages/client
|
||||
'@modelcontextprotocol/conformance':
|
||||
specifier: 0.2.0-alpha.7
|
||||
version: 0.2.0-alpha.7(@cfworker/json-schema@4.1.1)
|
||||
specifier: 0.2.0-alpha.9
|
||||
version: 0.2.0-alpha.9(@cfworker/json-schema@4.1.1)
|
||||
'@modelcontextprotocol/core-internal':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/core-internal
|
||||
|
|
@ -3140,8 +3140,8 @@ packages:
|
|||
'@manypkg/get-packages@1.1.3':
|
||||
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
|
||||
|
||||
'@modelcontextprotocol/conformance@0.2.0-alpha.7':
|
||||
resolution: {integrity: sha512-S3usVyTWdEqvJpyGnXAz6Uj4yboBwT44lrmMqaQtxKcw4PK8H5XfdH5NQqNmDl9/zQbk4oDxtXqzUyGqTbEDzg==}
|
||||
'@modelcontextprotocol/conformance@0.2.0-alpha.9':
|
||||
resolution: {integrity: sha512-Bi5P5TQlOQGPJxCT7UAHbpG7wsR7sNZskHGtCoZBo6vDu416D2FXPgM4wKbg91teIgj4HjGkhnzlvP7U2dszfQ==}
|
||||
hasBin: true
|
||||
|
||||
'@modelcontextprotocol/sdk@1.29.0':
|
||||
|
|
@ -7763,7 +7763,7 @@ snapshots:
|
|||
globby: 11.1.0
|
||||
read-yaml-file: 1.1.0
|
||||
|
||||
'@modelcontextprotocol/conformance@0.2.0-alpha.7(@cfworker/json-schema@4.1.1)':
|
||||
'@modelcontextprotocol/conformance@0.2.0-alpha.9(@cfworker/json-schema@4.1.1)':
|
||||
dependencies:
|
||||
'@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
'@octokit/rest': 22.0.1
|
||||
|
|
|
|||
|
|
@ -30,5 +30,8 @@ client: []
|
|||
|
||||
server: []
|
||||
# --- Carried-forward scenarios (also run by the 2025 legs) ---
|
||||
# (empty: json-schema-2020-12 burned by SEP-2106 fixture; sep-2164-resource-not-found
|
||||
# burned by the spec#2907 error-code renumber + alpha.5 referee.)
|
||||
# (empty: 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.)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# CI exits 0 if only these fail, exits 1 on unexpected failures or stale entries.
|
||||
#
|
||||
# Baseline established against the published @modelcontextprotocol/conformance
|
||||
# release pinned in package.json (0.2.0-alpha.7). Newer conformance releases
|
||||
# release pinned in package.json (0.2.0-alpha.9). Newer conformance releases
|
||||
# are adopted by deliberately bumping the package.json pin and reconciling
|
||||
# this file in the same change.
|
||||
#
|
||||
|
|
@ -24,7 +24,7 @@ client: []
|
|||
|
||||
server:
|
||||
# --- SEP-2663 (io.modelcontextprotocol/tasks) — server SDK does not implement the tasks extension ---
|
||||
# Extension-tagged scenarios; selected only by `--suite all` (the alpha.7 referee
|
||||
# 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
|
||||
# them, so they cannot flag these entries as stale. `tasks-status-notifications` is
|
||||
# intentionally absent: the referee SKIPs it unconditionally (harness rewrite pending
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"test:conformance:all": "pnpm run test:conformance:client:all && pnpm run test:conformance:server:all"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.7",
|
||||
"@modelcontextprotocol/conformance": "0.2.0-alpha.9",
|
||||
"@modelcontextprotocol/client": "workspace:^",
|
||||
"@modelcontextprotocol/server": "workspace:^",
|
||||
"@modelcontextprotocol/core-internal": "workspace:^",
|
||||
|
|
|
|||
|
|
@ -186,6 +186,63 @@ function createMcpServer() {
|
|||
})
|
||||
);
|
||||
|
||||
// SEP-2575 `server-stateless` diagnostic fixtures: the scenario hardcodes
|
||||
// these three tool names, and at conformance alpha.8 (conformance#372) the
|
||||
// checks behind them fail as untestable when the names are missing.
|
||||
|
||||
// Requires the `sampling` client capability via an MRTR createMessage
|
||||
// input request; the scenario calls it with empty clientCapabilities and
|
||||
// expects -32021 over HTTP 400.
|
||||
mcpServer.registerTool(
|
||||
'test_missing_capability',
|
||||
{
|
||||
description: 'SEP-2575: requires the `sampling` client capability (drives the -32021 undeclared-capability rejection)',
|
||||
inputSchema: z.object({})
|
||||
},
|
||||
async (_args, ctx): Promise<CallToolResult | InputRequiredResult> => {
|
||||
if (ctx.mcpReq.inputResponses?.['llm_answer'] === undefined) {
|
||||
return inputRequired({
|
||||
inputRequests: {
|
||||
llm_answer: inputRequired.createMessage({
|
||||
messages: [{ role: 'user', content: { type: 'text', text: 'Reply with the single word: pong' } }],
|
||||
maxTokens: 16
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
return { content: [{ type: 'text', text: 'sampling round-trip complete' }] };
|
||||
}
|
||||
);
|
||||
|
||||
// A plain successful call: the check only asserts that the response stream
|
||||
// carries no independent top-level JSON-RPC request. It must not elicit
|
||||
// (the scenario declares no `elicitation` capability); the referee's own
|
||||
// reference server does not elicit here either.
|
||||
mcpServer.registerTool(
|
||||
'test_streaming_elicitation',
|
||||
{
|
||||
description: 'SEP-2575: yields a response stream carrying no independent top-level JSON-RPC requests',
|
||||
inputSchema: z.object({})
|
||||
},
|
||||
async (): Promise<CallToolResult> => ({
|
||||
content: [{ type: 'text', text: 'stream observed: result frames only, no top-level requests' }]
|
||||
})
|
||||
);
|
||||
|
||||
// `ctx.mcpReq.log` is gated on the request's `_meta.logLevel`; the scenario
|
||||
// omits it and asserts no notifications/message frame appears.
|
||||
mcpServer.registerTool(
|
||||
'test_logging_tool',
|
||||
{
|
||||
description: 'SEP-2575: logs via ctx.mcpReq.log so the no-log-without-logLevel rule is exercised',
|
||||
inputSchema: z.object({})
|
||||
},
|
||||
async (_args, ctx): Promise<CallToolResult> => {
|
||||
await ctx.mcpReq.log('info', 'test_logging_tool ran (delivered only when the request set _meta.logLevel)');
|
||||
return { content: [{ type: 'text', text: 'logged through the request-scoped, logLevel-gated channel' }] };
|
||||
}
|
||||
);
|
||||
|
||||
// Simple text tool
|
||||
mcpServer.registerTool(
|
||||
'test_simple_text',
|
||||
|
|
|
|||
Loading…
Reference in a new issue