Skip to content

Commit cffb3f0

Browse files
committed
New tests: Backwards compat for elicitation client capabilities obj
1 parent 99447fe commit cffb3f0

File tree

3 files changed

+264
-1
lines changed

3 files changed

+264
-1
lines changed

src/client/index.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,81 @@ test('should allow setRequestHandler for declared elicitation capability', () =>
596596
}).toThrow('Client does not support sampling capability');
597597
});
598598

599+
test('should accept form-mode elicitation request when client advertises empty elicitation object (back-compat)', async () => {
600+
const server = new Server(
601+
{
602+
name: 'test server',
603+
version: '1.0'
604+
},
605+
{
606+
capabilities: {
607+
prompts: {},
608+
resources: {},
609+
tools: {},
610+
logging: {}
611+
}
612+
}
613+
);
614+
615+
const client = new Client(
616+
{
617+
name: 'test client',
618+
version: '1.0'
619+
},
620+
{
621+
capabilities: {
622+
elicitation: {}
623+
}
624+
}
625+
);
626+
627+
// Set up client handler for form-mode elicitation
628+
client.setRequestHandler(ElicitRequestSchema, request => {
629+
expect(request.params.mode).toBe('form');
630+
return {
631+
action: 'accept',
632+
content: {
633+
username: 'test-user',
634+
confirmed: true
635+
}
636+
};
637+
});
638+
639+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
640+
641+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
642+
643+
// Server should be able to send form-mode elicitation request
644+
// This works because getSupportedElicitationModes defaults to form mode
645+
// when neither form nor url are explicitly declared
646+
const result = await server.elicitFormInput({
647+
message: 'Please provide your username',
648+
requestedSchema: {
649+
type: 'object',
650+
properties: {
651+
username: {
652+
type: 'string',
653+
title: 'Username',
654+
description: 'Your username'
655+
},
656+
confirmed: {
657+
type: 'boolean',
658+
title: 'Confirm',
659+
description: 'Please confirm',
660+
default: false
661+
}
662+
},
663+
required: ['username']
664+
}
665+
});
666+
667+
expect(result.action).toBe('accept');
668+
expect(result.content).toEqual({
669+
username: 'test-user',
670+
confirmed: true
671+
});
672+
});
673+
599674
/***
600675
* Test: Type Checking
601676
* Test that custom request/notification/result schemas can be used with the Client class.

src/server/index.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,8 @@ test('should respect client elicitation capabilities', async () => {
304304

305305
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
306306

307-
expect(server.getClientCapabilities()).toEqual({ elicitation: {} });
307+
// After schema parsing, empty elicitation object should have form capability injected
308+
expect(server.getClientCapabilities()).toEqual({ elicitation: { form: {} } });
308309

309310
// This should work because elicitation is supported by the client
310311
await expect(
@@ -345,6 +346,91 @@ test('should respect client elicitation capabilities', async () => {
345346
).rejects.toThrow(/^Client does not support/);
346347
});
347348

349+
test('should apply back-compat form capability injection when client sends empty elicitation object', async () => {
350+
const server = new Server(
351+
{
352+
name: 'test server',
353+
version: '1.0'
354+
},
355+
{
356+
capabilities: {
357+
prompts: {},
358+
resources: {},
359+
tools: {},
360+
logging: {}
361+
}
362+
}
363+
);
364+
365+
const client = new Client(
366+
{
367+
name: 'test client',
368+
version: '1.0'
369+
},
370+
{
371+
capabilities: {
372+
elicitation: {}
373+
}
374+
}
375+
);
376+
377+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
378+
379+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
380+
381+
// Verify that the schema preprocessing injected form capability
382+
const clientCapabilities = server.getClientCapabilities();
383+
expect(clientCapabilities).toBeDefined();
384+
expect(clientCapabilities?.elicitation).toBeDefined();
385+
expect(clientCapabilities?.elicitation?.form).toBeDefined();
386+
expect(clientCapabilities?.elicitation?.form).toEqual({});
387+
expect(clientCapabilities?.elicitation?.url).toBeUndefined();
388+
});
389+
390+
test('should apply back-compat form capability injection when client sends elicitation with only applyDefaults', async () => {
391+
const server = new Server(
392+
{
393+
name: 'test server',
394+
version: '1.0'
395+
},
396+
{
397+
capabilities: {
398+
prompts: {},
399+
resources: {},
400+
tools: {},
401+
logging: {}
402+
}
403+
}
404+
);
405+
406+
const client = new Client(
407+
{
408+
name: 'test client',
409+
version: '1.0'
410+
},
411+
{
412+
capabilities: {
413+
elicitation: {
414+
applyDefaults: true
415+
}
416+
}
417+
}
418+
);
419+
420+
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
421+
422+
await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
423+
424+
// Verify that the schema preprocessing injected form capability while preserving applyDefaults
425+
const clientCapabilities = server.getClientCapabilities();
426+
expect(clientCapabilities).toBeDefined();
427+
expect(clientCapabilities?.elicitation).toBeDefined();
428+
expect(clientCapabilities?.elicitation?.form).toBeDefined();
429+
expect(clientCapabilities?.elicitation?.form).toEqual({});
430+
expect(clientCapabilities?.elicitation?.applyDefaults).toBe(true);
431+
expect(clientCapabilities?.elicitation?.url).toBeUndefined();
432+
});
433+
348434
test('should validate elicitation response against requested schema', async () => {
349435
const server = new Server(
350436
{

src/types.capabilities.test.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { ClientCapabilitiesSchema, InitializeRequestParamsSchema } from './types.js';
2+
3+
describe('ClientCapabilitiesSchema backwards compatibility', () => {
4+
describe('ElicitationCapabilitySchema preprocessing', () => {
5+
it('should inject form capability when elicitation is an empty object', () => {
6+
const capabilities = {
7+
elicitation: {}
8+
};
9+
10+
const result = ClientCapabilitiesSchema.parse(capabilities);
11+
expect(result.elicitation).toBeDefined();
12+
expect(result.elicitation?.form).toBeDefined();
13+
expect(result.elicitation?.form).toEqual({});
14+
expect(result.elicitation?.url).toBeUndefined();
15+
});
16+
17+
it('should inject form capability when elicitation has only applyDefaults', () => {
18+
const capabilities = {
19+
elicitation: {
20+
applyDefaults: true
21+
}
22+
};
23+
24+
const result = ClientCapabilitiesSchema.parse(capabilities);
25+
expect(result.elicitation).toBeDefined();
26+
expect(result.elicitation?.form).toBeDefined();
27+
expect(result.elicitation?.form).toEqual({});
28+
expect(result.elicitation?.applyDefaults).toBe(true);
29+
expect(result.elicitation?.url).toBeUndefined();
30+
});
31+
32+
it('should not inject form capability when form is explicitly declared', () => {
33+
const capabilities = {
34+
elicitation: {
35+
form: {}
36+
}
37+
};
38+
39+
const result = ClientCapabilitiesSchema.parse(capabilities);
40+
expect(result.elicitation).toBeDefined();
41+
expect(result.elicitation?.form).toBeDefined();
42+
expect(result.elicitation?.form).toEqual({});
43+
expect(result.elicitation?.url).toBeUndefined();
44+
});
45+
46+
it('should not inject form capability when url is explicitly declared', () => {
47+
const capabilities = {
48+
elicitation: {
49+
url: {}
50+
}
51+
};
52+
53+
const result = ClientCapabilitiesSchema.parse(capabilities);
54+
expect(result.elicitation).toBeDefined();
55+
expect(result.elicitation?.url).toBeDefined();
56+
expect(result.elicitation?.url).toEqual({});
57+
expect(result.elicitation?.form).toBeUndefined();
58+
});
59+
60+
it('should not inject form capability when both form and url are explicitly declared', () => {
61+
const capabilities = {
62+
elicitation: {
63+
form: {},
64+
url: {}
65+
}
66+
};
67+
68+
const result = ClientCapabilitiesSchema.parse(capabilities);
69+
expect(result.elicitation).toBeDefined();
70+
expect(result.elicitation?.form).toBeDefined();
71+
expect(result.elicitation?.url).toBeDefined();
72+
expect(result.elicitation?.form).toEqual({});
73+
expect(result.elicitation?.url).toEqual({});
74+
});
75+
76+
it('should not inject form capability when elicitation is undefined', () => {
77+
const capabilities = {};
78+
79+
const result = ClientCapabilitiesSchema.parse(capabilities);
80+
// When elicitation is not provided, it should remain undefined
81+
expect(result.elicitation).toBeUndefined();
82+
});
83+
84+
it('should work within InitializeRequestParamsSchema context', () => {
85+
const initializeParams = {
86+
protocolVersion: '2025-11-25',
87+
capabilities: {
88+
elicitation: {}
89+
},
90+
clientInfo: {
91+
name: 'test client',
92+
version: '1.0'
93+
}
94+
};
95+
96+
const result = InitializeRequestParamsSchema.parse(initializeParams);
97+
expect(result.capabilities.elicitation).toBeDefined();
98+
expect(result.capabilities.elicitation?.form).toBeDefined();
99+
expect(result.capabilities.elicitation?.form).toEqual({});
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)