Skip to content

Commit e19918d

Browse files
Add conformance tests for SEP-1330 elicitation enums (#22)
* 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. * Add reference server implementation for SEP-1330 conformance test Adds test_elicitation_sep1330_enums tool to everything-server that requests elicitation with all 5 enum schema variants. Also updates SERVER_REQUIREMENTS.md with full specification. * Fix prettier formatting * only test active tests (#24) * refactor maps to pull names from the test * bring names back * separate active from pending * only test active --------- Co-authored-by: Paul Carleton <paulcarletonjr@gmail.com>
1 parent 73af777 commit e19918d

File tree

8 files changed

+647
-46
lines changed

8 files changed

+647
-46
lines changed

SERVER_REQUIREMENTS.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,97 @@ If no progress token provided, just execute with delays.
463463

464464
**Reference**: [SEP-1034](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1034)
465465

466+
#### `test_elicitation_sep1330_enums`
467+
468+
**Arguments**: None
469+
470+
**Behavior**: Request user input from the client using `elicitation/create` with enum schema improvements (SEP-1330)
471+
472+
**Elicitation Request**:
473+
474+
```json
475+
{
476+
"method": "elicitation/create",
477+
"params": {
478+
"message": "Please select options from the enum fields",
479+
"requestedSchema": {
480+
"type": "object",
481+
"properties": {
482+
"untitledSingle": {
483+
"type": "string",
484+
"description": "Select one option",
485+
"enum": ["option1", "option2", "option3"]
486+
},
487+
"titledSingle": {
488+
"type": "string",
489+
"description": "Select one option with titles",
490+
"oneOf": [
491+
{ "const": "value1", "title": "First Option" },
492+
{ "const": "value2", "title": "Second Option" },
493+
{ "const": "value3", "title": "Third Option" }
494+
]
495+
},
496+
"legacyEnum": {
497+
"type": "string",
498+
"description": "Select one option (legacy)",
499+
"enum": ["opt1", "opt2", "opt3"],
500+
"enumNames": ["Option One", "Option Two", "Option Three"]
501+
},
502+
"untitledMulti": {
503+
"type": "array",
504+
"description": "Select multiple options",
505+
"minItems": 1,
506+
"maxItems": 3,
507+
"items": {
508+
"type": "string",
509+
"enum": ["option1", "option2", "option3"]
510+
}
511+
},
512+
"titledMulti": {
513+
"type": "array",
514+
"description": "Select multiple options with titles",
515+
"minItems": 1,
516+
"maxItems": 3,
517+
"items": {
518+
"anyOf": [
519+
{ "const": "value1", "title": "First Choice" },
520+
{ "const": "value2", "title": "Second Choice" },
521+
{ "const": "value3", "title": "Third Choice" }
522+
]
523+
}
524+
}
525+
},
526+
"required": []
527+
}
528+
}
529+
}
530+
```
531+
532+
**Returns**: Text content with the elicitation result
533+
534+
```json
535+
{
536+
"content": [
537+
{
538+
"type": "text",
539+
"text": "Elicitation completed: action=<accept/decline/cancel>, content={...}"
540+
}
541+
]
542+
}
543+
```
544+
545+
**Implementation Note**: This tool tests SEP-1330 support for enum schema improvements including:
546+
547+
- Untitled single-select enums (type: string with enum array)
548+
- Titled single-select enums (using oneOf with const/title objects)
549+
- Legacy titled enums (using deprecated enumNames array)
550+
- Untitled multi-select enums (type: array with items.enum)
551+
- Titled multi-select enums (using items.anyOf with const/title objects)
552+
553+
If the client doesn't support elicitation (no `elicitation` capability), return an error.
554+
555+
**Reference**: [SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330)
556+
466557
---
467558

468559
## 3. Resources Requirements

examples/servers/typescript/everything-server.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,105 @@ function createMcpServer() {
472472
}
473473
);
474474

475+
// SEP-1330: Elicitation with enum schema improvements
476+
mcpServer.registerTool(
477+
'test_elicitation_sep1330_enums',
478+
{
479+
description:
480+
'Tests elicitation with enum schema improvements per SEP-1330',
481+
inputSchema: {}
482+
},
483+
async () => {
484+
try {
485+
// Request user input with all 5 enum schema variants
486+
const result = await mcpServer.server.request(
487+
{
488+
method: 'elicitation/create',
489+
params: {
490+
message: 'Please select options from the enum fields',
491+
requestedSchema: {
492+
type: 'object',
493+
properties: {
494+
// Untitled single-select enum (basic)
495+
untitledSingle: {
496+
type: 'string',
497+
description: 'Select one option',
498+
enum: ['option1', 'option2', 'option3']
499+
},
500+
// Titled single-select enum (using oneOf with const/title)
501+
titledSingle: {
502+
type: 'string',
503+
description: 'Select one option with titles',
504+
oneOf: [
505+
{ const: 'value1', title: 'First Option' },
506+
{ const: 'value2', title: 'Second Option' },
507+
{ const: 'value3', title: 'Third Option' }
508+
]
509+
},
510+
// Legacy titled enum (using enumNames - deprecated)
511+
legacyEnum: {
512+
type: 'string',
513+
description: 'Select one option (legacy)',
514+
enum: ['opt1', 'opt2', 'opt3'],
515+
enumNames: ['Option One', 'Option Two', 'Option Three']
516+
},
517+
// Untitled multi-select enum
518+
untitledMulti: {
519+
type: 'array',
520+
description: 'Select multiple options',
521+
minItems: 1,
522+
maxItems: 3,
523+
items: {
524+
type: 'string',
525+
enum: ['option1', 'option2', 'option3']
526+
}
527+
},
528+
// Titled multi-select enum (using anyOf with const/title)
529+
titledMulti: {
530+
type: 'array',
531+
description: 'Select multiple options with titles',
532+
minItems: 1,
533+
maxItems: 3,
534+
items: {
535+
anyOf: [
536+
{ const: 'value1', title: 'First Choice' },
537+
{ const: 'value2', title: 'Second Choice' },
538+
{ const: 'value3', title: 'Third Choice' }
539+
]
540+
}
541+
}
542+
},
543+
required: []
544+
}
545+
}
546+
},
547+
z
548+
.object({ method: z.literal('elicitation/create') })
549+
.passthrough() as any
550+
);
551+
552+
const elicitResult = result as any;
553+
return {
554+
content: [
555+
{
556+
type: 'text',
557+
text: `Elicitation completed: action=${elicitResult.action}, content=${JSON.stringify(elicitResult.content || {})}`
558+
}
559+
]
560+
};
561+
} catch (error: any) {
562+
return {
563+
content: [
564+
{
565+
type: 'text',
566+
text: `Elicitation not supported or error: ${error.message}`
567+
}
568+
]
569+
};
570+
}
571+
}
572+
);
573+
475574
// Dynamic tool (registered later via timer)
476575

477576
// ===== RESOURCES =====

src/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ import {
1010
printServerSummary,
1111
runInteractiveMode
1212
} from './runner';
13-
import { listScenarios, listClientScenarios } from './scenarios';
13+
import {
14+
listScenarios,
15+
listClientScenarios,
16+
listActiveClientScenarios
17+
} from './scenarios';
1418
import { ConformanceCheck } from './types';
1519
import { ClientOptionsSchema, ServerOptionsSchema } from './schemas';
1620
import packageJson from '../package.json';
@@ -97,8 +101,8 @@ program
97101
const { failed } = printServerResults(result.checks);
98102
process.exit(failed > 0 ? 1 : 0);
99103
} else {
100-
// Run all scenarios
101-
const scenarios = listClientScenarios();
104+
// Run all active scenarios
105+
const scenarios = listActiveClientScenarios();
102106
console.log(
103107
`Running ${scenarios.length} scenarios against ${validated.url}\n`
104108
);

src/scenarios/client/auth/basic-dcr.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ServerLifecycle } from './helpers/serverLifecycle.js';
66
import { Request, Response } from 'express';
77

