fix(server-stateless): declare the elicitation capability on the streaming-probe request (#383)
This commit is contained in:
parent
468e53547d
commit
a989655390
2 changed files with 139 additions and 1 deletions
|
|
@ -628,6 +628,135 @@ describe('Stateless Server Scenario Negative Tests', () => {
|
||||||
expect(independentRequestCheck?.status).toBe('FAILURE');
|
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 () => {
|
test('Fails validation when logging occurs without an explicit client request logLevel metadata', async () => {
|
||||||
const mockUrl = mockFetchTarget((reqBody) => {
|
const mockUrl = mockFetchTarget((reqBody) => {
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -889,7 +889,16 @@ export class ServerStatelessScenario implements ClientScenario {
|
||||||
{
|
{
|
||||||
name: 'test_streaming_elicitation',
|
name: 'test_streaming_elicitation',
|
||||||
arguments: {},
|
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,
|
3,
|
||||||
600
|
600
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue