Skip to content

Commit c6c12e7

Browse files
authored
mcp: adopt sep-1330 for enhanced enum picks (#276963)
Refs modelcontextprotocol/modelcontextprotocol#1148
1 parent 40386f8 commit c6c12e7

File tree

2 files changed

+242
-12
lines changed

2 files changed

+242
-12
lines changed

src/vs/workbench/contrib/mcp/browser/mcpElicitationService.ts

Lines changed: 82 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { Action } from '../../../../base/common/actions.js';
77
import { assertNever } from '../../../../base/common/assert.js';
88
import { CancellationToken } from '../../../../base/common/cancellation.js';
99
import { DisposableStore } from '../../../../base/common/lifecycle.js';
10+
import { isDefined } from '../../../../base/common/types.js';
1011
import { localize } from '../../../../nls.js';
1112
import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js';
1213
import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';
@@ -20,6 +21,31 @@ import { MCP } from '../common/modelContextProtocol.js';
2021

2122
const noneItem: IQuickPickItem = { id: undefined, label: localize('mcp.elicit.enum.none', 'None'), description: localize('mcp.elicit.enum.none.description', 'No selection'), alwaysShow: true };
2223

24+
function isLegacyTitledEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.LegacyTitledEnumSchema & { enumNames: string[] } {
25+
const cast = schema as MCP.LegacyTitledEnumSchema;
26+
return cast.type === 'string' && Array.isArray(cast.enum) && Array.isArray(cast.enumNames);
27+
}
28+
29+
function isUntitledEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.LegacyTitledEnumSchema | MCP.UntitledSingleSelectEnumSchema {
30+
const cast = schema as MCP.LegacyTitledEnumSchema | MCP.UntitledSingleSelectEnumSchema;
31+
return cast.type === 'string' && Array.isArray(cast.enum);
32+
}
33+
34+
function isTitledSingleEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.TitledSingleSelectEnumSchema {
35+
const cast = schema as MCP.TitledSingleSelectEnumSchema;
36+
return cast.type === 'string' && Array.isArray(cast.oneOf);
37+
}
38+
39+
function isUntitledMultiEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.UntitledMultiSelectEnumSchema {
40+
const cast = schema as MCP.UntitledMultiSelectEnumSchema;
41+
return cast.type === 'array' && !!cast.items?.enum;
42+
}
43+
44+
function isTitledMultiEnumSchema(schema: MCP.PrimitiveSchemaDefinition): schema is MCP.TitledMultiSelectEnumSchema {
45+
const cast = schema as MCP.TitledMultiSelectEnumSchema;
46+
return cast.type === 'array' && !!cast.items?.anyOf;
47+
}
48+
2349
export class McpElicitationService implements IMcpElicitationService {
2450
declare readonly _serviceBrand: undefined;
2551

@@ -82,7 +108,7 @@ export class McpElicitationService implements IMcpElicitationService {
82108
try {
83109
const properties = Object.entries(elicitation.requestedSchema.properties);
84110
const requiredFields = new Set(elicitation.requestedSchema.required || []);
85-
const results: Record<string, string | number | boolean> = {};
111+
const results: Record<string, string | number | boolean | string[]> = {};
86112
const backSnapshots: { value: string; validationMessage?: string }[] = [];
87113

88114
quickPick.title = elicitation.message;
@@ -102,12 +128,20 @@ export class McpElicitationService implements IMcpElicitationService {
102128
quickPick.validationMessage = '';
103129
quickPick.buttons = i > 0 ? [this._quickInputService.backButton] : [];
104130

105-
let result: { type: 'value'; value: string | number | boolean | undefined } | { type: 'back' } | { type: 'cancel' };
131+
let result: { type: 'value'; value: string | number | boolean | undefined | string[] } | { type: 'back' } | { type: 'cancel' };
106132
if (schema.type === 'boolean') {
107-
result = await this._handleEnumField(quickPick, { ...schema, type: 'string', enum: ['true', 'false'], default: schema.default ? String(schema.default) : undefined }, isRequired, store, token);
133+
result = await this._handleEnumField(quickPick, { enum: [{ const: 'true' }, { const: 'false' }], default: schema.default ? String(schema.default) : undefined }, isRequired, store, token);
108134
if (result.type === 'value') { result.value = result.value === 'true' ? true : false; }
109-
} else if (schema.type === 'string' && 'enum' in schema) {
110-
result = await this._handleEnumField(quickPick, schema, isRequired, store, token);
135+
} else if (isLegacyTitledEnumSchema(schema)) {
136+
result = await this._handleEnumField(quickPick, { enum: schema.enum.map((v, i) => ({ const: v, title: schema.enumNames[i] })), default: schema.default }, isRequired, store, token);
137+
} else if (isUntitledEnumSchema(schema)) {
138+
result = await this._handleEnumField(quickPick, { enum: schema.enum.map(v => ({ const: v })), default: schema.default }, isRequired, store, token);
139+
} else if (isTitledSingleEnumSchema(schema)) {
140+
result = await this._handleEnumField(quickPick, { enum: schema.oneOf, default: schema.default }, isRequired, store, token);
141+
} else if (isTitledMultiEnumSchema(schema)) {
142+
result = await this._handleMultiEnumField(quickPick, { enum: schema.items.anyOf, default: schema.default }, isRequired, store, token);
143+
} else if (isUntitledMultiEnumSchema(schema)) {
144+
result = await this._handleMultiEnumField(quickPick, { enum: schema.items.enum.map(v => ({ const: v })), default: schema.default }, isRequired, store, token);
111145
} else {
112146
result = await this._handleInputField(quickPick, schema, isRequired, store, token);
113147
if (result.type === 'value' && (schema.type === 'number' || schema.type === 'integer')) {
@@ -152,23 +186,23 @@ export class McpElicitationService implements IMcpElicitationService {
152186

153187
private async _handleEnumField(
154188
quickPick: IQuickPick<IQuickPickItem>,
155-
schema: MCP.EnumSchema,
189+
schema: { default?: string; enum: { const: string; title?: string }[] },
156190
required: boolean,
157191
store: DisposableStore,
158192
token: CancellationToken
159193
) {
160-
const items: IQuickPickItem[] = schema.enum.map((value, index) => ({
194+
const items: IQuickPickItem[] = schema.enum.map(({ const: value, title }) => ({
161195
id: value,
162196
label: value,
163-
description: schema.enumNames?.[index],
197+
description: title,
164198
}));
165199

166200
if (!required) {
167201
items.push(noneItem);
168202
}
169203

170-
quickPick.items = items;
171204
quickPick.canSelectMany = false;
205+
quickPick.items = items;
172206
if (schema.default !== undefined) {
173207
quickPick.activeItems = items.filter(item => item.id === schema.default);
174208
}
@@ -188,6 +222,45 @@ export class McpElicitationService implements IMcpElicitationService {
188222
});
189223
}
190224

225+
private async _handleMultiEnumField(
226+
quickPick: IQuickPick<IQuickPickItem>,
227+
schema: { default?: string[]; enum: { const: string; title?: string }[] },
228+
required: boolean,
229+
store: DisposableStore,
230+
token: CancellationToken
231+
) {
232+
const items: IQuickPickItem[] = schema.enum.map(({ const: value, title }) => ({
233+
id: value,
234+
label: value,
235+
description: title,
236+
picked: !!schema.default?.includes(value),
237+
pickable: true,
238+
}));
239+
240+
if (!required) {
241+
items.push(noneItem);
242+
}
243+
244+
quickPick.canSelectMany = true;
245+
quickPick.items = items;
246+
247+
return new Promise<{ type: 'value'; value: string[] | undefined } | { type: 'back' } | { type: 'cancel' }>(resolve => {
248+
store.add(token.onCancellationRequested(() => resolve({ type: 'cancel' })));
249+
store.add(quickPick.onDidAccept(() => {
250+
const selected = quickPick.selectedItems[0];
251+
if (selected.id === undefined) {
252+
resolve({ type: 'value', value: undefined });
253+
} else {
254+
resolve({ type: 'value', value: quickPick.selectedItems.map(i => i.id).filter(isDefined) });
255+
}
256+
}));
257+
store.add(quickPick.onDidTriggerButton(() => resolve({ type: 'back' })));
258+
store.add(quickPick.onDidHide(() => resolve({ type: 'cancel' })));
259+
260+
quickPick.show();
261+
});
262+
}
263+
191264
private async _handleInputField(
192265
quickPick: IQuickPick<IQuickPickItem>,
193266
schema: MCP.NumberSchema | MCP.StringSchema,

src/vs/workbench/contrib/mcp/common/modelContextProtocol.ts

Lines changed: 160 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,15 +1546,172 @@ export namespace MCP {/* JSON-RPC types */
15461546
default?: boolean;
15471547
}
15481548

1549-
export interface EnumSchema {
1549+
/**
1550+
* Schema for single-selection enumeration without display titles for options.
1551+
*/
1552+
export interface UntitledSingleSelectEnumSchema {
15501553
type: "string";
1554+
/**
1555+
* Optional title for the enum field.
1556+
*/
15511557
title?: string;
1558+
/**
1559+
* Optional description for the enum field.
1560+
*/
15521561
description?: string;
1562+
/**
1563+
* Array of enum values to choose from.
1564+
*/
15531565
enum: string[];
1554-
enumNames?: string[]; // Display names for enum values
1566+
/**
1567+
* Optional default value.
1568+
*/
15551569
default?: string;
15561570
}
15571571

1572+
/**
1573+
* Schema for single-selection enumeration with display titles for each option.
1574+
*/
1575+
export interface TitledSingleSelectEnumSchema {
1576+
type: "string";
1577+
/**
1578+
* Optional title for the enum field.
1579+
*/
1580+
title?: string;
1581+
/**
1582+
* Optional description for the enum field.
1583+
*/
1584+
description?: string;
1585+
/**
1586+
* Array of enum options with values and display labels.
1587+
*/
1588+
oneOf: Array<{
1589+
/**
1590+
* The enum value.
1591+
*/
1592+
const: string;
1593+
/**
1594+
* Display label for this option.
1595+
*/
1596+
title: string;
1597+
}>;
1598+
/**
1599+
* Optional default value.
1600+
*/
1601+
default?: string;
1602+
}
1603+
1604+
// Combined single selection enumeration
1605+
export type SingleSelectEnumSchema =
1606+
| UntitledSingleSelectEnumSchema
1607+
| TitledSingleSelectEnumSchema;
1608+
1609+
/**
1610+
* Schema for multiple-selection enumeration without display titles for options.
1611+
*/
1612+
export interface UntitledMultiSelectEnumSchema {
1613+
type: "array";
1614+
/**
1615+
* Optional title for the enum field.
1616+
*/
1617+
title?: string;
1618+
/**
1619+
* Optional description for the enum field.
1620+
*/
1621+
description?: string;
1622+
/**
1623+
* Minimum number of items to select.
1624+
*/
1625+
minItems?: number;
1626+
/**
1627+
* Maximum number of items to select.
1628+
*/
1629+
maxItems?: number;
1630+
/**
1631+
* Schema for the array items.
1632+
*/
1633+
items: {
1634+
type: "string";
1635+
/**
1636+
* Array of enum values to choose from.
1637+
*/
1638+
enum: string[];
1639+
};
1640+
/**
1641+
* Optional default value.
1642+
*/
1643+
default?: string[];
1644+
}
1645+
1646+
/**
1647+
* Schema for multiple-selection enumeration with display titles for each option.
1648+
*/
1649+
export interface TitledMultiSelectEnumSchema {
1650+
type: "array";
1651+
/**
1652+
* Optional title for the enum field.
1653+
*/
1654+
title?: string;
1655+
/**
1656+
* Optional description for the enum field.
1657+
*/
1658+
description?: string;
1659+
/**
1660+
* Minimum number of items to select.
1661+
*/
1662+
minItems?: number;
1663+
/**
1664+
* Maximum number of items to select.
1665+
*/
1666+
maxItems?: number;
1667+
/**
1668+
* Schema for array items with enum options and display labels.
1669+
*/
1670+
items: {
1671+
/**
1672+
* Array of enum options with values and display labels.
1673+
*/
1674+
anyOf: Array<{
1675+
/**
1676+
* The constant enum value.
1677+
*/
1678+
const: string;
1679+
/**
1680+
* Display title for this option.
1681+
*/
1682+
title: string;
1683+
}>;
1684+
};
1685+
/**
1686+
* Optional default value.
1687+
*/
1688+
default?: string[];
1689+
}
1690+
1691+
// Combined multiple selection enumeration
1692+
export type MultiSelectEnumSchema =
1693+
| UntitledMultiSelectEnumSchema
1694+
| TitledMultiSelectEnumSchema;
1695+
1696+
1697+
export interface LegacyTitledEnumSchema {
1698+
type: "string";
1699+
title?: string;
1700+
description?: string;
1701+
enum: string[];
1702+
/**
1703+
* (Legacy) Display names for enum values.
1704+
* Non-standard according to JSON schema 2020-12.
1705+
*/
1706+
enumNames?: string[];
1707+
default?: string;
1708+
}
1709+
1710+
export type EnumSchema =
1711+
| SingleSelectEnumSchema
1712+
| MultiSelectEnumSchema
1713+
| LegacyTitledEnumSchema;
1714+
15581715
/**
15591716
* The client's response to an elicitation request.
15601717
*
@@ -1573,7 +1730,7 @@ export namespace MCP {/* JSON-RPC types */
15731730
* The submitted form data, only present when action is "accept".
15741731
* Contains values matching the requested schema.
15751732
*/
1576-
content?: { [key: string]: string | number | boolean };
1733+
content?: { [key: string]: string | number | boolean | string[] };
15771734
}
15781735

15791736
/* Client messages */

0 commit comments

Comments
 (0)