fix(server-stateless): declare the elicitation capability on the streaming-probe request (#383)

This commit is contained in:
Josh Abbott 2026-07-27 06:45:48 -04:00 • committed by GitHub
commit a989655390
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 139 additions and 1 deletions

View file

@ -628,6 +628,135 @@ describe('Stateless Server Scenario Negative Tests', () => {
expect(independentRequestCheck?.status).toBe('FAILURE');
});
// A spec-correct server that enforces per-request capability declaration:
// test_missing_capability requires sampling and test_streaming_elicitation
// requires elicitation; each is rejected with a spec-shaped -32021 unless
// the request's _meta declares the capability. A rejection reaches
// listenToStream as the parsed JSON error body — a single error frame.
function capabilityEnforcingMock(
streamingCallFrames: (reqBody: any) => any[]
) {
return mockFetchTarget((reqBody) => {
if (reqBody.method === 'server/discover') {
return discoverResponse(reqBody, { tools: {} }, 'capability-enforcer');
}
if (reqBody.method === 'tools/list') {
return {
status: 200,
body: {
jsonrpc: '2.0',
id: reqBody.id,
result: {
tools: [
{ name: 'test_missing_capability' },
{ name: 'test_streaming_elicitation' }
]
}
}
};
}
if (reqBody.method === 'tools/call') {
const declaredCaps =
reqBody.params?._meta?.[
'io.modelcontextprotocol/clientCapabilities'
] ?? {};
if (reqBody.params?.name === 'test_missing_capability') {
if (!declaredCaps.sampling) {
return {
status: 400,
body: {
jsonrpc: '2.0',
id: reqBody.id,
...spec32021({ sampling: {} })
}
};
}
return {
status: 200,
body: {
jsonrpc: '2.0',
id: reqBody.id,
result: { content: [{ type: 'text', text: 'ok' }] }
}
};
}
if (reqBody.params?.name === 'test_streaming_elicitation') {
if (!declaredCaps.elicitation) {
return {
isStream: true,
status: 400,
frames: [
{
jsonrpc: '2.0',
id: reqBody.id,
...spec32021({ elicitation: {} })
}
]
};
}
return {
isStream: true,
status: 200,
frames: streamingCallFrames(reqBody)
};
}
}
});
}
test('Passes both capability checks against a capability-enforcing server: the streaming probe declares the elicitation capability its fixture requires', async () => {
// sep-2575-server-rejects-undeclared-capability REQUIRES the -32021
// rejection of an undeclared capability; before the streaming probe
// declared elicitation, that same rejection made
// sep-2575-http-server-no-independent-requests-on-stream untestable —
// one scenario demanded and punished the identical server behavior.
const mockUrl = capabilityEnforcingMock((reqBody) => [
{
jsonrpc: '2.0',
method: 'notifications/progress',
params: { progressToken: 'token-abc', total: 100, value: 50 }
},
{
jsonrpc: '2.0',
id: reqBody.id,
result: { content: [{ type: 'text', text: 'Streaming complete' }] }
}
]);
const scenario = new ServerStatelessScenario();
const checks = await scenario.run(testContext(mockUrl));
expect(
findCheck(checks, 'sep-2575-server-rejects-undeclared-capability')?.status
).toBe('SUCCESS');
expect(
findCheck(
checks,
'sep-2575-http-server-no-independent-requests-on-stream'
)?.status
).toBe('SUCCESS');
});
test('Fails (untestable) the stream check when the server rejects the streaming probe despite the declared elicitation capability', async () => {
// Guards the notTestable branch: a server that rejects even a properly
// declared capability leaves nothing streamed to inspect, which must
// stay a red untestable outcome, not a vacuous SUCCESS.
const mockUrl = capabilityEnforcingMock((reqBody) => [
{ jsonrpc: '2.0', id: reqBody.id, ...spec32021({ elicitation: {} }) }
]);
const scenario = new ServerStatelessScenario();
const checks = await scenario.run(testContext(mockUrl));
const streamCheck = findCheck(
checks,
'sep-2575-http-server-no-independent-requests-on-stream'
);
expect(streamCheck?.status).toBe('FAILURE');
expect(streamCheck?.errorMessage).toContain('Not testable:');
expect(streamCheck?.errorMessage).toContain('-32021');
});
test('Fails validation when logging occurs without an explicit client request logLevel metadata', async () => {
const mockUrl = mockFetchTarget((reqBody) => {
if (

View file

@ -889,7 +889,16 @@ export class ServerStatelessScenario implements ClientScenario {
{
name: 'test_streaming_elicitation',
arguments: {},
_meta: validMeta
// The fixture legitimately needs the elicitation capability, so a
// capability-enforcing server rejects the shared empty-capability
// envelope with -32021 (the exact behavior
// sep-2575-server-rejects-undeclared-capability requires) and the
// stream could never be exercised. Declare it at the probe site
// only — validMeta must stay empty for that sibling check.
_meta: {
...validMeta,
'io.modelcontextprotocol/clientCapabilities': { elicitation: {} }
}
},
3,
600