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>
This commit is contained in:
parent
73af777bc2
commit
e19918d968
8 changed files with 647 additions and 46 deletions
|
|
@ -463,6 +463,97 @@ If no progress token provided, just execute with delays.
|
|||
|
||||
**Reference**: [SEP-1034](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1034)
|
||||
|
||||
#### `test_elicitation_sep1330_enums`
|
||||
|
||||
**Arguments**: None
|
||||
|
||||
**Behavior**: Request user input from the client using `elicitation/create` with enum schema improvements (SEP-1330)
|
||||
|
||||
**Elicitation Request**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "elicitation/create",
|
||||
"params": {
|
||||
"message": "Please select options from the enum fields",
|
||||
"requestedSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"untitledSingle": {
|
||||
"type": "string",
|
||||
"description": "Select one option",
|
||||
"enum": ["option1", "option2", "option3"]
|
||||
},
|
||||
"titledSingle": {
|
||||
"type": "string",
|
||||
"description": "Select one option with titles",
|
||||
"oneOf": [
|
||||
{ "const": "value1", "title": "First Option" },
|
||||
{ "const": "value2", "title": "Second Option" },
|
||||
{ "const": "value3", "title": "Third Option" }
|
||||
]
|
||||
},
|
||||
"legacyEnum": {
|
||||
"type": "string",
|
||||
"description": "Select one option (legacy)",
|
||||
"enum": ["opt1", "opt2", "opt3"],
|
||||
"enumNames": ["Option One", "Option Two", "Option Three"]
|
||||
},
|
||||
"untitledMulti": {
|
||||
"type": "array",
|
||||
"description": "Select multiple options",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["option1", "option2", "option3"]
|
||||
}
|
||||
},
|
||||
"titledMulti": {
|
||||
"type": "array",
|
||||
"description": "Select multiple options with titles",
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "const": "value1", "title": "First Choice" },
|
||||
{ "const": "value2", "title": "Second Choice" },
|
||||
{ "const": "value3", "title": "Third Choice" }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Returns**: Text content with the elicitation result
|
||||
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Elicitation completed: action=<accept/decline/cancel>, content={...}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Implementation Note**: This tool tests SEP-1330 support for enum schema improvements including:
|
||||
|
||||
- Untitled single-select enums (type: string with enum array)
|
||||
- Titled single-select enums (using oneOf with const/title objects)
|
||||
- Legacy titled enums (using deprecated enumNames array)
|
||||
- Untitled multi-select enums (type: array with items.enum)
|
||||
- Titled multi-select enums (using items.anyOf with const/title objects)
|
||||
|
||||
If the client doesn't support elicitation (no `elicitation` capability), return an error.
|
||||
|
||||
**Reference**: [SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330)
|
||||
|
||||
---
|
||||
|
||||
## 3. Resources Requirements
|
||||
|
|
|
|||
|
|
@ -472,6 +472,105 @@ function createMcpServer() {
|
|||
}
|
||||
);
|
||||
|
||||
// SEP-1330: Elicitation with enum schema improvements
|
||||
mcpServer.registerTool(
|
||||
'test_elicitation_sep1330_enums',
|
||||
{
|
||||
description:
|
||||
'Tests elicitation with enum schema improvements per SEP-1330',
|
||||
inputSchema: {}
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
// Request user input with all 5 enum schema variants
|
||||
const result = await mcpServer.server.request(
|
||||
{
|
||||
method: 'elicitation/create',
|
||||
params: {
|
||||
message: 'Please select options from the enum fields',
|
||||
requestedSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
// Untitled single-select enum (basic)
|
||||
untitledSingle: {
|
||||
type: 'string',
|
||||
description: 'Select one option',
|
||||
enum: ['option1', 'option2', 'option3']
|
||||
},
|
||||
// Titled single-select enum (using oneOf with const/title)
|
||||
titledSingle: {
|
||||
type: 'string',
|
||||
description: 'Select one option with titles',
|
||||
oneOf: [
|
||||
{ const: 'value1', title: 'First Option' },
|
||||
{ const: 'value2', title: 'Second Option' },
|
||||
{ const: 'value3', title: 'Third Option' }
|
||||
]
|
||||
},
|
||||
// Legacy titled enum (using enumNames - deprecated)
|
||||
legacyEnum: {
|
||||
type: 'string',
|
||||
description: 'Select one option (legacy)',
|
||||
enum: ['opt1', 'opt2', 'opt3'],
|
||||
enumNames: ['Option One', 'Option Two', 'Option Three']
|
||||
},
|
||||
// Untitled multi-select enum
|
||||
untitledMulti: {
|
||||
type: 'array',
|
||||
description: 'Select multiple options',
|
||||
minItems: 1,
|
||||
maxItems: 3,
|
||||
items: {
|
||||
type: 'string',
|
||||
enum: ['option1', 'option2', 'option3']
|
||||
}
|
||||
},
|
||||
// Titled multi-select enum (using anyOf with const/title)
|
||||
titledMulti: {
|
||||
type: 'array',
|
||||
description: 'Select multiple options with titles',
|
||||
minItems: 1,
|
||||
maxItems: 3,
|
||||
items: {
|
||||
anyOf: [
|
||||
{ const: 'value1', title: 'First Choice' },
|
||||
{ const: 'value2', title: 'Second Choice' },
|
||||
{ const: 'value3', title: 'Third Choice' }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
required: []
|
||||
}
|
||||
}
|
||||
},
|
||||
z
|
||||
.object({ method: z.literal('elicitation/create') })
|
||||
.passthrough() as any
|
||||
);
|
||||
|
||||
const elicitResult = result as any;
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Elicitation completed: action=${elicitResult.action}, content=${JSON.stringify(elicitResult.content || {})}`
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Elicitation not supported or error: ${error.message}`
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Dynamic tool (registered later via timer)
|
||||
|
||||
// ===== RESOURCES =====
|
||||
|
|
|
|||
10
src/index.ts
10
src/index.ts
|
|
@ -10,7 +10,11 @@ import {
|
|||
printServerSummary,
|
||||
runInteractiveMode
|
||||
} from './runner';
|
||||
import { listScenarios, listClientScenarios } from './scenarios';
|
||||
import {
|
||||
listScenarios,
|
||||
listClientScenarios,
|
||||
listActiveClientScenarios
|
||||
} from './scenarios';
|
||||
import { ConformanceCheck } from './types';
|
||||
import { ClientOptionsSchema, ServerOptionsSchema } from './schemas';
|
||||
import packageJson from '../package.json';
|
||||
|
|
@ -97,8 +101,8 @@ program
|
|||
const { failed } = printServerResults(result.checks);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
} else {
|
||||
// Run all scenarios
|
||||
const scenarios = listClientScenarios();
|
||||
// Run all active scenarios
|
||||
const scenarios = listActiveClientScenarios();
|
||||
console.log(
|
||||
`Running ${scenarios.length} scenarios against ${validated.url}\n`
|
||||
);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ServerLifecycle } from './helpers/serverLifecycle.js';
|
|||
import { Request, Response } from 'express';
|
||||
|
||||
export class AuthBasicDCRScenario implements Scenario {
|
||||
name = 'auth-basic-dcr';
|
||||
name = 'auth/basic-dcr';
|
||||
description =
|
||||
'Tests Basic OAuth flow with DCR, PRM at path-based location, OAuth metadata at root location, and no scopes required';
|
||||
private authServer = new ServerLifecycle(() => this.authBaseUrl);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ServerLifecycle } from './helpers/serverLifecycle.js';
|
|||
|
||||
export class AuthBasicMetadataVar1Scenario implements Scenario {
|
||||
// TODO: name should match what we put in the scenario map
|
||||
name = 'auth-basic-metadata-var1';
|
||||
name = 'auth/basic-metadata-var1';
|
||||
description =
|
||||
'Tests Basic OAuth flow with DCR, PRM at root location, OAuth metadata at OpenID discovery path, and no scopes required';
|
||||
private authServer = new ServerLifecycle(() => this.authBaseUrl);
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import {
|
|||
} from './server/tools.js';
|
||||
|
||||
import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js';
|
||||
import { ElicitationEnumsScenario } from './server/elicitation-enums.js';
|
||||
|
||||
import {
|
||||
ResourcesListScenario,
|
||||
|
|
@ -46,56 +47,83 @@ import {
|
|||
PromptsGetWithImageScenario
|
||||
} from './server/prompts.js';
|
||||
|
||||
export const scenarios = new Map<string, Scenario>([
|
||||
['initialize', new InitializeScenario()],
|
||||
['tools-call', new ToolsCallScenario()],
|
||||
['auth/basic-dcr', new AuthBasicDCRScenario()],
|
||||
['auth/basic-metadata-var1', new AuthBasicMetadataVar1Scenario()],
|
||||
[
|
||||
'elicitation-sep1034-client-defaults',
|
||||
new ElicitationClientDefaultsScenario()
|
||||
]
|
||||
]);
|
||||
// Pending client scenarios (not yet fully tested/implemented)
|
||||
const pendingClientScenariosList: ClientScenario[] = [
|
||||
// Elicitation scenarios (SEP-1330)
|
||||
new ElicitationEnumsScenario()
|
||||
];
|
||||
|
||||
export const clientScenarios = new Map<string, ClientScenario>([
|
||||
// All client scenarios
|
||||
const allClientScenariosList: ClientScenario[] = [
|
||||
// Lifecycle scenarios
|
||||
['server-initialize', new ServerInitializeScenario()],
|
||||
new ServerInitializeScenario(),
|
||||
|
||||
// Utilities scenarios
|
||||
['logging-set-level', new LoggingSetLevelScenario()],
|
||||
['completion-complete', new CompletionCompleteScenario()],
|
||||
new LoggingSetLevelScenario(),
|
||||
new CompletionCompleteScenario(),
|
||||
|
||||
// Tools scenarios
|
||||
['tools-list', new ToolsListScenario()],
|
||||
['tools-call-simple-text', new ToolsCallSimpleTextScenario()],
|
||||
['tools-call-image', new ToolsCallImageScenario()],
|
||||
['tools-call-audio', new ToolsCallAudioScenario()],
|
||||
['tools-call-embedded-resource', new ToolsCallEmbeddedResourceScenario()],
|
||||
['tools-call-mixed-content', new ToolsCallMultipleContentTypesScenario()],
|
||||
['tools-call-with-logging', new ToolsCallWithLoggingScenario()],
|
||||
['tools-call-error', new ToolsCallErrorScenario()],
|
||||
['tools-call-with-progress', new ToolsCallWithProgressScenario()],
|
||||
['tools-call-sampling', new ToolsCallSamplingScenario()],
|
||||
['tools-call-elicitation', new ToolsCallElicitationScenario()],
|
||||
new ToolsListScenario(),
|
||||
new ToolsCallSimpleTextScenario(),
|
||||
new ToolsCallImageScenario(),
|
||||
new ToolsCallAudioScenario(),
|
||||
new ToolsCallEmbeddedResourceScenario(),
|
||||
new ToolsCallMultipleContentTypesScenario(),
|
||||
new ToolsCallWithLoggingScenario(),
|
||||
new ToolsCallErrorScenario(),
|
||||
new ToolsCallWithProgressScenario(),
|
||||
new ToolsCallSamplingScenario(),
|
||||
new ToolsCallElicitationScenario(),
|
||||
|
||||
// Elicitation scenarios (SEP-1034)
|
||||
['elicitation-sep1034-defaults', new ElicitationDefaultsScenario()],
|
||||
new ElicitationDefaultsScenario(),
|
||||
|
||||
// Elicitation scenarios (SEP-1330) - pending
|
||||
...pendingClientScenariosList,
|
||||
|
||||
// Resources scenarios
|
||||
['resources-list', new ResourcesListScenario()],
|
||||
['resources-read-text', new ResourcesReadTextScenario()],
|
||||
['resources-read-binary', new ResourcesReadBinaryScenario()],
|
||||
['resources-templates-read', new ResourcesTemplateReadScenario()],
|
||||
['resources-subscribe', new ResourcesSubscribeScenario()],
|
||||
['resources-unsubscribe', new ResourcesUnsubscribeScenario()],
|
||||
new ResourcesListScenario(),
|
||||
new ResourcesReadTextScenario(),
|
||||
new ResourcesReadBinaryScenario(),
|
||||
new ResourcesTemplateReadScenario(),
|
||||
new ResourcesSubscribeScenario(),
|
||||
new ResourcesUnsubscribeScenario(),
|
||||
|
||||
// Prompts scenarios
|
||||
['prompts-list', new PromptsListScenario()],
|
||||
['prompts-get-simple', new PromptsGetSimpleScenario()],
|
||||
['prompts-get-with-args', new PromptsGetWithArgsScenario()],
|
||||
['prompts-get-embedded-resource', new PromptsGetEmbeddedResourceScenario()],
|
||||
['prompts-get-with-image', new PromptsGetWithImageScenario()]
|
||||
]);
|
||||
new PromptsListScenario(),
|
||||
new PromptsGetSimpleScenario(),
|
||||
new PromptsGetWithArgsScenario(),
|
||||
new PromptsGetEmbeddedResourceScenario(),
|
||||
new PromptsGetWithImageScenario()
|
||||
];
|
||||
|
||||
// Active client scenarios (excludes pending)
|
||||
const activeClientScenariosList: ClientScenario[] =
|
||||
allClientScenariosList.filter(
|
||||
(scenario) =>
|
||||
!pendingClientScenariosList.some(
|
||||
(pending) => pending.name === scenario.name
|
||||
)
|
||||
);
|
||||
|
||||
// Client scenarios map - built from list
|
||||
export const clientScenarios = new Map<string, ClientScenario>(
|
||||
allClientScenariosList.map((scenario) => [scenario.name, scenario])
|
||||
);
|
||||
|
||||
// Scenario scenarios
|
||||
const scenariosList: Scenario[] = [
|
||||
new InitializeScenario(),
|
||||
new ToolsCallScenario(),
|
||||
new AuthBasicDCRScenario(),
|
||||
new AuthBasicMetadataVar1Scenario(),
|
||||
new ElicitationClientDefaultsScenario()
|
||||
];
|
||||
|
||||
// Scenarios map - built from list
|
||||
export const scenarios = new Map<string, Scenario>(
|
||||
scenariosList.map((scenario) => [scenario.name, scenario])
|
||||
);
|
||||
|
||||
export function registerScenario(name: string, scenario: Scenario): void {
|
||||
scenarios.set(name, scenario);
|
||||
|
|
@ -116,3 +144,7 @@ export function listScenarios(): string[] {
|
|||
export function listClientScenarios(): string[] {
|
||||
return Array.from(clientScenarios.keys());
|
||||
}
|
||||
|
||||
export function listActiveClientScenarios(): string[] {
|
||||
return activeClientScenariosList.map((scenario) => scenario.name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { listClientScenarios, getClientScenario } from '../index.js';
|
||||
import { getClientScenario, listActiveClientScenarios } from '../index.js';
|
||||
import path from 'path';
|
||||
|
||||
describe('Server Scenarios', () => {
|
||||
|
|
@ -76,7 +76,7 @@ describe('Server Scenarios', () => {
|
|||
});
|
||||
|
||||
// Generate individual test for each scenario
|
||||
const scenarios = listClientScenarios();
|
||||
const scenarios = listActiveClientScenarios();
|
||||
|
||||
for (const scenarioName of scenarios) {
|
||||
it(`${scenarioName}`, async () => {
|
||||
|
|
|
|||
375
src/scenarios/server/elicitation-enums.ts
Normal file
375
src/scenarios/server/elicitation-enums.ts
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
/**
|
||||
* SEP-1330: Elicitation enum schema improvements test scenarios for MCP servers
|
||||
*/
|
||||
|
||||
import { ClientScenario, ConformanceCheck } from '../../types.js';
|
||||
import { connectToServer } from './client-helper.js';
|
||||
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
||||
|
||||
export class ElicitationEnumsScenario implements ClientScenario {
|
||||
name = 'elicitation-sep1330-enums';
|
||||
description = 'Test elicitation with enum schema improvements (SEP-1330)';
|
||||
|
||||
async run(serverUrl: string): Promise<ConformanceCheck[]> {
|
||||
const checks: ConformanceCheck[] = [];
|
||||
|
||||
try {
|
||||
const connection = await connectToServer(serverUrl);
|
||||
|
||||
let capturedRequest: any = null;
|
||||
connection.client.setRequestHandler(
|
||||
ElicitRequestSchema,
|
||||
async (request) => {
|
||||
capturedRequest = request;
|
||||
// Return mock data for all enum types
|
||||
return {
|
||||
action: 'accept',
|
||||
content: {
|
||||
untitledSingle: 'option1',
|
||||
titledSingle: 'value1',
|
||||
legacyEnum: 'opt1',
|
||||
untitledMulti: ['option1', 'option2'],
|
||||
titledMulti: ['value1', 'value2']
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
await connection.client.callTool({
|
||||
name: 'test_elicitation_sep1330_enums',
|
||||
arguments: {}
|
||||
});
|
||||
|
||||
// Validate that elicitation was requested
|
||||
if (!capturedRequest) {
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-general',
|
||||
name: 'ElicitationSEP1330General',
|
||||
description: 'Server requests elicitation with enum schemas',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: 'Server did not request elicitation from client',
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
]
|
||||
});
|
||||
await connection.close();
|
||||
return checks;
|
||||
}
|
||||
|
||||
const schema = capturedRequest.params?.requestedSchema;
|
||||
const properties = schema?.properties;
|
||||
|
||||
// Validate untitled single-select enum
|
||||
const untitledSingleErrors: string[] = [];
|
||||
if (!properties?.untitledSingle) {
|
||||
untitledSingleErrors.push(
|
||||
'Missing untitled single-select enum field "untitledSingle"'
|
||||
);
|
||||
} else {
|
||||
if (properties.untitledSingle.type !== 'string') {
|
||||
untitledSingleErrors.push(
|
||||
`Expected type "string", got "${properties.untitledSingle.type}"`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!properties.untitledSingle.enum ||
|
||||
!Array.isArray(properties.untitledSingle.enum)
|
||||
) {
|
||||
untitledSingleErrors.push('Missing or invalid enum array');
|
||||
}
|
||||
if (properties.untitledSingle.oneOf) {
|
||||
untitledSingleErrors.push(
|
||||
'Untitled enum should not have oneOf property'
|
||||
);
|
||||
}
|
||||
if (properties.untitledSingle.enumNames) {
|
||||
untitledSingleErrors.push(
|
||||
'Untitled enum should not have enumNames property'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-untitled-single',
|
||||
name: 'ElicitationSEP1330UntitledSingle',
|
||||
description: 'Untitled single-select enum schema uses enum array',
|
||||
status: untitledSingleErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
untitledSingleErrors.length > 0
|
||||
? untitledSingleErrors.join('; ')
|
||||
: undefined,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
field: 'untitledSingle',
|
||||
schema: properties?.untitledSingle
|
||||
}
|
||||
});
|
||||
|
||||
// Validate titled single-select enum (using oneOf with const/title)
|
||||
const titledSingleErrors: string[] = [];
|
||||
if (!properties?.titledSingle) {
|
||||
titledSingleErrors.push(
|
||||
'Missing titled single-select enum field "titledSingle"'
|
||||
);
|
||||
} else {
|
||||
if (properties.titledSingle.type !== 'string') {
|
||||
titledSingleErrors.push(
|
||||
`Expected type "string", got "${properties.titledSingle.type}"`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!properties.titledSingle.oneOf ||
|
||||
!Array.isArray(properties.titledSingle.oneOf)
|
||||
) {
|
||||
titledSingleErrors.push(
|
||||
'Missing or invalid oneOf array for titled enum'
|
||||
);
|
||||
} else {
|
||||
// Validate oneOf structure has const/title pairs
|
||||
const invalidItems = properties.titledSingle.oneOf.filter(
|
||||
(item: any) =>
|
||||
typeof item.const !== 'string' || typeof item.title !== 'string'
|
||||
);
|
||||
if (invalidItems.length > 0) {
|
||||
titledSingleErrors.push(
|
||||
`oneOf items must have "const" (string) and "title" (string) properties`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (properties.titledSingle.enum) {
|
||||
titledSingleErrors.push(
|
||||
'Titled enum should use oneOf instead of enum array'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-titled-single',
|
||||
name: 'ElicitationSEP1330TitledSingle',
|
||||
description:
|
||||
'Titled single-select enum schema uses oneOf with const/title',
|
||||
status: titledSingleErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
titledSingleErrors.length > 0
|
||||
? titledSingleErrors.join('; ')
|
||||
: undefined,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
field: 'titledSingle',
|
||||
schema: properties?.titledSingle
|
||||
}
|
||||
});
|
||||
|
||||
// Validate legacy titled enum (using enumNames - deprecated)
|
||||
const legacyEnumErrors: string[] = [];
|
||||
if (!properties?.legacyEnum) {
|
||||
legacyEnumErrors.push('Missing legacy titled enum field "legacyEnum"');
|
||||
} else {
|
||||
if (properties.legacyEnum.type !== 'string') {
|
||||
legacyEnumErrors.push(
|
||||
`Expected type "string", got "${properties.legacyEnum.type}"`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!properties.legacyEnum.enum ||
|
||||
!Array.isArray(properties.legacyEnum.enum)
|
||||
) {
|
||||
legacyEnumErrors.push('Missing or invalid enum array');
|
||||
}
|
||||
if (
|
||||
!properties.legacyEnum.enumNames ||
|
||||
!Array.isArray(properties.legacyEnum.enumNames)
|
||||
) {
|
||||
legacyEnumErrors.push(
|
||||
'Missing or invalid enumNames array for legacy titled enum'
|
||||
);
|
||||
} else if (
|
||||
properties.legacyEnum.enum &&
|
||||
properties.legacyEnum.enumNames.length !==
|
||||
properties.legacyEnum.enum.length
|
||||
) {
|
||||
legacyEnumErrors.push(
|
||||
`enumNames length (${properties.legacyEnum.enumNames.length}) must match enum length (${properties.legacyEnum.enum.length})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-legacy-enumnames',
|
||||
name: 'ElicitationSEP1330LegacyEnumNames',
|
||||
description: 'Legacy titled enum schema uses enumNames (deprecated)',
|
||||
status: legacyEnumErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
legacyEnumErrors.length > 0 ? legacyEnumErrors.join('; ') : undefined,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
field: 'legacyEnum',
|
||||
schema: properties?.legacyEnum
|
||||
}
|
||||
});
|
||||
|
||||
// Validate untitled multi-select enum
|
||||
const untitledMultiErrors: string[] = [];
|
||||
if (!properties?.untitledMulti) {
|
||||
untitledMultiErrors.push(
|
||||
'Missing untitled multi-select enum field "untitledMulti"'
|
||||
);
|
||||
} else {
|
||||
if (properties.untitledMulti.type !== 'array') {
|
||||
untitledMultiErrors.push(
|
||||
`Expected type "array", got "${properties.untitledMulti.type}"`
|
||||
);
|
||||
}
|
||||
if (!properties.untitledMulti.items) {
|
||||
untitledMultiErrors.push('Missing items property for array type');
|
||||
} else {
|
||||
if (properties.untitledMulti.items.type !== 'string') {
|
||||
untitledMultiErrors.push(
|
||||
`Expected items.type "string", got "${properties.untitledMulti.items.type}"`
|
||||
);
|
||||
}
|
||||
if (
|
||||
!properties.untitledMulti.items.enum ||
|
||||
!Array.isArray(properties.untitledMulti.items.enum)
|
||||
) {
|
||||
untitledMultiErrors.push('Missing or invalid items.enum array');
|
||||
}
|
||||
if (properties.untitledMulti.items.anyOf) {
|
||||
untitledMultiErrors.push(
|
||||
'Untitled multi-select should use items.enum, not items.anyOf'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-untitled-multi',
|
||||
name: 'ElicitationSEP1330UntitledMulti',
|
||||
description:
|
||||
'Untitled multi-select enum schema uses array with items.enum',
|
||||
status: untitledMultiErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
untitledMultiErrors.length > 0
|
||||
? untitledMultiErrors.join('; ')
|
||||
: undefined,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
field: 'untitledMulti',
|
||||
schema: properties?.untitledMulti
|
||||
}
|
||||
});
|
||||
|
||||
// Validate titled multi-select enum (using items.anyOf with const/title)
|
||||
const titledMultiErrors: string[] = [];
|
||||
if (!properties?.titledMulti) {
|
||||
titledMultiErrors.push(
|
||||
'Missing titled multi-select enum field "titledMulti"'
|
||||
);
|
||||
} else {
|
||||
if (properties.titledMulti.type !== 'array') {
|
||||
titledMultiErrors.push(
|
||||
`Expected type "array", got "${properties.titledMulti.type}"`
|
||||
);
|
||||
}
|
||||
if (!properties.titledMulti.items) {
|
||||
titledMultiErrors.push('Missing items property for array type');
|
||||
} else {
|
||||
if (
|
||||
!properties.titledMulti.items.anyOf ||
|
||||
!Array.isArray(properties.titledMulti.items.anyOf)
|
||||
) {
|
||||
titledMultiErrors.push(
|
||||
'Missing or invalid items.anyOf array for titled multi-select'
|
||||
);
|
||||
} else {
|
||||
// Validate anyOf structure has const/title pairs
|
||||
const invalidItems = properties.titledMulti.items.anyOf.filter(
|
||||
(item: any) =>
|
||||
typeof item.const !== 'string' || typeof item.title !== 'string'
|
||||
);
|
||||
if (invalidItems.length > 0) {
|
||||
titledMultiErrors.push(
|
||||
`items.anyOf entries must have "const" (string) and "title" (string) properties`
|
||||
);
|
||||
}
|
||||
}
|
||||
if (properties.titledMulti.items.enum) {
|
||||
titledMultiErrors.push(
|
||||
'Titled multi-select should use items.anyOf, not items.enum'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-titled-multi',
|
||||
name: 'ElicitationSEP1330TitledMulti',
|
||||
description:
|
||||
'Titled multi-select enum schema uses array with items.anyOf',
|
||||
status: titledMultiErrors.length === 0 ? 'SUCCESS' : 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage:
|
||||
titledMultiErrors.length > 0
|
||||
? titledMultiErrors.join('; ')
|
||||
: undefined,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
],
|
||||
details: {
|
||||
field: 'titledMulti',
|
||||
schema: properties?.titledMulti
|
||||
}
|
||||
});
|
||||
|
||||
await connection.close();
|
||||
} catch (error) {
|
||||
checks.push({
|
||||
id: 'elicitation-sep1330-general',
|
||||
name: 'ElicitationSEP1330General',
|
||||
description: 'Server requests elicitation with enum schemas',
|
||||
status: 'FAILURE',
|
||||
timestamp: new Date().toISOString(),
|
||||
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
specReferences: [
|
||||
{
|
||||
id: 'SEP-1330',
|
||||
url: 'https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330'
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue