Skip to content

Commit 3059635

Browse files
Add SEP-1330 conformance test for elicitation enum schemas
Adds server-side conformance test that validates servers properly request elicitation with SEP-1330 enum schema improvements: - Untitled single-select enum (type: string, enum: [...]) - Titled single-select enum (oneOf with const/title) - Legacy titled enum (enumNames for backward compatibility) - Untitled multi-select enum (type: array, items.enum) - Titled multi-select enum (items.anyOf with const/title) Test expects server to implement `test_elicitation_sep1330_enums` tool.
1 parent 595bf4d commit 3059635

2 files changed

Lines changed: 379 additions & 0 deletions

File tree

src/scenarios/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
} from './server/tools.js';
2929

3030
import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js';
31+
import { ElicitationEnumsScenario } from './server/elicitation-enums.js';
3132

3233
import {
3334
ResourcesListScenario,
@@ -81,6 +82,9 @@ export const clientScenarios = new Map<string, ClientScenario>([
8182
// Elicitation scenarios (SEP-1034)
8283
['elicitation-sep1034-defaults', new ElicitationDefaultsScenario()],
8384

85+
// Elicitation scenarios (SEP-1330)
86+
['elicitation-sep1330-enums', new ElicitationEnumsScenario()],
87+
8488
// Resources scenarios
8589
['resources-list', new ResourcesListScenario()],
8690
['resources-read-text', new ResourcesReadTextScenario()],
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
/**
2+
* SEP-1330: Elicitation enum schema improvements test scenarios for MCP servers
3+
*/
4+
5+
import { ClientScenario, ConformanceCheck } from '../../types.js';
6+
import { connectToServer } from './client-helper.js';
7+
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
8+
9+
export class ElicitationEnumsScenario implements ClientScenario {
10+
name = 'elicitation-sep1330-enums';
11+
description = 'Test elicitation with enum schema improvements (SEP-1330)';
12+
13+
async run(serverUrl: string): Promise<ConformanceCheck[]> {
14+
const checks: ConformanceCheck[] = [];
15+
16+
try {
17+
const connection = await connectToServer(serverUrl);
18+
19+
let capturedRequest: any = null;
20+
connection.client.setRequestHandler(
21+
ElicitRequestSchema,
22+
async (request) => {
23+
capturedRequest = request;
24+
// Return mock data for all enum types
25+
return {
26+
action: 'accept',
27+
content: {
28+
untitledSingle: 'option1',
29+
titledSingle: 'value1',
30+
legacyEnum: 'opt1',
31+
untitledMulti: ['option1', 'option2'],
32+
titledMulti: ['value1', 'value2']
33+
}
34+
};
35+
}
36+
);
37+
38+
await connection.client.callTool({
39+
name: 'test_elicitation_sep1330_enums',
40+
arguments: {}
41+
});
42+
43+
// Validate that elicitation was requested
44+
if (!capturedRequest) {
45+
checks.push({
46+
id: 'elicitation-sep1330-general',
47+
name: 'ElicitationSEP1330General',
48+
description: 'Server requests elicitation with enum schemas',
49+
status: 'FAILURE',
50+
timestamp: new Date().toISOString(),
51+
errorMessage: 'Server did not request elicitation from client',
52+
specReferences: [
53+
{
54+
id: 'SEP-1330',
55+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
56+
}
57+
]
58+
});
59+
await connection.close();
60+
return checks;
61+
}
62+
63+
const schema = capturedRequest.params?.requestedSchema;
64+
const properties = schema?.properties;
65+
66+
// Validate untitled single-select enum
67+
const untitledSingleErrors: string[] = [];
68+
if (!properties?.untitledSingle) {
69+
untitledSingleErrors.push(
70+
'Missing untitled single-select enum field "untitledSingle"'
71+
);
72+
} else {
73+
if (properties.untitledSingle.type !== 'string') {
74+
untitledSingleErrors.push(
75+
`Expected type "string", got "${properties.untitledSingle.type}"`
76+
);
77+
}
78+
if (
79+
!properties.untitledSingle.enum ||
80+
!Array.isArray(properties.untitledSingle.enum)
81+
) {
82+
untitledSingleErrors.push('Missing or invalid enum array');
83+
}
84+
if (properties.untitledSingle.oneOf) {
85+
untitledSingleErrors.push(
86+
'Untitled enum should not have oneOf property'
87+
);
88+
}
89+
if (properties.untitledSingle.enumNames) {
90+
untitledSingleErrors.push(
91+
'Untitled enum should not have enumNames property'
92+
);
93+
}
94+
}
95+
96+
checks.push({
97+
id: 'elicitation-sep1330-untitled-single',
98+
name: 'ElicitationSEP1330UntitledSingle',
99+
description: 'Untitled single-select enum schema uses enum array',
100+
status: untitledSingleErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
101+
timestamp: new Date().toISOString(),
102+
errorMessage:
103+
untitledSingleErrors.length > 0
104+
? untitledSingleErrors.join('; ')
105+
: undefined,
106+
specReferences: [
107+
{
108+
id: 'SEP-1330',
109+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
110+
}
111+
],
112+
details: {
113+
field: 'untitledSingle',
114+
schema: properties?.untitledSingle
115+
}
116+
});
117+
118+
// Validate titled single-select enum (using oneOf with const/title)
119+
const titledSingleErrors: string[] = [];
120+
if (!properties?.titledSingle) {
121+
titledSingleErrors.push(
122+
'Missing titled single-select enum field "titledSingle"'
123+
);
124+
} else {
125+
if (properties.titledSingle.type !== 'string') {
126+
titledSingleErrors.push(
127+
`Expected type "string", got "${properties.titledSingle.type}"`
128+
);
129+
}
130+
if (
131+
!properties.titledSingle.oneOf ||
132+
!Array.isArray(properties.titledSingle.oneOf)
133+
) {
134+
titledSingleErrors.push(
135+
'Missing or invalid oneOf array for titled enum'
136+
);
137+
} else {
138+
// Validate oneOf structure has const/title pairs
139+
const invalidItems = properties.titledSingle.oneOf.filter(
140+
(item: any) =>
141+
typeof item.const !== 'string' || typeof item.title !== 'string'
142+
);
143+
if (invalidItems.length > 0) {
144+
titledSingleErrors.push(
145+
`oneOf items must have "const" (string) and "title" (string) properties`
146+
);
147+
}
148+
}
149+
if (properties.titledSingle.enum) {
150+
titledSingleErrors.push(
151+
'Titled enum should use oneOf instead of enum array'
152+
);
153+
}
154+
}
155+
156+
checks.push({
157+
id: 'elicitation-sep1330-titled-single',
158+
name: 'ElicitationSEP1330TitledSingle',
159+
description:
160+
'Titled single-select enum schema uses oneOf with const/title',
161+
status: titledSingleErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
162+
timestamp: new Date().toISOString(),
163+
errorMessage:
164+
titledSingleErrors.length > 0
165+
? titledSingleErrors.join('; ')
166+
: undefined,
167+
specReferences: [
168+
{
169+
id: 'SEP-1330',
170+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
171+
}
172+
],
173+
details: {
174+
field: 'titledSingle',
175+
schema: properties?.titledSingle
176+
}
177+
});
178+
179+
// Validate legacy titled enum (using enumNames - deprecated)
180+
const legacyEnumErrors: string[] = [];
181+
if (!properties?.legacyEnum) {
182+
legacyEnumErrors.push('Missing legacy titled enum field "legacyEnum"');
183+
} else {
184+
if (properties.legacyEnum.type !== 'string') {
185+
legacyEnumErrors.push(
186+
`Expected type "string", got "${properties.legacyEnum.type}"`
187+
);
188+
}
189+
if (
190+
!properties.legacyEnum.enum ||
191+
!Array.isArray(properties.legacyEnum.enum)
192+
) {
193+
legacyEnumErrors.push('Missing or invalid enum array');
194+
}
195+
if (
196+
!properties.legacyEnum.enumNames ||
197+
!Array.isArray(properties.legacyEnum.enumNames)
198+
) {
199+
legacyEnumErrors.push(
200+
'Missing or invalid enumNames array for legacy titled enum'
201+
);
202+
} else if (
203+
properties.legacyEnum.enum &&
204+
properties.legacyEnum.enumNames.length !==
205+
properties.legacyEnum.enum.length
206+
) {
207+
legacyEnumErrors.push(
208+
`enumNames length (${properties.legacyEnum.enumNames.length}) must match enum length (${properties.legacyEnum.enum.length})`
209+
);
210+
}
211+
}
212+
213+
checks.push({
214+
id: 'elicitation-sep1330-legacy-enumnames',
215+
name: 'ElicitationSEP1330LegacyEnumNames',
216+
description: 'Legacy titled enum schema uses enumNames (deprecated)',
217+
status: legacyEnumErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
218+
timestamp: new Date().toISOString(),
219+
errorMessage:
220+
legacyEnumErrors.length > 0 ? legacyEnumErrors.join('; ') : undefined,
221+
specReferences: [
222+
{
223+
id: 'SEP-1330',
224+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
225+
}
226+
],
227+
details: {
228+
field: 'legacyEnum',
229+
schema: properties?.legacyEnum
230+
}
231+
});
232+
233+
// Validate untitled multi-select enum
234+
const untitledMultiErrors: string[] = [];
235+
if (!properties?.untitledMulti) {
236+
untitledMultiErrors.push(
237+
'Missing untitled multi-select enum field "untitledMulti"'
238+
);
239+
} else {
240+
if (properties.untitledMulti.type !== 'array') {
241+
untitledMultiErrors.push(
242+
`Expected type "array", got "${properties.untitledMulti.type}"`
243+
);
244+
}
245+
if (!properties.untitledMulti.items) {
246+
untitledMultiErrors.push('Missing items property for array type');
247+
} else {
248+
if (properties.untitledMulti.items.type !== 'string') {
249+
untitledMultiErrors.push(
250+
`Expected items.type "string", got "${properties.untitledMulti.items.type}"`
251+
);
252+
}
253+
if (
254+
!properties.untitledMulti.items.enum ||
255+
!Array.isArray(properties.untitledMulti.items.enum)
256+
) {
257+
untitledMultiErrors.push('Missing or invalid items.enum array');
258+
}
259+
if (properties.untitledMulti.items.anyOf) {
260+
untitledMultiErrors.push(
261+
'Untitled multi-select should use items.enum, not items.anyOf'
262+
);
263+
}
264+
}
265+
}
266+
267+
checks.push({
268+
id: 'elicitation-sep1330-untitled-multi',
269+
name: 'ElicitationSEP1330UntitledMulti',
270+
description:
271+
'Untitled multi-select enum schema uses array with items.enum',
272+
status: untitledMultiErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
273+
timestamp: new Date().toISOString(),
274+
errorMessage:
275+
untitledMultiErrors.length > 0
276+
? untitledMultiErrors.join('; ')
277+
: undefined,
278+
specReferences: [
279+
{
280+
id: 'SEP-1330',
281+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
282+
}
283+
],
284+
details: {
285+
field: 'untitledMulti',
286+
schema: properties?.untitledMulti
287+
}
288+
});
289+
290+
// Validate titled multi-select enum (using items.anyOf with const/title)
291+
const titledMultiErrors: string[] = [];
292+
if (!properties?.titledMulti) {
293+
titledMultiErrors.push(
294+
'Missing titled multi-select enum field "titledMulti"'
295+
);
296+
} else {
297+
if (properties.titledMulti.type !== 'array') {
298+
titledMultiErrors.push(
299+
`Expected type "array", got "${properties.titledMulti.type}"`
300+
);
301+
}
302+
if (!properties.titledMulti.items) {
303+
titledMultiErrors.push('Missing items property for array type');
304+
} else {
305+
if (
306+
!properties.titledMulti.items.anyOf ||
307+
!Array.isArray(properties.titledMulti.items.anyOf)
308+
) {
309+
titledMultiErrors.push(
310+
'Missing or invalid items.anyOf array for titled multi-select'
311+
);
312+
} else {
313+
// Validate anyOf structure has const/title pairs
314+
const invalidItems = properties.titledMulti.items.anyOf.filter(
315+
(item: any) =>
316+
typeof item.const !== 'string' || typeof item.title !== 'string'
317+
);
318+
if (invalidItems.length > 0) {
319+
titledMultiErrors.push(
320+
`items.anyOf entries must have "const" (string) and "title" (string) properties`
321+
);
322+
}
323+
}
324+
if (properties.titledMulti.items.enum) {
325+
titledMultiErrors.push(
326+
'Titled multi-select should use items.anyOf, not items.enum'
327+
);
328+
}
329+
}
330+
}
331+
332+
checks.push({
333+
id: 'elicitation-sep1330-titled-multi',
334+
name: 'ElicitationSEP1330TitledMulti',
335+
description:
336+
'Titled multi-select enum schema uses array with items.anyOf',
337+
status: titledMultiErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
338+
timestamp: new Date().toISOString(),
339+
errorMessage:
340+
titledMultiErrors.length > 0
341+
? titledMultiErrors.join('; ')
342+
: undefined,
343+
specReferences: [
344+
{
345+
id: 'SEP-1330',
346+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
347+
}
348+
],
349+
details: {
350+
field: 'titledMulti',
351+
schema: properties?.titledMulti
352+
}
353+
});
354+
355+
await connection.close();
356+
} catch (error) {
357+
checks.push({
358+
id: 'elicitation-sep1330-general',
359+
name: 'ElicitationSEP1330General',
360+
description: 'Server requests elicitation with enum schemas',
361+
status: 'FAILURE',
362+
timestamp: new Date().toISOString(),
363+
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
364+
specReferences: [
365+
{
366+
id: 'SEP-1330',
367+
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
368+
}
369+
]
370+
});
371+
}
372+
373+
return checks;
374+
}
375+
}

0 commit comments

Comments
 (0)