conformance/src/scenarios/server/elicitation-enums.ts
Paul Carleton d303013e5d
[auth] Implement CIMD conformance test (#37)
* add cimd test and negative test

* Improve auth-test-no-cimd.ts documentation for clarity

Address review feedback: expanded comments to better explain what CIMD is,
why this client is non-compliant, and what behavior it's testing.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Remove .js extensions from relative imports

tsx doesn't require .js extensions for TypeScript imports.
Keeps .js for @modelcontextprotocol/sdk imports as those are external packages.

Also improves auth-test-no-cimd.ts documentation for clarity.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Skip auth/basic-cimd test until SDK support lands

Waiting on typescript-sdk PR #1127 for CIMD support.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix trailing whitespace in auth-test-no-cimd.ts

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 21:00:13 +00:00

398 lines
14 KiB
TypeScript

/**
* SEP-1330: Elicitation enum schema improvements test scenarios for MCP servers
*/
import { ClientScenario, ConformanceCheck } from '../../types';
import { connectToServer } from './client-helper';
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).
**Server Implementation Requirements:**
Implement a tool named \`test_elicitation_sep1330_enums\` (no arguments) that requests \`elicitation/create\` from the client with a schema containing all 5 enum variants:
1. **Untitled single-select**: \`{ type: "string", enum: ["option1", "option2", "option3"] }\`
2. **Titled single-select**: \`{ type: "string", oneOf: [{ const: "value1", title: "First Option" }, ...] }\`
3. **Legacy titled (deprecated)**: \`{ type: "string", enum: ["opt1", "opt2", "opt3"], enumNames: ["Option One", "Option Two", "Option Three"] }\`
4. **Untitled multi-select**: \`{ type: "array", items: { type: "string", enum: ["option1", "option2", "option3"] } }\`
5. **Titled multi-select**: \`{ type: "array", items: { anyOf: [{ const: "value1", title: "First Choice" }, ...] } }\`
**Returns**: Text content with the elicitation result
\`\`\`json
{
"content": [
{
"type": "text",
"text": "Elicitation completed: action=<accept/decline/cancel>, content={...}"
}
]
}
\`\`\``;
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;
}
}