@duckmind/dm-windows-x64 0.63.4 → 0.63.9
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +236 -13
- package/extensions/dm-context/package.json +1 -1
- package/extensions/dm-context/skills/context-management/SKILL.md +173 -223
- package/extensions/dm-context/skills/context-management/references/development-and-troubleshooting.md +75 -0
- package/extensions/dm-context/skills/context-management/references/interleaved-async-work.md +143 -0
- package/extensions/dm-context/skills/context-management/references/planning-and-execution.md +81 -0
- package/extensions/dm-context/skills/context-management/references/repeated-items-and-batch-work.md +69 -0
- package/extensions/dm-context/skills/context-management/references/retry-branch-and-pivot.md +80 -0
- package/extensions/dm-context/skills/context-management/references/search-research-and-reading.md +103 -0
- package/extensions/dm-context/skills/context-management/references/task-switching-and-cleanup.md +73 -0
- package/extensions/dm-context/src/context.js +3 -2
- package/extensions/dm-context/src/index.js +168 -84
- package/extensions/dm-mcp-adapter/OAUTH.md +3 -0
- package/extensions/dm-mcp-adapter/THIRD_PARTY_NOTICES.md +4 -0
- package/extensions/dm-mcp-adapter/abort.js +35 -0
- package/extensions/dm-mcp-adapter/agent-dir.js +45 -0
- package/extensions/dm-mcp-adapter/agent-plugin-loader.js +349 -0
- package/extensions/dm-mcp-adapter/app-bridge.bundle.js +5843 -0
- package/extensions/dm-mcp-adapter/cli.js +354 -0
- package/extensions/dm-mcp-adapter/commands.js +619 -0
- package/extensions/dm-mcp-adapter/config.js +1060 -0
- package/extensions/dm-mcp-adapter/consent-manager.js +60 -0
- package/extensions/dm-mcp-adapter/direct-tools.js +544 -0
- package/extensions/dm-mcp-adapter/dist/agent-dir.js +45 -0
- package/extensions/dm-mcp-adapter/dist/agent-plugin-loader.js +349 -0
- package/extensions/dm-mcp-adapter/dist/config.js +1060 -0
- package/extensions/dm-mcp-adapter/dist/mcp-bearer-store.js +311 -0
- package/extensions/dm-mcp-adapter/dist/metadata-cache.js +285 -0
- package/extensions/dm-mcp-adapter/dist/package-mcp-loader.js +144 -0
- package/extensions/dm-mcp-adapter/dist/resource-tools.js +7 -0
- package/extensions/dm-mcp-adapter/dist/types.js +247 -0
- package/extensions/dm-mcp-adapter/dist/ui-app-bridge-helpers.js +36 -0
- package/extensions/dm-mcp-adapter/dist/ui-stream-types.js +58 -0
- package/extensions/dm-mcp-adapter/dist/ui-tool-visibility.js +26 -0
- package/extensions/dm-mcp-adapter/dist/utils.js +307 -0
- package/extensions/dm-mcp-adapter/elicitation-handler.js +306 -0
- package/extensions/dm-mcp-adapter/error-signal.js +16 -0
- package/extensions/dm-mcp-adapter/errors.js +225 -0
- package/extensions/dm-mcp-adapter/failure-backoff.js +19 -0
- package/extensions/dm-mcp-adapter/glimpse-ui.js +61 -0
- package/extensions/dm-mcp-adapter/host-html-template.js +464 -0
- package/extensions/dm-mcp-adapter/index.js +1079 -0
- package/extensions/dm-mcp-adapter/init.js +624 -0
- package/extensions/dm-mcp-adapter/json-schema-validator.js +53 -0
- package/extensions/dm-mcp-adapter/lifecycle.js +385 -0
- package/extensions/dm-mcp-adapter/logger.js +115 -0
- package/extensions/dm-mcp-adapter/mcp-auth-flow.js +824 -0
- package/extensions/dm-mcp-adapter/mcp-auth.js +732 -0
- package/extensions/dm-mcp-adapter/mcp-bearer-store.js +311 -0
- package/extensions/dm-mcp-adapter/mcp-callback-server.js +388 -0
- package/extensions/dm-mcp-adapter/mcp-code.js +291 -0
- package/extensions/dm-mcp-adapter/mcp-keyring-helper.cjs +88 -0
- package/extensions/dm-mcp-adapter/mcp-oauth-provider.js +490 -0
- package/extensions/dm-mcp-adapter/mcp-output-guard.js +386 -0
- package/extensions/dm-mcp-adapter/mcp-panel.js +951 -0
- package/extensions/dm-mcp-adapter/mcp-probe.js +151 -0
- package/extensions/dm-mcp-adapter/mcp-references.js +263 -0
- package/extensions/dm-mcp-adapter/mcp-script-worker.mjs +126 -0
- package/extensions/dm-mcp-adapter/mcp-setup-panel.js +578 -0
- package/extensions/dm-mcp-adapter/mcp-status.js +80 -0
- package/extensions/dm-mcp-adapter/mcp-trace.js +215 -0
- package/extensions/dm-mcp-adapter/metadata-cache.js +292 -0
- package/extensions/dm-mcp-adapter/namespace-tools.js +155 -0
- package/extensions/dm-mcp-adapter/npx-resolver.js +465 -0
- package/extensions/dm-mcp-adapter/oauth-handler.js +17 -0
- package/extensions/dm-mcp-adapter/oauth.js +17 -0
- package/extensions/dm-mcp-adapter/onboarding-state.js +62 -0
- package/extensions/dm-mcp-adapter/package-mcp-loader.js +144 -0
- package/extensions/dm-mcp-adapter/package.json +68 -0
- package/extensions/dm-mcp-adapter/panel-keys.js +26 -0
- package/extensions/dm-mcp-adapter/prompts.js +258 -0
- package/extensions/dm-mcp-adapter/proxy-modes.js +1336 -0
- package/extensions/dm-mcp-adapter/request-headers-command.js +293 -0
- package/extensions/dm-mcp-adapter/resource-tools.js +7 -0
- package/extensions/dm-mcp-adapter/runtime-owner.js +79 -0
- package/extensions/dm-mcp-adapter/sampling-handler.js +222 -0
- package/extensions/dm-mcp-adapter/sandbox-proxy-template.js +190 -0
- package/extensions/dm-mcp-adapter/search-ranking.js +198 -0
- package/extensions/dm-mcp-adapter/server-manager.js +1258 -0
- package/extensions/dm-mcp-adapter/session-recovery.js +82 -0
- package/extensions/dm-mcp-adapter/skills/mcp-scripting/SKILL.md +39 -0
- package/extensions/dm-mcp-adapter/state.js +0 -0
- package/extensions/dm-mcp-adapter/test-runner.mjs +18 -0
- package/extensions/dm-mcp-adapter/tool-approval.js +151 -0
- package/extensions/dm-mcp-adapter/tool-metadata.js +247 -0
- package/extensions/dm-mcp-adapter/tool-registrar.js +212 -0
- package/extensions/dm-mcp-adapter/tool-result-renderer.js +355 -0
- package/extensions/dm-mcp-adapter/ts-shape.js +157 -0
- package/extensions/dm-mcp-adapter/types.js +264 -0
- package/extensions/dm-mcp-adapter/ui-app-bridge-helpers.js +36 -0
- package/extensions/dm-mcp-adapter/ui-resource-handler.js +209 -0
- package/extensions/dm-mcp-adapter/ui-server.js +815 -0
- package/extensions/dm-mcp-adapter/ui-session.js +455 -0
- package/extensions/dm-mcp-adapter/ui-stream-types.js +58 -0
- package/extensions/dm-mcp-adapter/ui-tool-visibility.js +26 -0
- package/extensions/dm-mcp-adapter/unix-socket-transport.js +83 -0
- package/extensions/dm-mcp-adapter/utils.js +307 -0
- package/extensions/dm-skills-manager/THIRD_PARTY_NOTICES.md +27 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/components.js +265 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/constants.js +31 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation-fallback.js +20 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/creation.js +145 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dialog.js +738 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/dm-ai-compat.js +15 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/format.js +125 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/glyphs.js +142 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/layout.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/paths.js +76 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/registry.js +95 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/settings.js +93 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/startup.js +32 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/toggle.js +54 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/types.js +14 -0
- package/extensions/dm-skills-manager/extensions/skills-manager/ui.js +121 -0
- package/extensions/dm-skills-manager/extensions/skills-manager.js +111 -0
- package/extensions/dm-skills-manager/package.json +121 -0
- package/package.json +13 -1
- package/extensions/dm-localllm-provider/detect.test.js +0 -1051
- package/extensions/dm-localllm-provider/index.test.js +0 -149
- package/extensions/dm-localllm-provider/keychain.test.js +0 -61
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { homedir, platform } from "node:os";
|
|
3
|
+
import { extname, isAbsolute, join } from "node:path";
|
|
4
|
+
async function execOpen(pi, target, browser, signal) {
|
|
5
|
+
const os = platform();
|
|
6
|
+
if (os === "darwin") {
|
|
7
|
+
if (browser) {
|
|
8
|
+
return isAbsolute(browser) && extname(browser).toLowerCase() !== ".app" ? pi.exec(browser, [target], signal ? { signal } : {}) : pi.exec("open", ["-a", browser, target], signal ? { signal } : {});
|
|
9
|
+
}
|
|
10
|
+
return pi.exec("open", [target], signal ? { signal } : {});
|
|
11
|
+
}
|
|
12
|
+
if (os === "win32") {
|
|
13
|
+
return browser ? pi.exec("cmd", ["/c", "start", "", browser, target], signal ? { signal } : {}) : pi.exec("cmd", ["/c", "start", "", target], signal ? { signal } : {});
|
|
14
|
+
}
|
|
15
|
+
return browser ? pi.exec(browser, [target], signal ? { signal } : {}) : pi.exec("xdg-open", [target], signal ? { signal } : {});
|
|
16
|
+
}
|
|
17
|
+
export async function openUrl(pi, url, browser, signal) {
|
|
18
|
+
const result = await execOpen(pi, url, browser, signal);
|
|
19
|
+
if (result.code !== 0) {
|
|
20
|
+
throw new Error(result.stderr || `Failed to open browser (exit code ${result.code})`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function openPath(pi, targetPath) {
|
|
24
|
+
const result = await execOpen(pi, targetPath);
|
|
25
|
+
if (result.code !== 0) {
|
|
26
|
+
throw new Error(result.stderr || `Failed to open path (exit code ${result.code})`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function parallelLimit(items, limit, fn) {
|
|
30
|
+
const results = [];
|
|
31
|
+
const iterator = items.entries();
|
|
32
|
+
async function worker() {
|
|
33
|
+
while (true) {
|
|
34
|
+
const next = iterator.next();
|
|
35
|
+
if (next.done)
|
|
36
|
+
return;
|
|
37
|
+
const [index, item] = next.value;
|
|
38
|
+
results[index] = await fn(item);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const workers = Array(Math.min(limit, items.length)).fill(null).map(() => worker());
|
|
42
|
+
await Promise.all(workers);
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
export function getConfigPathFromArgv() {
|
|
46
|
+
const idx = process.argv.indexOf("--mcp-config");
|
|
47
|
+
if (idx >= 0 && idx + 1 < process.argv.length) {
|
|
48
|
+
return process.argv[idx + 1];
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
export function interpolateEnvVars(value, environment = process.env) {
|
|
53
|
+
return value.replace(/\$\{(\w+)\}/g, (_, name) => environment[name] ?? "").replace(/\$env:(\w+)/g, (_, name) => environment[name] ?? "").replace(/\{env:(\w+)\}/g, (_, name) => environment[name] ?? "");
|
|
54
|
+
}
|
|
55
|
+
function getMissingEnvVars(value, environment) {
|
|
56
|
+
const missing = new Set;
|
|
57
|
+
for (const match of value.matchAll(/\$\{(\w+)\}|\$env:(\w+)|\{env:(\w+)\}/g)) {
|
|
58
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
59
|
+
if (name && environment[name] === undefined) {
|
|
60
|
+
missing.add(name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [...missing];
|
|
64
|
+
}
|
|
65
|
+
export function toStringRecord(value) {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
67
|
+
return;
|
|
68
|
+
const result = {};
|
|
69
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
70
|
+
if (typeof entry === "string")
|
|
71
|
+
result[key] = entry;
|
|
72
|
+
}
|
|
73
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
74
|
+
}
|
|
75
|
+
function interpolateSecretExpression(value, environment) {
|
|
76
|
+
if (value.startsWith("!!"))
|
|
77
|
+
return interpolateEnvVars(value.slice(1), environment);
|
|
78
|
+
return value.startsWith("!") ? value : interpolateEnvVars(value, environment);
|
|
79
|
+
}
|
|
80
|
+
export function interpolateEnvRecord(values, environment = process.env) {
|
|
81
|
+
if (!values)
|
|
82
|
+
return;
|
|
83
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
84
|
+
key,
|
|
85
|
+
interpolateSecretExpression(value, environment)
|
|
86
|
+
]));
|
|
87
|
+
}
|
|
88
|
+
const COMMAND_SECRET_TIMEOUT_MS = 1e4;
|
|
89
|
+
const COMMAND_SECRET_MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
90
|
+
export function resolveCommandSecret(value, context) {
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return;
|
|
93
|
+
if (value.startsWith("!!"))
|
|
94
|
+
return interpolateEnvVars(value.slice(1));
|
|
95
|
+
if (!value.startsWith("!"))
|
|
96
|
+
return interpolateEnvVars(value);
|
|
97
|
+
const result = spawnSync(value.slice(1), {
|
|
98
|
+
shell: true,
|
|
99
|
+
encoding: "utf8",
|
|
100
|
+
timeout: COMMAND_SECRET_TIMEOUT_MS,
|
|
101
|
+
maxBuffer: COMMAND_SECRET_MAX_OUTPUT_BYTES,
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
windowsHide: true
|
|
104
|
+
});
|
|
105
|
+
if (result.error) {
|
|
106
|
+
const code = result.error.code;
|
|
107
|
+
const reason = code === "ETIMEDOUT" ? "command timed out after 10 seconds" : code === "ENOBUFS" ? "command output exceeded 1 MiB" : "command failed to start";
|
|
108
|
+
throw new Error(`Failed to resolve ${context}: ${reason}`);
|
|
109
|
+
}
|
|
110
|
+
if (result.status !== 0) {
|
|
111
|
+
throw new Error(`Failed to resolve ${context}: command exited with code ${result.status ?? "unknown"}`);
|
|
112
|
+
}
|
|
113
|
+
const resolved = result.stdout.trim();
|
|
114
|
+
if (!resolved)
|
|
115
|
+
throw new Error(`Failed to resolve ${context}: command returned empty output`);
|
|
116
|
+
return resolved;
|
|
117
|
+
}
|
|
118
|
+
export function resolveCommandSecretsRecord(values, context) {
|
|
119
|
+
if (!values)
|
|
120
|
+
return;
|
|
121
|
+
return Object.fromEntries(Object.entries(values).map(([key, value]) => [
|
|
122
|
+
key,
|
|
123
|
+
resolveCommandSecret(value, context(key))
|
|
124
|
+
]));
|
|
125
|
+
}
|
|
126
|
+
export function resolveServerUrl(definition, environment = process.env) {
|
|
127
|
+
if (definition.url == null)
|
|
128
|
+
return;
|
|
129
|
+
if (typeof definition.url !== "string") {
|
|
130
|
+
throw new Error("MCP server URL must be a string");
|
|
131
|
+
}
|
|
132
|
+
const missing = getMissingEnvVars(definition.url, environment);
|
|
133
|
+
if (missing.length > 0) {
|
|
134
|
+
throw new Error(`Missing environment variable${missing.length === 1 ? "" : "s"} in MCP server URL: ${missing.join(", ")}`);
|
|
135
|
+
}
|
|
136
|
+
const resolved = interpolateEnvVars(definition.url, environment);
|
|
137
|
+
try {
|
|
138
|
+
new URL(resolved);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
throw new Error(`Invalid MCP server URL after environment interpolation: ${resolved}`, { cause: error });
|
|
141
|
+
}
|
|
142
|
+
return resolved;
|
|
143
|
+
}
|
|
144
|
+
export function resolveConfigPath(value, environment = process.env) {
|
|
145
|
+
if (value === undefined)
|
|
146
|
+
return;
|
|
147
|
+
const resolved = interpolateEnvVars(value, environment);
|
|
148
|
+
if (resolved === "~")
|
|
149
|
+
return homedir();
|
|
150
|
+
if (resolved.startsWith("~/") || resolved.startsWith("~\\")) {
|
|
151
|
+
return join(homedir(), resolved.slice(2));
|
|
152
|
+
}
|
|
153
|
+
return resolved;
|
|
154
|
+
}
|
|
155
|
+
export function resolveBearerToken(definition, environment = process.env) {
|
|
156
|
+
if (definition.bearerToken !== undefined) {
|
|
157
|
+
return interpolateSecretExpression(definition.bearerToken, environment);
|
|
158
|
+
}
|
|
159
|
+
return definition.bearerTokenEnv ? environment[definition.bearerTokenEnv] : undefined;
|
|
160
|
+
}
|
|
161
|
+
export function stripOscSequences(text) {
|
|
162
|
+
let result = "";
|
|
163
|
+
let index = 0;
|
|
164
|
+
while (index < text.length) {
|
|
165
|
+
const isEscOsc = text.charCodeAt(index) === 27 && text[index + 1] === "]";
|
|
166
|
+
const isC1Osc = text.charCodeAt(index) === 157;
|
|
167
|
+
if (!isEscOsc && !isC1Osc) {
|
|
168
|
+
result += text[index++];
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
index += isEscOsc ? 2 : 1;
|
|
172
|
+
while (index < text.length) {
|
|
173
|
+
const code = text.charCodeAt(index++);
|
|
174
|
+
if (code === 7 || code === 156)
|
|
175
|
+
break;
|
|
176
|
+
if (code === 27 && text[index] === "\\") {
|
|
177
|
+
index++;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
export function sanitizeTerminalText(text) {
|
|
185
|
+
return stripOscSequences(text).replace(/(?:\x1b\[[0-?]*[ -/]*[@-~]|\x1b[@-Z\\-_])/g, "").replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ").replace(/\s+/g, " ").trim();
|
|
186
|
+
}
|
|
187
|
+
export function formatTerminalError(error) {
|
|
188
|
+
const messages = [];
|
|
189
|
+
const seen = new Set;
|
|
190
|
+
const collect = (value) => {
|
|
191
|
+
if (seen.has(value))
|
|
192
|
+
return;
|
|
193
|
+
if (typeof value === "object" && value !== null || typeof value === "function")
|
|
194
|
+
seen.add(value);
|
|
195
|
+
if (value instanceof AggregateError) {
|
|
196
|
+
const countBefore = messages.length;
|
|
197
|
+
for (const nested of value.errors)
|
|
198
|
+
collect(nested);
|
|
199
|
+
if (value.cause !== undefined)
|
|
200
|
+
collect(value.cause);
|
|
201
|
+
if (messages.length === countBefore && value.message)
|
|
202
|
+
messages.push(value.message);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (value instanceof Error) {
|
|
206
|
+
if (value.message)
|
|
207
|
+
messages.push(value.message);
|
|
208
|
+
if (value.cause !== undefined)
|
|
209
|
+
collect(value.cause);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
messages.push(String(value));
|
|
213
|
+
};
|
|
214
|
+
collect(error);
|
|
215
|
+
return sanitizeTerminalText([...new Set(messages)].join(": "));
|
|
216
|
+
}
|
|
217
|
+
export function truncateAtWord(text, target) {
|
|
218
|
+
if (!text || text.length <= target)
|
|
219
|
+
return text;
|
|
220
|
+
const truncated = text.slice(0, target);
|
|
221
|
+
const lastSpace = truncated.lastIndexOf(" ");
|
|
222
|
+
if (lastSpace > target * 0.6) {
|
|
223
|
+
return truncated.slice(0, lastSpace) + "...";
|
|
224
|
+
}
|
|
225
|
+
return truncated + "...";
|
|
226
|
+
}
|
|
227
|
+
export function normalizeDirectToolInputSchema(schema) {
|
|
228
|
+
const inputSchema = schema && typeof schema === "object" && !Array.isArray(schema) ? schema : { type: "object", properties: {} };
|
|
229
|
+
const { $schema, additionalProperties, ...normalized } = inputSchema;
|
|
230
|
+
return normalized;
|
|
231
|
+
}
|
|
232
|
+
export function normalizeToolArguments(value, context = "tool arguments") {
|
|
233
|
+
if (value === undefined || value === null || value === "")
|
|
234
|
+
return {};
|
|
235
|
+
if (typeof value === "string") {
|
|
236
|
+
const trimmed = value.trim();
|
|
237
|
+
if (trimmed === "")
|
|
238
|
+
return {};
|
|
239
|
+
let parsed;
|
|
240
|
+
try {
|
|
241
|
+
parsed = JSON.parse(trimmed);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
throw new Error(`${context}: invalid args JSON (${error instanceof SyntaxError ? error.message : String(error)}); ` + `pass args as a JSON object, or as a valid JSON string encoding one`, { cause: error });
|
|
244
|
+
}
|
|
245
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
246
|
+
throw new Error(`${context}: expected a JSON object, got ${Array.isArray(parsed) ? "array" : typeof parsed}`);
|
|
247
|
+
}
|
|
248
|
+
return parsed;
|
|
249
|
+
}
|
|
250
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
251
|
+
throw new Error(`${context}: expected a JSON object, got ${Array.isArray(value) ? "array" : typeof value}`);
|
|
252
|
+
}
|
|
253
|
+
assertJsonSerializable(value, context);
|
|
254
|
+
try {
|
|
255
|
+
return JSON.parse(JSON.stringify(value));
|
|
256
|
+
} catch (error) {
|
|
257
|
+
throw new Error(`${context}: value is not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function assertJsonSerializable(value, context, path = "") {
|
|
261
|
+
if (value === null)
|
|
262
|
+
return;
|
|
263
|
+
if (typeof value === "string" || typeof value === "boolean")
|
|
264
|
+
return;
|
|
265
|
+
if (typeof value === "number") {
|
|
266
|
+
if (!Number.isFinite(value))
|
|
267
|
+
throw new Error(`${context}: value at ${path || "root"} is not a finite number`);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (Array.isArray(value)) {
|
|
271
|
+
for (const [index, item] of value.entries())
|
|
272
|
+
assertJsonSerializable(item, context, `${path}[${index}]`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (typeof value === "object") {
|
|
276
|
+
if (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) {
|
|
277
|
+
throw new Error(`${context}: value at ${path || "root"} is not a plain JSON object`);
|
|
278
|
+
}
|
|
279
|
+
if (Object.getOwnPropertySymbols(value).length > 0) {
|
|
280
|
+
throw new Error(`${context}: value at ${path || "root"} has symbol keys`);
|
|
281
|
+
}
|
|
282
|
+
for (const [key, item] of Object.entries(value)) {
|
|
283
|
+
assertJsonSerializable(item, context, path ? `${path}.${key}` : key);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
throw new Error(`${context}: value at ${path || "root"} is not JSON-serializable`);
|
|
288
|
+
}
|
|
289
|
+
export function formatAuthRequiredMessage(config, serverName, defaultMessage) {
|
|
290
|
+
const template = config.settings?.authRequiredMessage;
|
|
291
|
+
return template ? template.replaceAll("${server}", serverName) : defaultMessage;
|
|
292
|
+
}
|
|
293
|
+
export function formatMcpStatus(config, message) {
|
|
294
|
+
if (config.settings?.mcpFooterStatus === "off")
|
|
295
|
+
return;
|
|
296
|
+
return `${config.settings?.showStatusIcon === false ? "MCP: " : "\uD83D\uDD0C MCP: "}${message}`;
|
|
297
|
+
}
|
|
298
|
+
export function extractToolUiStreamMode(toolMeta) {
|
|
299
|
+
const uiMeta = toolMeta?.ui;
|
|
300
|
+
if (!uiMeta || typeof uiMeta !== "object")
|
|
301
|
+
return;
|
|
302
|
+
const streamMode = uiMeta["dm-mcp-adapter.streamMode"];
|
|
303
|
+
if (streamMode === "eager" || streamMode === "stream-first") {
|
|
304
|
+
return streamMode;
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProtocolError,
|
|
3
|
+
ProtocolErrorCode
|
|
4
|
+
} from "@modelcontextprotocol/client";
|
|
5
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
|
|
6
|
+
import open from "open";
|
|
7
|
+
export function registerElicitationHandler(client, options) {
|
|
8
|
+
client.setRequestHandler("elicitation/create", (request) => handleElicitationRequest(options, request));
|
|
9
|
+
}
|
|
10
|
+
export async function handleElicitationRequest(options, request) {
|
|
11
|
+
return request.params.mode === "url" ? handleUrlElicitation(options, request.params) : handleFormElicitation(options, request.params);
|
|
12
|
+
}
|
|
13
|
+
export async function handleFormElicitation(options, params) {
|
|
14
|
+
const properties = Object.entries(params.requestedSchema.properties);
|
|
15
|
+
const decision = await options.ui.select(`MCP Input Request
|
|
16
|
+
Server: ${options.serverName}
|
|
17
|
+
|
|
18
|
+
${params.message}`, ["Continue", "Decline"]);
|
|
19
|
+
if (decision === undefined)
|
|
20
|
+
return { action: "cancel" };
|
|
21
|
+
if (decision === "Decline")
|
|
22
|
+
return { action: "decline" };
|
|
23
|
+
if (properties.length === 0)
|
|
24
|
+
return { action: "accept", content: {} };
|
|
25
|
+
const values = {};
|
|
26
|
+
for (const [name, schema] of properties) {
|
|
27
|
+
const value = await collectValidField(options.ui, params, name, schema);
|
|
28
|
+
if (value.status === "cancelled")
|
|
29
|
+
return { action: "cancel" };
|
|
30
|
+
values[name] = value.value;
|
|
31
|
+
}
|
|
32
|
+
while (true) {
|
|
33
|
+
const content = coerceAndValidateFormValues(params, values);
|
|
34
|
+
const action = await options.ui.select(formatReview(options.serverName, properties, content), ["Submit", "Edit", "Decline"]);
|
|
35
|
+
if (action === undefined)
|
|
36
|
+
return { action: "cancel" };
|
|
37
|
+
if (action === "Decline")
|
|
38
|
+
return { action: "decline" };
|
|
39
|
+
if (action === "Submit")
|
|
40
|
+
return { action: "accept", content };
|
|
41
|
+
const labels = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)} (${name})`);
|
|
42
|
+
const selected = await options.ui.select("Choose a field to edit", labels);
|
|
43
|
+
if (selected === undefined)
|
|
44
|
+
return { action: "cancel" };
|
|
45
|
+
const property = properties[labels.indexOf(selected)];
|
|
46
|
+
if (!property)
|
|
47
|
+
continue;
|
|
48
|
+
const [name, schema] = property;
|
|
49
|
+
const value = await collectValidField(options.ui, params, name, schema, values[name]);
|
|
50
|
+
if (value.status === "cancelled")
|
|
51
|
+
return { action: "cancel" };
|
|
52
|
+
values[name] = value.value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function collectValidField(ui, params, name, schema, current) {
|
|
56
|
+
const required = params.requestedSchema.required?.includes(name) === true;
|
|
57
|
+
while (true) {
|
|
58
|
+
const result = await collectField(ui, params, name, schema, current);
|
|
59
|
+
if (result.status === "cancelled")
|
|
60
|
+
return result;
|
|
61
|
+
try {
|
|
62
|
+
coerceAndValidateFormValues({
|
|
63
|
+
...params,
|
|
64
|
+
requestedSchema: {
|
|
65
|
+
type: "object",
|
|
66
|
+
properties: { [name]: schema },
|
|
67
|
+
...required ? { required: [name] } : {}
|
|
68
|
+
}
|
|
69
|
+
}, { [name]: result.value });
|
|
70
|
+
return result;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
73
|
+
current = result.value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function collectField(ui, params, name, schema, current) {
|
|
78
|
+
const required = params.requestedSchema.required?.includes(name) === true;
|
|
79
|
+
const title = [schema.title ?? humanizeName(name), required ? "(required)" : "", schema.description].filter(Boolean).join(" ");
|
|
80
|
+
if (schema.type === "string" && (("enum" in schema) || ("oneOf" in schema))) {
|
|
81
|
+
const choices = "oneOf" in schema ? schema.oneOf.map((option) => ({ value: option.const, display: formatChoice(option.const, option.title) })) : schema.enum.map((value, index) => ({
|
|
82
|
+
value,
|
|
83
|
+
display: formatChoice(value, "enumNames" in schema ? schema.enumNames?.[index] : undefined)
|
|
84
|
+
}));
|
|
85
|
+
const displays = uniqueLabels(choices.map((choice) => choice.display));
|
|
86
|
+
const actions = [...displays];
|
|
87
|
+
const useDefault = schema.default === undefined ? undefined : uniqueAction("Use default", actions);
|
|
88
|
+
if (useDefault)
|
|
89
|
+
actions.push(useDefault);
|
|
90
|
+
const omit = required ? undefined : uniqueAction("Omit", actions);
|
|
91
|
+
if (omit)
|
|
92
|
+
actions.push(omit);
|
|
93
|
+
const action = await ui.select(title, actions);
|
|
94
|
+
if (action === undefined)
|
|
95
|
+
return { status: "cancelled" };
|
|
96
|
+
if (action === useDefault)
|
|
97
|
+
return { status: "collected", value: schema.default };
|
|
98
|
+
if (action === omit)
|
|
99
|
+
return { status: "collected", value: undefined };
|
|
100
|
+
return { status: "collected", value: choices[displays.indexOf(action)]?.value };
|
|
101
|
+
}
|
|
102
|
+
if (schema.type === "boolean") {
|
|
103
|
+
const actions = ["Yes", "No"];
|
|
104
|
+
if (schema.default !== undefined)
|
|
105
|
+
actions.push("Use default");
|
|
106
|
+
if (!required)
|
|
107
|
+
actions.push("Omit");
|
|
108
|
+
const action = await ui.select(title, actions);
|
|
109
|
+
if (action === undefined)
|
|
110
|
+
return { status: "cancelled" };
|
|
111
|
+
if (action === "Use default")
|
|
112
|
+
return { status: "collected", value: schema.default };
|
|
113
|
+
if (action === "Omit")
|
|
114
|
+
return { status: "collected", value: undefined };
|
|
115
|
+
return { status: "collected", value: action === "Yes" };
|
|
116
|
+
}
|
|
117
|
+
if (schema.type === "array") {
|
|
118
|
+
const actions = ["Choose values"];
|
|
119
|
+
if (schema.default !== undefined)
|
|
120
|
+
actions.push("Use default");
|
|
121
|
+
if (!required)
|
|
122
|
+
actions.push("Omit");
|
|
123
|
+
const action = await ui.select(title, actions);
|
|
124
|
+
if (action === undefined)
|
|
125
|
+
return { status: "cancelled" };
|
|
126
|
+
if (action === "Use default")
|
|
127
|
+
return { status: "collected", value: schema.default };
|
|
128
|
+
if (action === "Omit")
|
|
129
|
+
return { status: "collected", value: undefined };
|
|
130
|
+
const choices = extractMultiSelectOptions(schema);
|
|
131
|
+
const selected = new Set(Array.isArray(current) ? current : []);
|
|
132
|
+
while (true) {
|
|
133
|
+
const displays = uniqueLabels(choices.map((choice) => selected.has(choice.value) ? `✓ ${choice.display}` : choice.display));
|
|
134
|
+
const done = uniqueAction("Done", displays);
|
|
135
|
+
const picked = await ui.select(title, [...displays, done]);
|
|
136
|
+
if (picked === undefined)
|
|
137
|
+
return { status: "cancelled" };
|
|
138
|
+
if (picked === done)
|
|
139
|
+
return { status: "collected", value: [...selected] };
|
|
140
|
+
const choice = choices[displays.indexOf(picked)];
|
|
141
|
+
if (!choice)
|
|
142
|
+
continue;
|
|
143
|
+
if (selected.has(choice.value))
|
|
144
|
+
selected.delete(choice.value);
|
|
145
|
+
else
|
|
146
|
+
selected.add(choice.value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const actions = ["Enter value"];
|
|
150
|
+
if (schema.default !== undefined)
|
|
151
|
+
actions.push("Use default");
|
|
152
|
+
if (!required)
|
|
153
|
+
actions.push("Omit");
|
|
154
|
+
const action = await ui.select(title, actions);
|
|
155
|
+
if (action === undefined)
|
|
156
|
+
return { status: "cancelled" };
|
|
157
|
+
if (action === "Use default")
|
|
158
|
+
return { status: "collected", value: schema.default };
|
|
159
|
+
if (action === "Omit")
|
|
160
|
+
return { status: "collected", value: undefined };
|
|
161
|
+
const entered = await ui.input(title, current === undefined ? undefined : String(current));
|
|
162
|
+
return entered === undefined ? { status: "cancelled" } : { status: "collected", value: entered };
|
|
163
|
+
}
|
|
164
|
+
export function coerceAndValidateFormValues(params, values) {
|
|
165
|
+
const output = {};
|
|
166
|
+
const required = new Set(params.requestedSchema.required ?? []);
|
|
167
|
+
for (const [name, schema] of Object.entries(params.requestedSchema.properties)) {
|
|
168
|
+
const value = values[name];
|
|
169
|
+
if (value === undefined) {
|
|
170
|
+
if (required.has(name))
|
|
171
|
+
throw new Error(`Missing required elicitation field: ${name}`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (schema.type === "string") {
|
|
175
|
+
const stringValue = String(value);
|
|
176
|
+
const limits = schema;
|
|
177
|
+
if (limits.minLength !== undefined && stringValue.length < limits.minLength) {
|
|
178
|
+
throw new Error(`Elicitation field ${name} is shorter than minimum length ${limits.minLength}`);
|
|
179
|
+
}
|
|
180
|
+
if (limits.maxLength !== undefined && stringValue.length > limits.maxLength) {
|
|
181
|
+
throw new Error(`Elicitation field ${name} is longer than maximum length ${limits.maxLength}`);
|
|
182
|
+
}
|
|
183
|
+
if ("enum" in schema && !schema.enum.includes(stringValue)) {
|
|
184
|
+
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
185
|
+
}
|
|
186
|
+
if ("oneOf" in schema && !schema.oneOf.some((option) => option.const === stringValue)) {
|
|
187
|
+
throw new Error(`Elicitation field ${name} is not an allowed value`);
|
|
188
|
+
}
|
|
189
|
+
output[name] = stringValue;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
193
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
194
|
+
throw new Error(`Elicitation field ${name} must be a number`);
|
|
195
|
+
}
|
|
196
|
+
const numberValue = typeof value === "number" ? value : Number(value);
|
|
197
|
+
if (!Number.isFinite(numberValue))
|
|
198
|
+
throw new Error(`Elicitation field ${name} must be a number`);
|
|
199
|
+
if (schema.type === "integer" && !Number.isInteger(numberValue)) {
|
|
200
|
+
throw new Error(`Elicitation field ${name} must be an integer`);
|
|
201
|
+
}
|
|
202
|
+
if (schema.minimum !== undefined && numberValue < schema.minimum) {
|
|
203
|
+
throw new Error(`Elicitation field ${name} is below minimum ${schema.minimum}`);
|
|
204
|
+
}
|
|
205
|
+
if (schema.maximum !== undefined && numberValue > schema.maximum) {
|
|
206
|
+
throw new Error(`Elicitation field ${name} is above maximum ${schema.maximum}`);
|
|
207
|
+
}
|
|
208
|
+
output[name] = numberValue;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (schema.type === "boolean") {
|
|
212
|
+
output[name] = typeof value === "boolean" ? value : value === "true";
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (schema.type === "array") {
|
|
216
|
+
if (!Array.isArray(value))
|
|
217
|
+
throw new Error(`Elicitation field ${name} must be a list`);
|
|
218
|
+
const allowed = new Set(extractMultiSelectOptions(schema).map((option) => option.value));
|
|
219
|
+
const arrayValue = value.map(String);
|
|
220
|
+
if (schema.minItems !== undefined && arrayValue.length < schema.minItems) {
|
|
221
|
+
throw new Error(`Elicitation field ${name} has fewer than ${schema.minItems} selections`);
|
|
222
|
+
}
|
|
223
|
+
if (schema.maxItems !== undefined && arrayValue.length > schema.maxItems) {
|
|
224
|
+
throw new Error(`Elicitation field ${name} has more than ${schema.maxItems} selections`);
|
|
225
|
+
}
|
|
226
|
+
if (arrayValue.some((item) => !allowed.has(item))) {
|
|
227
|
+
throw new Error(`Elicitation field ${name} contains an invalid selection`);
|
|
228
|
+
}
|
|
229
|
+
output[name] = arrayValue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const validation = new AjvJsonSchemaValidator().getValidator(params.requestedSchema)(output);
|
|
233
|
+
if (!validation.valid) {
|
|
234
|
+
throw new Error(`Invalid elicitation response: ${validation.errorMessage}`);
|
|
235
|
+
}
|
|
236
|
+
return output;
|
|
237
|
+
}
|
|
238
|
+
function formatChoice(value, title) {
|
|
239
|
+
return title && title !== value ? `${title} (${value})` : value;
|
|
240
|
+
}
|
|
241
|
+
function uniqueLabels(labels) {
|
|
242
|
+
const used = new Set;
|
|
243
|
+
return labels.map((label) => {
|
|
244
|
+
let unique = label;
|
|
245
|
+
while (used.has(unique))
|
|
246
|
+
unique += "…";
|
|
247
|
+
used.add(unique);
|
|
248
|
+
return unique;
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function uniqueAction(label, choices) {
|
|
252
|
+
let unique = label;
|
|
253
|
+
while (choices.includes(unique))
|
|
254
|
+
unique += "…";
|
|
255
|
+
return unique;
|
|
256
|
+
}
|
|
257
|
+
function extractMultiSelectOptions(schema) {
|
|
258
|
+
const items = schema.items;
|
|
259
|
+
return items.anyOf ? items.anyOf.map((option) => ({ value: option.const, display: formatChoice(option.const, option.title) })) : (items.enum ?? []).map((value) => ({ value, display: value }));
|
|
260
|
+
}
|
|
261
|
+
function formatReview(serverName, properties, content) {
|
|
262
|
+
const rows = properties.map(([name, schema]) => `${schema.title ?? humanizeName(name)}: ${content[name] === undefined ? "(omitted)" : String(content[name])}`);
|
|
263
|
+
return [`Review input for ${serverName}`, "", ...rows].join(`
|
|
264
|
+
`);
|
|
265
|
+
}
|
|
266
|
+
export async function handleUrlElicitation(options, params) {
|
|
267
|
+
if (!options.allowUrl)
|
|
268
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation is not supported");
|
|
269
|
+
let parsed;
|
|
270
|
+
try {
|
|
271
|
+
parsed = new URL(params.url);
|
|
272
|
+
} catch {
|
|
273
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation supplied an invalid URL");
|
|
274
|
+
}
|
|
275
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
276
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation only supports HTTP and HTTPS URLs");
|
|
277
|
+
}
|
|
278
|
+
const decision = await options.ui.select([
|
|
279
|
+
"MCP Browser Request",
|
|
280
|
+
`Server: ${options.serverName}`,
|
|
281
|
+
"",
|
|
282
|
+
params.message,
|
|
283
|
+
"",
|
|
284
|
+
`Host: ${parsed.host}`,
|
|
285
|
+
`Full URL: ${params.url}`,
|
|
286
|
+
"",
|
|
287
|
+
"Open this URL in your browser?"
|
|
288
|
+
].join(`
|
|
289
|
+
`), ["Open", "Decline"]);
|
|
290
|
+
if (decision === undefined)
|
|
291
|
+
return { action: "cancel" };
|
|
292
|
+
if (decision === "Decline")
|
|
293
|
+
return { action: "decline" };
|
|
294
|
+
try {
|
|
295
|
+
await open(params.url);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
options.ui.notify(`Could not open MCP elicitation URL: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
298
|
+
return { action: "cancel" };
|
|
299
|
+
}
|
|
300
|
+
options.onUrlAccepted?.(params.elicitationId);
|
|
301
|
+
options.ui.notify("Opened browser for MCP elicitation.", "info");
|
|
302
|
+
return { action: "accept" };
|
|
303
|
+
}
|
|
304
|
+
function humanizeName(name) {
|
|
305
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/^./, (char) => char.toUpperCase());
|
|
306
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { INPUT_REQUIRED_NEEDS_UI } from "./errors.js";
|
|
2
|
+
export function toolErrorOverride(details) {
|
|
3
|
+
if (details && typeof details === "object" && "error" in details) {
|
|
4
|
+
const code = details.error;
|
|
5
|
+
if (code === "tool_error" || code === "call_failed" || code === INPUT_REQUIRED_NEEDS_UI) {
|
|
6
|
+
return { isError: true };
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
if (details && typeof details === "object" && details.mode === "script") {
|
|
10
|
+
const calls = details.calls;
|
|
11
|
+
if (Array.isArray(calls) && calls.some((call) => !!call && typeof call === "object" && call.error === INPUT_REQUIRED_NEEDS_UI)) {
|
|
12
|
+
return { isError: true };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|