pi-mcp-adapter 2.9.0 → 2.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +27 -4
- package/commands.ts +6 -5
- package/direct-tools.ts +18 -4
- package/elicitation-handler.ts +252 -258
- package/index.ts +27 -2
- package/init.ts +7 -6
- package/mcp-auth-flow.ts +127 -30
- package/mcp-oauth-provider.ts +48 -1
- package/mcp-panel.ts +12 -9
- package/mcp-setup-panel.ts +13 -9
- package/package.json +3 -2
- package/panel-keys.ts +37 -0
- package/proxy-modes.ts +119 -4
- package/server-manager.ts +88 -39
- package/tool-metadata.ts +98 -34
- package/types.ts +0 -1
- package/ui-resource-handler.ts +2 -1
- package/ui-session.ts +2 -1
package/elicitation-handler.ts
CHANGED
|
@@ -2,352 +2,346 @@ import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
3
|
import {
|
|
4
4
|
ElicitRequestSchema,
|
|
5
|
+
ErrorCode,
|
|
6
|
+
McpError,
|
|
5
7
|
type ElicitRequest,
|
|
6
8
|
type ElicitRequestFormParams,
|
|
7
9
|
type ElicitRequestURLParams,
|
|
8
10
|
type ElicitResult,
|
|
9
11
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
|
|
13
|
+
import type { JsonSchemaType } from "@modelcontextprotocol/sdk/validation/types.js";
|
|
10
14
|
import open from "open";
|
|
11
15
|
|
|
12
|
-
export type
|
|
16
|
+
export type ElicitationValue = string | number | boolean | string[] | undefined;
|
|
17
|
+
type FormProperty = ElicitRequestFormParams["requestedSchema"]["properties"][string];
|
|
13
18
|
|
|
14
|
-
export
|
|
15
|
-
value: string;
|
|
16
|
-
label?: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export type ExtensionUIFormField =
|
|
21
|
-
| {
|
|
22
|
-
type: "text";
|
|
23
|
-
name: string;
|
|
24
|
-
label: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
placeholder?: string;
|
|
27
|
-
required?: boolean;
|
|
28
|
-
defaultValue?: string;
|
|
29
|
-
minLength?: number;
|
|
30
|
-
maxLength?: number;
|
|
31
|
-
pattern?: string;
|
|
32
|
-
}
|
|
33
|
-
| {
|
|
34
|
-
type: "number" | "integer";
|
|
35
|
-
name: string;
|
|
36
|
-
label: string;
|
|
37
|
-
description?: string;
|
|
38
|
-
required?: boolean;
|
|
39
|
-
defaultValue?: number;
|
|
40
|
-
minimum?: number;
|
|
41
|
-
maximum?: number;
|
|
42
|
-
}
|
|
43
|
-
| {
|
|
44
|
-
type: "boolean";
|
|
45
|
-
name: string;
|
|
46
|
-
label: string;
|
|
47
|
-
description?: string;
|
|
48
|
-
defaultValue?: boolean;
|
|
49
|
-
}
|
|
50
|
-
| {
|
|
51
|
-
type: "select";
|
|
52
|
-
name: string;
|
|
53
|
-
label: string;
|
|
54
|
-
description?: string;
|
|
55
|
-
required?: boolean;
|
|
56
|
-
options: ExtensionUIFormSelectOption[];
|
|
57
|
-
defaultValue?: string;
|
|
58
|
-
}
|
|
59
|
-
| {
|
|
60
|
-
type: "multiSelect";
|
|
61
|
-
name: string;
|
|
62
|
-
label: string;
|
|
63
|
-
description?: string;
|
|
64
|
-
required?: boolean;
|
|
65
|
-
options: ExtensionUIFormSelectOption[];
|
|
66
|
-
defaultValue?: string[];
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
export interface ExtensionUIFormRequest {
|
|
70
|
-
title: string;
|
|
71
|
-
message?: string;
|
|
72
|
-
fields: ExtensionUIFormField[];
|
|
73
|
-
submitLabel?: string;
|
|
74
|
-
secondaryLabel?: string;
|
|
75
|
-
cancelLabel?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export type ExtensionUIFormResult =
|
|
79
|
-
| { action: "submit"; values: Record<string, ExtensionUIFormValue> }
|
|
80
|
-
| { action: "secondary" }
|
|
81
|
-
| { action: "cancel" };
|
|
82
|
-
|
|
83
|
-
export interface ElicitationUIContext extends ExtensionUIContext {
|
|
84
|
-
form(request: ExtensionUIFormRequest): Promise<ExtensionUIFormResult>;
|
|
85
|
-
}
|
|
19
|
+
export type ElicitationUIContext = ExtensionUIContext;
|
|
86
20
|
|
|
87
21
|
export interface ElicitationHandlerOptions {
|
|
88
22
|
serverName: string;
|
|
89
23
|
ui: ElicitationUIContext;
|
|
90
|
-
|
|
24
|
+
allowUrl: boolean;
|
|
25
|
+
onUrlAccepted?: (elicitationId: string) => void;
|
|
91
26
|
}
|
|
92
27
|
|
|
93
|
-
export type ServerElicitationConfig = Omit<ElicitationHandlerOptions, "serverName">;
|
|
28
|
+
export type ServerElicitationConfig = Omit<ElicitationHandlerOptions, "serverName" | "onUrlAccepted">;
|
|
94
29
|
|
|
95
30
|
export function registerElicitationHandler(client: Client, options: ElicitationHandlerOptions): void {
|
|
96
|
-
client.setRequestHandler(ElicitRequestSchema, (request) =>
|
|
97
|
-
|
|
98
|
-
});
|
|
31
|
+
client.setRequestHandler(ElicitRequestSchema, (request) =>
|
|
32
|
+
handleElicitationRequest(options, request));
|
|
99
33
|
}
|
|
100
34
|
|
|
101
35
|
export async function handleElicitationRequest(
|
|
102
36
|
options: ElicitationHandlerOptions,
|
|
103
37
|
request: ElicitRequest,
|
|
104
38
|
): Promise<ElicitResult> {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
return handleFormElicitation(options, params);
|
|
39
|
+
return request.params.mode === "url"
|
|
40
|
+
? handleUrlElicitation(options, request.params)
|
|
41
|
+
: handleFormElicitation(options, request.params);
|
|
110
42
|
}
|
|
111
43
|
|
|
112
44
|
export async function handleFormElicitation(
|
|
113
45
|
options: ElicitationHandlerOptions,
|
|
114
46
|
params: ElicitRequestFormParams,
|
|
115
47
|
): Promise<ElicitResult> {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
48
|
+
const decision = await options.ui.select(
|
|
49
|
+
`MCP Input Request\nServer: ${options.serverName}\n\n${params.message}`,
|
|
50
|
+
["Continue", "Decline"],
|
|
51
|
+
);
|
|
52
|
+
if (decision === undefined) return { action: "cancel" };
|
|
53
|
+
if (decision === "Decline") return { action: "decline" };
|
|
54
|
+
|
|
55
|
+
const values: Record<string, ElicitationValue> = {};
|
|
56
|
+
const properties = Object.entries(params.requestedSchema.properties);
|
|
57
|
+
for (const [name, schema] of properties) {
|
|
58
|
+
const value = await collectValidField(options.ui, params, name, schema);
|
|
59
|
+
if (!("value" in value)) return { action: "cancel" };
|
|
60
|
+
values[name] = value.value;
|
|
120
61
|
}
|
|
121
|
-
return {
|
|
122
|
-
action: "accept",
|
|
123
|
-
content: coerceAndValidateFormValues(params, result.values),
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
62
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
submitLabel: "Open",
|
|
147
|
-
secondaryLabel: "Decline",
|
|
148
|
-
cancelLabel: "Cancel",
|
|
149
|
-
});
|
|
150
|
-
if (result.action === "secondary") return { action: "decline" };
|
|
151
|
-
if (result.action === "cancel") return { action: "cancel" };
|
|
63
|
+
while (true) {
|
|
64
|
+
const content = coerceAndValidateFormValues(params, values);
|
|
65
|
+
const action = await options.ui.select(
|
|
66
|
+
formatReview(options.serverName, properties, content),
|
|
67
|
+
properties.length > 0 ? ["Submit", "Edit", "Decline"] : ["Submit", "Decline"],
|
|
68
|
+
);
|
|
69
|
+
if (action === undefined) return { action: "cancel" };
|
|
70
|
+
if (action === "Decline") return { action: "decline" };
|
|
71
|
+
if (action === "Submit") return { action: "accept", content };
|
|
72
|
+
|
|
73
|
+
const labels = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)} (${name})`);
|
|
74
|
+
const selected = await options.ui.select("Choose a field to edit", labels);
|
|
75
|
+
if (selected === undefined) return { action: "cancel" };
|
|
76
|
+
const property = properties[labels.indexOf(selected)];
|
|
77
|
+
if (!property) continue;
|
|
78
|
+
const [name, schema] = property;
|
|
79
|
+
const value = await collectValidField(options.ui, params, name, schema, values[name]);
|
|
80
|
+
if (!("value" in value)) return { action: "cancel" };
|
|
81
|
+
values[name] = value.value;
|
|
152
82
|
}
|
|
153
|
-
|
|
154
|
-
await open(browserUrl.toString());
|
|
155
|
-
options.ui.notify("Opened browser for MCP elicitation.", "info");
|
|
156
|
-
return { action: "accept" };
|
|
157
83
|
}
|
|
158
84
|
|
|
159
|
-
|
|
160
|
-
|
|
85
|
+
async function collectValidField(
|
|
86
|
+
ui: ElicitationUIContext,
|
|
161
87
|
params: ElicitRequestFormParams,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (schema.type === "string" && "enum" in schema && Array.isArray(schema.enum)) {
|
|
189
|
-
const enumNames = "enumNames" in schema && Array.isArray(schema.enumNames) ? schema.enumNames : undefined;
|
|
190
|
-
return omitUndefined({
|
|
191
|
-
...base,
|
|
192
|
-
type: "select" as const,
|
|
193
|
-
options: schema.enum.map((value, index) => omitUndefined({ value, label: enumNames?.[index] })),
|
|
194
|
-
defaultValue: schema.default,
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (schema.type === "array") {
|
|
199
|
-
return omitUndefined({
|
|
200
|
-
...base,
|
|
201
|
-
type: "multiSelect" as const,
|
|
202
|
-
options: extractMultiSelectOptions(schema),
|
|
203
|
-
defaultValue: schema.default,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
88
|
+
name: string,
|
|
89
|
+
schema: FormProperty,
|
|
90
|
+
current?: ElicitationValue,
|
|
91
|
+
): Promise<{ cancelled: true } | { cancelled: false; value: ElicitationValue }> {
|
|
92
|
+
const required = params.requestedSchema.required?.includes(name) === true;
|
|
93
|
+
while (true) {
|
|
94
|
+
const result = await collectField(ui, params, name, schema, current);
|
|
95
|
+
if (!("value" in result)) return result;
|
|
96
|
+
try {
|
|
97
|
+
coerceAndValidateFormValues({
|
|
98
|
+
...params,
|
|
99
|
+
requestedSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: { [name]: schema },
|
|
102
|
+
...(required ? { required: [name] } : {}),
|
|
103
|
+
},
|
|
104
|
+
}, { [name]: result.value });
|
|
105
|
+
return result;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
108
|
+
current = result.value;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
206
112
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
113
|
+
async function collectField(
|
|
114
|
+
ui: ElicitationUIContext,
|
|
115
|
+
params: ElicitRequestFormParams,
|
|
116
|
+
name: string,
|
|
117
|
+
schema: FormProperty,
|
|
118
|
+
current?: ElicitationValue,
|
|
119
|
+
): Promise<{ cancelled: true } | { cancelled: false; value: ElicitationValue }> {
|
|
120
|
+
const required = params.requestedSchema.required?.includes(name) === true;
|
|
121
|
+
const title = [schema.title ?? humanizeName(name), required ? "(required)" : "", schema.description]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(" ");
|
|
124
|
+
|
|
125
|
+
if (schema.type === "string" && ("enum" in schema || "oneOf" in schema)) {
|
|
126
|
+
const choices = "oneOf" in schema
|
|
127
|
+
? schema.oneOf.map(option => ({ value: option.const, display: formatChoice(option.const, option.title) }))
|
|
128
|
+
: schema.enum.map((value, index) => ({
|
|
129
|
+
value,
|
|
130
|
+
display: formatChoice(value, "enumNames" in schema ? schema.enumNames?.[index] : undefined),
|
|
131
|
+
}));
|
|
132
|
+
const displays = uniqueLabels(choices.map(choice => choice.display));
|
|
133
|
+
const actions = [...displays];
|
|
134
|
+
const useDefault = schema.default === undefined ? undefined : uniqueAction("Use default", actions);
|
|
135
|
+
if (useDefault) actions.push(useDefault);
|
|
136
|
+
const omit = required ? undefined : uniqueAction("Omit", actions);
|
|
137
|
+
if (omit) actions.push(omit);
|
|
138
|
+
const action = await ui.select(title, actions);
|
|
139
|
+
if (action === undefined) return { cancelled: true };
|
|
140
|
+
if (action === useDefault) return { cancelled: false, value: schema.default };
|
|
141
|
+
if (action === omit) return { cancelled: false, value: undefined };
|
|
142
|
+
return { cancelled: false, value: choices[displays.indexOf(action)]?.value };
|
|
143
|
+
}
|
|
216
144
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
145
|
+
if (schema.type === "boolean") {
|
|
146
|
+
const actions = ["Yes", "No"];
|
|
147
|
+
if (schema.default !== undefined) actions.push("Use default");
|
|
148
|
+
if (!required) actions.push("Omit");
|
|
149
|
+
const action = await ui.select(title, actions);
|
|
150
|
+
if (action === undefined) return { cancelled: true };
|
|
151
|
+
if (action === "Use default") return { cancelled: false, value: schema.default };
|
|
152
|
+
if (action === "Omit") return { cancelled: false, value: undefined };
|
|
153
|
+
return { cancelled: false, value: action === "Yes" };
|
|
154
|
+
}
|
|
226
155
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
156
|
+
if (schema.type === "array") {
|
|
157
|
+
const actions = ["Choose values"];
|
|
158
|
+
if (schema.default !== undefined) actions.push("Use default");
|
|
159
|
+
if (!required) actions.push("Omit");
|
|
160
|
+
const action = await ui.select(title, actions);
|
|
161
|
+
if (action === undefined) return { cancelled: true };
|
|
162
|
+
if (action === "Use default") return { cancelled: false, value: schema.default };
|
|
163
|
+
if (action === "Omit") return { cancelled: false, value: undefined };
|
|
164
|
+
|
|
165
|
+
const choices = extractMultiSelectOptions(schema);
|
|
166
|
+
const selected = new Set(Array.isArray(current) ? current : []);
|
|
167
|
+
while (true) {
|
|
168
|
+
const displays = uniqueLabels(choices.map(choice => selected.has(choice.value) ? `✓ ${choice.display}` : choice.display));
|
|
169
|
+
const done = uniqueAction("Done", displays);
|
|
170
|
+
const picked = await ui.select(title, [...displays, done]);
|
|
171
|
+
if (picked === undefined) return { cancelled: true };
|
|
172
|
+
if (picked === done) return { cancelled: false, value: [...selected] };
|
|
173
|
+
const choice = choices[displays.indexOf(picked)];
|
|
174
|
+
if (!choice) continue;
|
|
175
|
+
if (selected.has(choice.value)) selected.delete(choice.value);
|
|
176
|
+
else selected.add(choice.value);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
238
179
|
|
|
239
|
-
|
|
240
|
-
if (
|
|
241
|
-
if (
|
|
242
|
-
|
|
180
|
+
const actions = ["Enter value"];
|
|
181
|
+
if (schema.default !== undefined) actions.push("Use default");
|
|
182
|
+
if (!required) actions.push("Omit");
|
|
183
|
+
const action = await ui.select(title, actions);
|
|
184
|
+
if (action === undefined) return { cancelled: true };
|
|
185
|
+
if (action === "Use default") return { cancelled: false, value: schema.default };
|
|
186
|
+
if (action === "Omit") return { cancelled: false, value: undefined };
|
|
187
|
+
const entered = await ui.input(title, current === undefined ? undefined : String(current));
|
|
188
|
+
return entered === undefined ? { cancelled: true } : { cancelled: false, value: entered };
|
|
243
189
|
}
|
|
244
190
|
|
|
245
191
|
export function coerceAndValidateFormValues(
|
|
246
192
|
params: ElicitRequestFormParams,
|
|
247
|
-
values: Record<string,
|
|
193
|
+
values: Record<string, ElicitationValue>,
|
|
248
194
|
): Record<string, string | number | boolean | string[]> {
|
|
249
195
|
const output: Record<string, string | number | boolean | string[]> = {};
|
|
250
196
|
const required = new Set(params.requestedSchema.required ?? []);
|
|
251
|
-
|
|
252
197
|
for (const [name, schema] of Object.entries(params.requestedSchema.properties)) {
|
|
253
|
-
const
|
|
254
|
-
if (
|
|
198
|
+
const value = values[name];
|
|
199
|
+
if (value === undefined) {
|
|
255
200
|
if (required.has(name)) throw new Error(`Missing required elicitation field: ${name}`);
|
|
256
201
|
continue;
|
|
257
202
|
}
|
|
258
|
-
|
|
259
203
|
if (schema.type === "string") {
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
throw new Error(`Elicitation field ${name} is shorter than minimum length ${
|
|
204
|
+
const stringValue = String(value);
|
|
205
|
+
const limits = schema as typeof schema & { minLength?: number; maxLength?: number };
|
|
206
|
+
if (limits.minLength !== undefined && stringValue.length < limits.minLength) {
|
|
207
|
+
throw new Error(`Elicitation field ${name} is shorter than minimum length ${limits.minLength}`);
|
|
264
208
|
}
|
|
265
|
-
if (
|
|
266
|
-
throw new Error(`Elicitation field ${name} is longer than maximum length ${
|
|
209
|
+
if (limits.maxLength !== undefined && stringValue.length > limits.maxLength) {
|
|
210
|
+
throw new Error(`Elicitation field ${name} is longer than maximum length ${limits.maxLength}`);
|
|
267
211
|
}
|
|
268
|
-
if ("enum" in schema &&
|
|
212
|
+
if ("enum" in schema && !schema.enum.includes(stringValue)) {
|
|
269
213
|
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
270
214
|
}
|
|
271
|
-
if ("oneOf" in schema &&
|
|
215
|
+
if ("oneOf" in schema && !schema.oneOf.some(option => option.const === stringValue)) {
|
|
272
216
|
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
273
217
|
}
|
|
274
|
-
output[name] =
|
|
218
|
+
output[name] = stringValue;
|
|
275
219
|
continue;
|
|
276
220
|
}
|
|
277
|
-
|
|
278
221
|
if (schema.type === "number" || schema.type === "integer") {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
222
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
223
|
+
throw new Error(`Elicitation field ${name} must be a number`);
|
|
224
|
+
}
|
|
225
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
226
|
+
if (!Number.isFinite(numberValue)) throw new Error(`Elicitation field ${name} must be a number`);
|
|
227
|
+
if (schema.type === "integer" && !Number.isInteger(numberValue)) {
|
|
228
|
+
throw new Error(`Elicitation field ${name} must be an integer`);
|
|
229
|
+
}
|
|
230
|
+
if (schema.minimum !== undefined && numberValue < schema.minimum) {
|
|
283
231
|
throw new Error(`Elicitation field ${name} is below minimum ${schema.minimum}`);
|
|
284
232
|
}
|
|
285
|
-
if (schema.maximum !== undefined &&
|
|
233
|
+
if (schema.maximum !== undefined && numberValue > schema.maximum) {
|
|
286
234
|
throw new Error(`Elicitation field ${name} is above maximum ${schema.maximum}`);
|
|
287
235
|
}
|
|
288
|
-
output[name] =
|
|
236
|
+
output[name] = numberValue;
|
|
289
237
|
continue;
|
|
290
238
|
}
|
|
291
|
-
|
|
292
239
|
if (schema.type === "boolean") {
|
|
293
|
-
output[name] = typeof
|
|
240
|
+
output[name] = typeof value === "boolean" ? value : value === "true";
|
|
294
241
|
continue;
|
|
295
242
|
}
|
|
296
|
-
|
|
297
243
|
if (schema.type === "array") {
|
|
298
|
-
if (!Array.isArray(
|
|
299
|
-
const allowed = new Set(extractMultiSelectOptions(schema).map(
|
|
300
|
-
const
|
|
301
|
-
if (schema.minItems !== undefined &&
|
|
244
|
+
if (!Array.isArray(value)) throw new Error(`Elicitation field ${name} must be a list`);
|
|
245
|
+
const allowed = new Set(extractMultiSelectOptions(schema).map(option => option.value));
|
|
246
|
+
const arrayValue = value.map(String);
|
|
247
|
+
if (schema.minItems !== undefined && arrayValue.length < schema.minItems) {
|
|
302
248
|
throw new Error(`Elicitation field ${name} has fewer than ${schema.minItems} selections`);
|
|
303
249
|
}
|
|
304
|
-
if (schema.maxItems !== undefined &&
|
|
250
|
+
if (schema.maxItems !== undefined && arrayValue.length > schema.maxItems) {
|
|
305
251
|
throw new Error(`Elicitation field ${name} has more than ${schema.maxItems} selections`);
|
|
306
252
|
}
|
|
307
|
-
|
|
308
|
-
|
|
253
|
+
if (arrayValue.some(item => !allowed.has(item))) {
|
|
254
|
+
throw new Error(`Elicitation field ${name} contains an invalid selection`);
|
|
309
255
|
}
|
|
310
|
-
output[name] =
|
|
256
|
+
output[name] = arrayValue;
|
|
311
257
|
}
|
|
312
258
|
}
|
|
313
|
-
|
|
259
|
+
const validation = new AjvJsonSchemaValidator()
|
|
260
|
+
.getValidator(params.requestedSchema as JsonSchemaType)(output);
|
|
261
|
+
if (!validation.valid) {
|
|
262
|
+
throw new Error(`Invalid elicitation response: ${validation.errorMessage}`);
|
|
263
|
+
}
|
|
314
264
|
return output;
|
|
315
265
|
}
|
|
316
266
|
|
|
317
|
-
function
|
|
267
|
+
function formatChoice(value: string, title?: string): string {
|
|
268
|
+
return title && title !== value ? `${title} (${value})` : value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function uniqueLabels(labels: string[]): string[] {
|
|
272
|
+
const used = new Set<string>();
|
|
273
|
+
return labels.map(label => {
|
|
274
|
+
let unique = label;
|
|
275
|
+
while (used.has(unique)) unique += "…";
|
|
276
|
+
used.add(unique);
|
|
277
|
+
return unique;
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function uniqueAction(label: string, choices: string[]): string {
|
|
282
|
+
let unique = label;
|
|
283
|
+
while (choices.includes(unique)) unique += "…";
|
|
284
|
+
return unique;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function extractMultiSelectOptions(schema: Extract<FormProperty, { type: "array" }>): Array<{ value: string; display: string }> {
|
|
318
288
|
const items = schema.items as { enum?: string[]; anyOf?: Array<{ const: string; title: string }> };
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
return (items.enum ?? []).map((value) => ({ value }));
|
|
289
|
+
return items.anyOf
|
|
290
|
+
? items.anyOf.map(option => ({ value: option.const, display: formatChoice(option.const, option.title) }))
|
|
291
|
+
: (items.enum ?? []).map(value => ({ value, display: value }));
|
|
323
292
|
}
|
|
324
293
|
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
294
|
+
function formatReview(
|
|
295
|
+
serverName: string,
|
|
296
|
+
properties: Array<[string, FormProperty]>,
|
|
297
|
+
content: Record<string, string | number | boolean | string[]>,
|
|
298
|
+
): string {
|
|
299
|
+
const rows = properties.map(([name, schema]) =>
|
|
300
|
+
`${schema.title ?? humanizeName(name)}: ${content[name] === undefined ? "(omitted)" : String(content[name])}`);
|
|
301
|
+
return [`Review input for ${serverName}`, "", ...rows].join("\n");
|
|
330
302
|
}
|
|
331
303
|
|
|
332
|
-
function
|
|
333
|
-
|
|
304
|
+
export async function handleUrlElicitation(
|
|
305
|
+
options: ElicitationHandlerOptions,
|
|
306
|
+
params: ElicitRequestURLParams,
|
|
307
|
+
): Promise<ElicitResult> {
|
|
308
|
+
if (!options.allowUrl) throw new McpError(ErrorCode.InvalidParams, "URL elicitation is not supported");
|
|
309
|
+
|
|
310
|
+
let parsed: URL;
|
|
311
|
+
try {
|
|
312
|
+
parsed = new URL(params.url);
|
|
313
|
+
} catch {
|
|
314
|
+
throw new McpError(ErrorCode.InvalidParams, "URL elicitation supplied an invalid URL");
|
|
315
|
+
}
|
|
334
316
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
335
|
-
throw new
|
|
317
|
+
throw new McpError(ErrorCode.InvalidParams, "URL elicitation only supports HTTP and HTTPS URLs");
|
|
336
318
|
}
|
|
337
|
-
return parsed;
|
|
338
|
-
}
|
|
339
319
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
320
|
+
const decision = await options.ui.select([
|
|
321
|
+
"MCP Browser Request",
|
|
322
|
+
`Server: ${options.serverName}`,
|
|
323
|
+
"",
|
|
324
|
+
params.message,
|
|
325
|
+
"",
|
|
326
|
+
`Host: ${parsed.host}`,
|
|
327
|
+
`Full URL: ${params.url}`,
|
|
328
|
+
"",
|
|
329
|
+
"Open this URL in your browser?",
|
|
330
|
+
].join("\n"), ["Open", "Decline"]);
|
|
331
|
+
if (decision === undefined) return { action: "cancel" };
|
|
332
|
+
if (decision === "Decline") return { action: "decline" };
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await open(params.url);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
options.ui.notify(`Could not open MCP elicitation URL: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
338
|
+
return { action: "cancel" };
|
|
344
339
|
}
|
|
345
|
-
|
|
340
|
+
options.onUrlAccepted?.(params.elicitationId);
|
|
341
|
+
options.ui.notify("Opened browser for MCP elicitation.", "info");
|
|
342
|
+
return { action: "accept" };
|
|
346
343
|
}
|
|
347
344
|
|
|
348
|
-
function
|
|
349
|
-
|
|
350
|
-
if (value[key] === undefined) delete value[key];
|
|
351
|
-
}
|
|
352
|
-
return value;
|
|
345
|
+
function humanizeName(name: string): string {
|
|
346
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, char => char.toUpperCase());
|
|
353
347
|
}
|
package/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { loadMcpConfig } from "./config.ts";
|
|
|
6
6
|
import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.ts";
|
|
7
7
|
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.ts";
|
|
8
8
|
import { loadMetadataCache } from "./metadata-cache.ts";
|
|
9
|
-
import { executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
|
|
9
|
+
import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
|
|
10
10
|
import { getConfigPathFromArgv, truncateAtWord } from "./utils.ts";
|
|
11
11
|
import { initializeOAuth, shutdownOAuth } from "./mcp-auth-flow.ts";
|
|
12
12
|
import { createMcpDirectToolCallRenderer, renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.ts";
|
|
@@ -263,7 +263,7 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
263
263
|
regex: Type.Optional(Type.Boolean({ description: "Treat search as regex (default: substring match)" })),
|
|
264
264
|
includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
|
|
265
265
|
server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
|
|
266
|
-
action: Type.Optional(Type.String({ description: "Action: 'ui-messages'
|
|
266
|
+
action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
|
|
267
267
|
}),
|
|
268
268
|
renderResult: renderMcpToolResult,
|
|
269
269
|
async execute(_toolCallId, params: {
|
|
@@ -314,6 +314,31 @@ export default function mcpAdapter(pi: ExtensionAPI) {
|
|
|
314
314
|
if (params.action === "ui-messages") {
|
|
315
315
|
return executeUiMessages(state);
|
|
316
316
|
}
|
|
317
|
+
if (params.action === "auth-start") {
|
|
318
|
+
if (!params.server) {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text" as const, text: "auth-start requires `server`. Example: mcp({ action: \"auth-start\", server: \"linear-server\" })" }],
|
|
321
|
+
details: { mode: "auth-start", error: "missing_server" },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
return executeAuthStart(state, params.server);
|
|
325
|
+
}
|
|
326
|
+
if (params.action === "auth-complete") {
|
|
327
|
+
if (!params.server) {
|
|
328
|
+
return {
|
|
329
|
+
content: [{ type: "text" as const, text: "auth-complete requires `server`." }],
|
|
330
|
+
details: { mode: "auth-complete", error: "missing_server" },
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const input = parsedArgs?.redirectUrl ?? parsedArgs?.code ?? parsedArgs?.input;
|
|
334
|
+
if (typeof input !== "string" || input.trim().length === 0) {
|
|
335
|
+
return {
|
|
336
|
+
content: [{ type: "text" as const, text: "auth-complete requires args with `redirectUrl`, `code`, or `input`." }],
|
|
337
|
+
details: { mode: "auth-complete", error: "missing_input" },
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return executeAuthComplete(state, params.server, input);
|
|
341
|
+
}
|
|
317
342
|
if (params.tool) {
|
|
318
343
|
return executeCall(state, params.tool, parsedArgs, params.server, getPiTools);
|
|
319
344
|
}
|