88
export class AuthBasicDCRScenario implements Scenario {
9-
name = 'auth-basic-dcr';
9+
name = 'auth/basic-dcr';
1010
description =
1111
'Tests Basic OAuth flow with DCR, PRM at path-based location, OAuth metadata at root location, and no scopes required';
1212
private authServer = new ServerLifecycle(() => this.authBaseUrl);

src/scenarios/client/auth/basic-metadata-var1.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { ServerLifecycle } from './helpers/serverLifecycle.js';
66

77
export class AuthBasicMetadataVar1Scenario implements Scenario {
88
// TODO: name should match what we put in the scenario map
9-
name = 'auth-basic-metadata-var1';
9+
name = 'auth/basic-metadata-var1';
1010
description =
1111
'Tests Basic OAuth flow with DCR, PRM at root location, OAuth metadata at OpenID discovery path, and no scopes required';
1212
private authServer = new ServerLifecycle(() => this.authBaseUrl);

src/scenarios/index.ts

Lines changed: 71 additions & 39 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,
@@ -46,56 +47,83 @@ import {
4647
PromptsGetWithImageScenario
4748
} from './server/prompts.js';
4849

49-
export const scenarios = new Map<string, Scenario>([
50-
['initialize', new InitializeScenario()],
51-
['tools-call', new ToolsCallScenario()],
52-
['auth/basic-dcr', new AuthBasicDCRScenario()],
53-
['auth/basic-metadata-var1', new AuthBasicMetadataVar1Scenario()],
54-
[
55-
'elicitation-sep1034-client-defaults',
56-
new ElicitationClientDefaultsScenario()
57-
]
58-
]);
59-
60-
export const clientScenarios = new Map<string, ClientScenario>([
50+
// Pending client scenarios (not yet fully tested/implemented)
51+
const pendingClientScenariosList: ClientScenario[] = [
52+
// Elicitation scenarios (SEP-1330)
53+
new ElicitationEnumsScenario()
54+
];
55+
56+
// All client scenarios
57+
const allClientScenariosList: ClientScenario[] = [
6158
// Lifecycle scenarios
62-
['server-initialize', new ServerInitializeScenario()],
59+
new ServerInitializeScenario(),
6360

6461
// Utilities scenarios
65-
['logging-set-level', new LoggingSetLevelScenario()],
66-
['completion-complete', new CompletionCompleteScenario()],
62+
new LoggingSetLevelScenario(),
63+
new CompletionCompleteScenario(),
6764

6865
// Tools scenarios
69-
['tools-list', new ToolsListScenario()],
70-
['tools-call-simple-text', new ToolsCallSimpleTextScenario()],
71-
['tools-call-image', new ToolsCallImageScenario()],
72-
['tools-call-audio', new ToolsCallAudioScenario()],
73-
['tools-call-embedded-resource', new ToolsCallEmbeddedResourceScenario()],
74-
['tools-call-mixed-content', new ToolsCallMultipleContentTypesScenario()],
75-
['tools-call-with-logging', new ToolsCallWithLoggingScenario()],
76-
['tools-call-error', new ToolsCallErrorScenario()],
77-
['tools-call-with-progress', new ToolsCallWithProgressScenario()],
78-
['tools-call-sampling', new ToolsCallSamplingScenario()],
79-
['tools-call-elicitation', new ToolsCallElicitationScenario()],
66+
new ToolsListScenario(),
67+
new ToolsCallSimpleTextScenario(),
68+
new ToolsCallImageScenario(),
69+
new ToolsCallAudioScenario(),
70+
new ToolsCallEmbeddedResourceScenario(),
71+
new ToolsCallMultipleContentTypesScenario(),
72+
new ToolsCallWithLoggingScenario(),
73+
new ToolsCallErrorScenario(),
74+
new ToolsCallWithProgressScenario(),
75+
new ToolsCallSamplingScenario(),
76+
new ToolsCallElicitationScenario(),
8077

8178
// Elicitation scenarios (SEP-1034)
82-
['elicitation-sep1034-defaults', new ElicitationDefaultsScenario()],
79+
new ElicitationDefaultsScenario(),
80+
81+
// Elicitation scenarios (SEP-1330) - pending
82+
...pendingClientScenariosList,
8383

8484
// Resources scenarios
85-
['resources-list', new ResourcesListScenario()],
86-
['resources-read-text', new ResourcesReadTextScenario()],
87-
['resources-read-binary', new ResourcesReadBinaryScenario()],
88-
['resources-templates-read', new ResourcesTemplateReadScenario()],
89-
['resources-subscribe', new ResourcesSubscribeScenario()],
90-
['resources-unsubscribe', new ResourcesUnsubscribeScenario()],
85+
new ResourcesListScenario(),
86+
new ResourcesReadTextScenario(),
87+
new ResourcesReadBinaryScenario(),
88+
new ResourcesTemplateReadScenario(),
89+
new ResourcesSubscribeScenario(),
90+
new ResourcesUnsubscribeScenario(),
9191

9292
// Prompts scenarios
93-
['prompts-list', new PromptsListScenario()],
94-
['prompts-get-simple', new PromptsGetSimpleScenario()],
95-
['prompts-get-with-args', new PromptsGetWithArgsScenario()],
96-
['prompts-get-embedded-resource', new PromptsGetEmbeddedResourceScenario()],
97-
['prompts-get-with-image', new PromptsGetWithImageScenario()]
98-
]);
93+
new PromptsListScenario(),
94+
new PromptsGetSimpleScenario(),
95+
new PromptsGetWithArgsScenario(),
96+
new PromptsGetEmbeddedResourceScenario(),
97+
new PromptsGetWithImageScenario()
98+
];
99+
100+
// Active client scenarios (excludes pending)
101+
const activeClientScenariosList: ClientScenario[] =
102+
allClientScenariosList.filter(
103+
(scenario) =>
104+
!pendingClientScenariosList.some(
105+
(pending) => pending.name === scenario.name
106+
)
107+
);
108+
109+
// Client scenarios map - built from list
110+
export const clientScenarios = new Map<string, ClientScenario>(
111+
allClientScenariosList.map((scenario) => [scenario.name, scenario])
112+
);
113+
114+
// Scenario scenarios
115+
const scenariosList: Scenario[] = [
116+
new InitializeScenario(),
117+
new ToolsCallScenario(),
118+
new AuthBasicDCRScenario(),
119+
new AuthBasicMetadataVar1Scenario(),
120+
new ElicitationClientDefaultsScenario()
121+
];
122+
123+
// Scenarios map - built from list
124+
export const scenarios = new Map<string, Scenario>(
125+
scenariosList.map((scenario) => [scenario.name, scenario])
126+
);
99127

100128
export function registerScenario(name: string, scenario: Scenario): void {
101129
scenarios.set(name, scenario);
@@ -116,3 +144,7 @@ export function listScenarios(): string[] {
116144
export function listClientScenarios(): string[] {
117145
return Array.from(clientScenarios.keys());
118146
}
147+
148+
export function listActiveClientScenarios(): string[] {
149+
return activeClientScenariosList.map((scenario) => scenario.name);
150+
}

0 commit comments

Comments
 (0)