@duckmind/dm-windows-x64 0.63.6 → 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 +174 -11
- 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/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,157 @@
|
|
|
1
|
+
const UNSUPPORTED_KEYWORDS = ["if", "then", "else", "allOf", "not", "patternProperties", "additionalProperties"];
|
|
2
|
+
export function renderTsShape(inputSchema) {
|
|
3
|
+
try {
|
|
4
|
+
if (!isSchema(inputSchema))
|
|
5
|
+
return null;
|
|
6
|
+
const definitions = new Map;
|
|
7
|
+
for (const key of ["$defs", "definitions"]) {
|
|
8
|
+
const rawDefinitions = inputSchema[key];
|
|
9
|
+
if (rawDefinitions === undefined)
|
|
10
|
+
continue;
|
|
11
|
+
if (!isSchema(rawDefinitions))
|
|
12
|
+
return null;
|
|
13
|
+
for (const [name, definition] of Object.entries(rawDefinitions)) {
|
|
14
|
+
if (!isSchema(definition))
|
|
15
|
+
return null;
|
|
16
|
+
definitions.set(`${key}/${decodePointerToken(name)}`, definition);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const aliases = new Map;
|
|
20
|
+
const usedAliases = new Set;
|
|
21
|
+
let aliasIndex = 0;
|
|
22
|
+
const aliasFor = (definitionKey) => {
|
|
23
|
+
let alias = aliases.get(definitionKey);
|
|
24
|
+
if (alias)
|
|
25
|
+
return alias;
|
|
26
|
+
const name = definitionKey.slice(definitionKey.indexOf("/") + 1);
|
|
27
|
+
alias = /^[A-Za-z_$][\w$]*$/.test(name) && !usedAliases.has(name) ? name : `Definition${++aliasIndex}`;
|
|
28
|
+
while (usedAliases.has(alias))
|
|
29
|
+
alias = `Definition${++aliasIndex}`;
|
|
30
|
+
aliases.set(definitionKey, alias);
|
|
31
|
+
usedAliases.add(alias);
|
|
32
|
+
return alias;
|
|
33
|
+
};
|
|
34
|
+
const render = (schema) => {
|
|
35
|
+
if (!isSchema(schema) || hasUnsupportedKeyword(schema))
|
|
36
|
+
return null;
|
|
37
|
+
if ("$ref" in schema) {
|
|
38
|
+
if (typeof schema.$ref !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
const match = schema.$ref.match(/^#\/(\$defs|definitions)\/([^/]+)$/);
|
|
41
|
+
if (!match)
|
|
42
|
+
return null;
|
|
43
|
+
const definitionGroup = match[1];
|
|
44
|
+
const definitionName = match[2];
|
|
45
|
+
if (definitionGroup === undefined || definitionName === undefined)
|
|
46
|
+
return null;
|
|
47
|
+
const definitionKey = `${definitionGroup}/${decodePointerToken(definitionName)}`;
|
|
48
|
+
if (!definitions.has(definitionKey))
|
|
49
|
+
return null;
|
|
50
|
+
return aliasFor(definitionKey);
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(schema.enum)) {
|
|
53
|
+
const values = schema.enum.map(renderLiteral);
|
|
54
|
+
return values.every((value) => value !== null) ? values.join(" | ") : null;
|
|
55
|
+
}
|
|
56
|
+
if (Object.hasOwn(schema, "const"))
|
|
57
|
+
return renderLiteral(schema.const);
|
|
58
|
+
if (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf)) {
|
|
59
|
+
const variants = schema.anyOf ?? schema.oneOf;
|
|
60
|
+
if (variants.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const rendered = variants.map(render);
|
|
63
|
+
return rendered.every((value) => value !== null) ? rendered.join(" | ") : null;
|
|
64
|
+
}
|
|
65
|
+
if (schema.type === "object" || schema.properties !== undefined) {
|
|
66
|
+
if (schema.properties === undefined)
|
|
67
|
+
return "{}";
|
|
68
|
+
if (!isSchema(schema.properties))
|
|
69
|
+
return null;
|
|
70
|
+
const required = new Set(Array.isArray(schema.required) ? schema.required.filter((name) => typeof name === "string") : []);
|
|
71
|
+
const properties = [];
|
|
72
|
+
for (const [name, property] of Object.entries(schema.properties)) {
|
|
73
|
+
const rendered = render(property);
|
|
74
|
+
if (rendered === null)
|
|
75
|
+
return null;
|
|
76
|
+
properties.push(`${formatPropertyName(name)}${required.has(name) ? "" : "?"}: ${rendered};`);
|
|
77
|
+
}
|
|
78
|
+
return properties.length === 0 ? "{}" : `{ ${properties.join(" ")} }`;
|
|
79
|
+
}
|
|
80
|
+
if (schema.type === "array") {
|
|
81
|
+
if (schema.items === undefined)
|
|
82
|
+
return "unknown[]";
|
|
83
|
+
const item = render(schema.items);
|
|
84
|
+
return item === null ? null : `${needsParentheses(item) ? `(${item})` : item}[]`;
|
|
85
|
+
}
|
|
86
|
+
if (Array.isArray(schema.type)) {
|
|
87
|
+
const types = schema.type.map(renderType);
|
|
88
|
+
return types.every((type) => type !== null) ? types.join(" | ") : null;
|
|
89
|
+
}
|
|
90
|
+
if (typeof schema.type === "string")
|
|
91
|
+
return renderType(schema.type);
|
|
92
|
+
return "unknown";
|
|
93
|
+
};
|
|
94
|
+
const root = render(inputSchema);
|
|
95
|
+
if (root === null)
|
|
96
|
+
return null;
|
|
97
|
+
const definitionsText = [];
|
|
98
|
+
for (const [key, alias] of aliases) {
|
|
99
|
+
const definition = definitions.get(key);
|
|
100
|
+
if (!definition)
|
|
101
|
+
return null;
|
|
102
|
+
const rendered = render(definition);
|
|
103
|
+
if (rendered === null)
|
|
104
|
+
return null;
|
|
105
|
+
definitionsText.push(`type ${alias} = ${rendered};`);
|
|
106
|
+
}
|
|
107
|
+
return definitionsText.length > 0 ? `${definitionsText.join(`
|
|
108
|
+
`)}
|
|
109
|
+
|
|
110
|
+
${root}` : root;
|
|
111
|
+
} catch {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function isSchema(value) {
|
|
116
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
117
|
+
}
|
|
118
|
+
function hasUnsupportedKeyword(schema) {
|
|
119
|
+
return UNSUPPORTED_KEYWORDS.some((keyword) => {
|
|
120
|
+
if (!Object.hasOwn(schema, keyword))
|
|
121
|
+
return false;
|
|
122
|
+
return keyword !== "additionalProperties" || schema.additionalProperties !== false;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
function decodePointerToken(token) {
|
|
126
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
127
|
+
}
|
|
128
|
+
function renderType(type) {
|
|
129
|
+
switch (type) {
|
|
130
|
+
case "string":
|
|
131
|
+
return "string";
|
|
132
|
+
case "number":
|
|
133
|
+
case "integer":
|
|
134
|
+
return "number";
|
|
135
|
+
case "boolean":
|
|
136
|
+
return "boolean";
|
|
137
|
+
case "null":
|
|
138
|
+
return "null";
|
|
139
|
+
case "object":
|
|
140
|
+
return "{}";
|
|
141
|
+
case "array":
|
|
142
|
+
return "unknown[]";
|
|
143
|
+
default:
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function renderLiteral(value) {
|
|
148
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
149
|
+
return JSON.stringify(value);
|
|
150
|
+
return typeof value === "number" && Number.isFinite(value) ? String(value) : null;
|
|
151
|
+
}
|
|
152
|
+
function formatPropertyName(name) {
|
|
153
|
+
return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
|
|
154
|
+
}
|
|
155
|
+
function needsParentheses(type) {
|
|
156
|
+
return type.includes(" | ");
|
|
157
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export const MCP_STATUS_EVENT = "dm-mcp-adapter/status/v1";
|
|
2
|
+
export const MCP_STATUS_SNAPSHOT_VERSION = 1;
|
|
3
|
+
export {
|
|
4
|
+
UI_STREAM_HOST_CONTEXT_KEY,
|
|
5
|
+
UI_STREAM_REQUEST_META_KEY,
|
|
6
|
+
UI_STREAM_RESULT_PATCH_METHOD,
|
|
7
|
+
SERVER_STREAM_RESULT_PATCH_METHOD,
|
|
8
|
+
UI_STREAM_STRUCTURED_CONTENT_KEY,
|
|
9
|
+
uiStreamModeSchema,
|
|
10
|
+
visualizationStreamPhaseSchema,
|
|
11
|
+
visualizationStreamFrameTypeSchema,
|
|
12
|
+
visualizationStreamStatusSchema,
|
|
13
|
+
uiStreamHostContextSchema,
|
|
14
|
+
visualizationStreamEnvelopeSchema,
|
|
15
|
+
uiStreamCallToolResultSchema,
|
|
16
|
+
uiStreamResultPatchNotificationSchema,
|
|
17
|
+
serverStreamResultPatchNotificationSchema,
|
|
18
|
+
getUiStreamHostContext,
|
|
19
|
+
getVisualizationStreamEnvelope
|
|
20
|
+
} from "./ui-stream-types.js";
|
|
21
|
+
export function extractUiPromptText(params) {
|
|
22
|
+
if (params.type === "prompt" || params.prompt) {
|
|
23
|
+
const prompt = params.prompt ?? String(params.message ?? "");
|
|
24
|
+
return prompt || undefined;
|
|
25
|
+
}
|
|
26
|
+
if (params.role === "user" && Array.isArray(params.content)) {
|
|
27
|
+
const text = params.content.map((block) => block && typeof block === "object" && ("text" in block) ? String(block.text ?? "") : "").filter(Boolean).join(`
|
|
28
|
+
|
|
29
|
+
`);
|
|
30
|
+
return text || undefined;
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
export function parseUiPromptHandoff(prompt) {
|
|
35
|
+
const newlineIndex = prompt.indexOf(`
|
|
36
|
+
`);
|
|
37
|
+
if (newlineIndex <= 0) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const intent = prompt.slice(0, newlineIndex).trim();
|
|
41
|
+
const payloadText = prompt.slice(newlineIndex + 1).trim();
|
|
42
|
+
if (!intent || !payloadText) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(intent)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(payloadText);
|
|
50
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
intent,
|
|
55
|
+
params: parsed,
|
|
56
|
+
raw: prompt
|
|
57
|
+
};
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function createUiModelContextUpdate(params, maxChars = 12000) {
|
|
63
|
+
const payload = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));
|
|
64
|
+
if (Object.keys(payload).length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const serialized = JSON.stringify(payload);
|
|
67
|
+
if (serialized.length <= maxChars) {
|
|
68
|
+
return { payload, summary: serialized, truncated: false };
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
summary: `${serialized.slice(0, Math.max(0, maxChars - 1))}…`,
|
|
72
|
+
truncated: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function isServerDisabled(definition) {
|
|
76
|
+
return definition?.disabled === true;
|
|
77
|
+
}
|
|
78
|
+
export const MCP_TOOL_APPROVAL_REQUEST_EVENT = "dm-mcp-adapter:tool-approval-request";
|
|
79
|
+
function sanitizeServerPrefix(serverName, preserveProviderValid = true) {
|
|
80
|
+
const validCharacters = preserveProviderValid ? /^[A-Za-z0-9_-]$/ : /^[A-Za-z0-9]$/;
|
|
81
|
+
return Array.from(serverName, (char) => validCharacters.test(char) ? char : `_${char.codePointAt(0).toString(16)}_`).join("");
|
|
82
|
+
}
|
|
83
|
+
export function getServerPrefix(serverName, mode) {
|
|
84
|
+
if (mode === "none")
|
|
85
|
+
return "";
|
|
86
|
+
if (mode === "short") {
|
|
87
|
+
let short = sanitizeServerPrefix(serverName.replace(/-?mcp$/i, ""));
|
|
88
|
+
if (!short)
|
|
89
|
+
short = "mcp";
|
|
90
|
+
return short;
|
|
91
|
+
}
|
|
92
|
+
if (mode === "mcp")
|
|
93
|
+
return `mcp__${sanitizeServerPrefix(serverName)}`;
|
|
94
|
+
return sanitizeServerPrefix(serverName);
|
|
95
|
+
}
|
|
96
|
+
export function formatToolName(toolName, serverName, prefix) {
|
|
97
|
+
const p = getServerPrefix(serverName, prefix);
|
|
98
|
+
const sanitized = toolName.replace(/\./g, "_");
|
|
99
|
+
return p ? `${p}_${sanitized}` : sanitized;
|
|
100
|
+
}
|
|
101
|
+
export function resolveToolPrefix(definition, globalPrefix) {
|
|
102
|
+
return definition?.toolPrefix ?? globalPrefix ?? "server";
|
|
103
|
+
}
|
|
104
|
+
export function resolveServerFromToolName(toolName, serverNames, prefix) {
|
|
105
|
+
if (prefix === "none")
|
|
106
|
+
return;
|
|
107
|
+
const candidates = [];
|
|
108
|
+
for (const name of serverNames) {
|
|
109
|
+
const p = getServerPrefix(name, prefix);
|
|
110
|
+
if (p && toolName.startsWith(p + "_"))
|
|
111
|
+
candidates.push({ name, prefix: p });
|
|
112
|
+
}
|
|
113
|
+
if (candidates.length === 0)
|
|
114
|
+
return;
|
|
115
|
+
candidates.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
116
|
+
const best = candidates[0];
|
|
117
|
+
if (candidates.some((c) => c.prefix === best.prefix && c.name !== best.name)) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
return best?.name;
|
|
121
|
+
}
|
|
122
|
+
export function sanitizePromptName(name) {
|
|
123
|
+
const cleaned = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^[_-]+|[_-]+$/g, "");
|
|
124
|
+
if (!cleaned)
|
|
125
|
+
return "prompt";
|
|
126
|
+
return /^[0-9]/.test(cleaned) ? `_${cleaned}` : cleaned;
|
|
127
|
+
}
|
|
128
|
+
export function formatPromptCommandName(promptName, serverName, prefix) {
|
|
129
|
+
const serverPart = getServerPrefix(serverName, prefix) || sanitizeServerPrefix(serverName) || "server";
|
|
130
|
+
return `mcp__${serverPart}__${sanitizePromptName(promptName)}`;
|
|
131
|
+
}
|
|
132
|
+
function getLegacyServerPrefix(serverName, mode) {
|
|
133
|
+
if (mode === "none")
|
|
134
|
+
return "";
|
|
135
|
+
if (mode === "short")
|
|
136
|
+
return sanitizeServerPrefix(serverName.replace(/-?mcp$/i, ""), false) || "mcp";
|
|
137
|
+
if (mode === "mcp")
|
|
138
|
+
return `mcp__${sanitizeServerPrefix(serverName, false)}`;
|
|
139
|
+
return sanitizeServerPrefix(serverName, false);
|
|
140
|
+
}
|
|
141
|
+
function formatLegacyToolName(toolName, serverName, prefix) {
|
|
142
|
+
const serverPrefix = getLegacyServerPrefix(serverName, prefix);
|
|
143
|
+
const sanitizedToolName = toolName.replace(/[.-]/g, "_");
|
|
144
|
+
return serverPrefix ? `${serverPrefix}_${sanitizedToolName}` : sanitizedToolName;
|
|
145
|
+
}
|
|
146
|
+
export function getToolNameCandidates(toolName, serverName, prefix, includeLegacy = true) {
|
|
147
|
+
const candidates = new Set([
|
|
148
|
+
toolName,
|
|
149
|
+
formatToolName(toolName, serverName, prefix),
|
|
150
|
+
formatToolName(toolName, serverName, "server"),
|
|
151
|
+
formatToolName(toolName, serverName, "short"),
|
|
152
|
+
formatToolName(toolName, serverName, "mcp")
|
|
153
|
+
]);
|
|
154
|
+
if (includeLegacy) {
|
|
155
|
+
const legacyToolName = toolName.replace(/-/g, "_");
|
|
156
|
+
candidates.add(legacyToolName);
|
|
157
|
+
candidates.add(formatToolName(legacyToolName, serverName, prefix));
|
|
158
|
+
candidates.add(formatToolName(legacyToolName, serverName, "server"));
|
|
159
|
+
candidates.add(formatToolName(legacyToolName, serverName, "short"));
|
|
160
|
+
candidates.add(formatToolName(legacyToolName, serverName, "mcp"));
|
|
161
|
+
candidates.add(formatLegacyToolName(toolName, serverName, prefix));
|
|
162
|
+
candidates.add(formatLegacyToolName(toolName, serverName, "server"));
|
|
163
|
+
candidates.add(formatLegacyToolName(toolName, serverName, "short"));
|
|
164
|
+
candidates.add(formatLegacyToolName(toolName, serverName, "mcp"));
|
|
165
|
+
candidates.add(formatToolName(toolName, serverName, prefix).replace(/-/g, "_"));
|
|
166
|
+
candidates.add(formatToolName(toolName, serverName, "server").replace(/-/g, "_"));
|
|
167
|
+
candidates.add(formatToolName(toolName, serverName, "short").replace(/-/g, "_"));
|
|
168
|
+
candidates.add(formatToolName(toolName, serverName, "mcp").replace(/-/g, "_"));
|
|
169
|
+
}
|
|
170
|
+
return candidates;
|
|
171
|
+
}
|
|
172
|
+
function globToRegExp(pattern) {
|
|
173
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
174
|
+
return new RegExp(`^${escaped}$`);
|
|
175
|
+
}
|
|
176
|
+
export function createToolSelectorCandidateIndex(allCurrentCandidates, additionalCurrentCandidatesByToolName) {
|
|
177
|
+
return {
|
|
178
|
+
allCurrentCandidates,
|
|
179
|
+
matchingCountByPattern: new Map,
|
|
180
|
+
matcherByPattern: new Map,
|
|
181
|
+
...additionalCurrentCandidatesByToolName ? { additionalCurrentCandidatesByToolName } : {}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function matchesToolPattern(candidates, patterns) {
|
|
185
|
+
if (!Array.isArray(patterns) || patterns.length === 0)
|
|
186
|
+
return false;
|
|
187
|
+
for (const pattern of patterns) {
|
|
188
|
+
if (typeof pattern !== "string")
|
|
189
|
+
continue;
|
|
190
|
+
if (!pattern.includes("*") && !pattern.includes("?") && candidates.has(pattern)) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
if ((pattern.includes("*") || pattern.includes("?")) && [...candidates].some((candidate) => globToRegExp(pattern).test(candidate))) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function indexHasOtherCurrentMatch(index, toolName, currentCandidates, pattern) {
|
|
200
|
+
const additionalCandidates = index.additionalCurrentCandidatesByToolName?.get(toolName);
|
|
201
|
+
const hasCandidate = (candidate) => index.allCurrentCandidates.has(candidate) || additionalCandidates?.has(candidate) === true;
|
|
202
|
+
const isGlob = pattern.includes("*") || pattern.includes("?");
|
|
203
|
+
if (!isGlob) {
|
|
204
|
+
return hasCandidate(pattern) && !currentCandidates.has(pattern);
|
|
205
|
+
}
|
|
206
|
+
let matcher = index.matcherByPattern.get(pattern);
|
|
207
|
+
if (!matcher) {
|
|
208
|
+
matcher = globToRegExp(pattern);
|
|
209
|
+
index.matcherByPattern.set(pattern, matcher);
|
|
210
|
+
}
|
|
211
|
+
let matchingCount = index.matchingCountByPattern.get(pattern);
|
|
212
|
+
if (matchingCount === undefined) {
|
|
213
|
+
matchingCount = 0;
|
|
214
|
+
for (const candidate of index.allCurrentCandidates) {
|
|
215
|
+
if (matcher.test(candidate))
|
|
216
|
+
matchingCount++;
|
|
217
|
+
}
|
|
218
|
+
index.matchingCountByPattern.set(pattern, matchingCount);
|
|
219
|
+
}
|
|
220
|
+
let totalMatchingCount = matchingCount;
|
|
221
|
+
if (additionalCandidates) {
|
|
222
|
+
for (const candidate of additionalCandidates) {
|
|
223
|
+
if (!index.allCurrentCandidates.has(candidate) && matcher.test(candidate))
|
|
224
|
+
totalMatchingCount++;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (totalMatchingCount === 0)
|
|
228
|
+
return false;
|
|
229
|
+
let currentMatchingCount = 0;
|
|
230
|
+
for (const candidate of currentCandidates) {
|
|
231
|
+
if (hasCandidate(candidate) && matcher.test(candidate))
|
|
232
|
+
currentMatchingCount++;
|
|
233
|
+
}
|
|
234
|
+
return totalMatchingCount > currentMatchingCount;
|
|
235
|
+
}
|
|
236
|
+
function matchesToolSelector(toolName, serverName, prefix, patterns, otherCurrentCandidates) {
|
|
237
|
+
if (!Array.isArray(patterns) || patterns.length === 0)
|
|
238
|
+
return false;
|
|
239
|
+
const currentCandidates = getToolNameCandidates(toolName, serverName, prefix, false);
|
|
240
|
+
if (matchesToolPattern(currentCandidates, patterns))
|
|
241
|
+
return true;
|
|
242
|
+
if (!otherCurrentCandidates)
|
|
243
|
+
return matchesToolPattern(getToolNameCandidates(toolName, serverName, prefix), patterns);
|
|
244
|
+
const legacyCandidates = getToolNameCandidates(toolName, serverName, prefix);
|
|
245
|
+
for (const candidate of currentCandidates)
|
|
246
|
+
legacyCandidates.delete(candidate);
|
|
247
|
+
return patterns.some((pattern) => {
|
|
248
|
+
if (typeof pattern !== "string" || !matchesToolPattern(legacyCandidates, [pattern]))
|
|
249
|
+
return false;
|
|
250
|
+
const hasCollision = otherCurrentCandidates instanceof Set ? matchesToolPattern(otherCurrentCandidates, [pattern]) : indexHasOtherCurrentMatch(otherCurrentCandidates, toolName, currentCandidates, pattern);
|
|
251
|
+
return !hasCollision;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
export function isToolIncluded(toolName, serverName, prefix, includeTools, otherCurrentCandidates) {
|
|
255
|
+
if (!Array.isArray(includeTools) || includeTools.length === 0)
|
|
256
|
+
return true;
|
|
257
|
+
return matchesToolSelector(toolName, serverName, prefix, includeTools, otherCurrentCandidates);
|
|
258
|
+
}
|
|
259
|
+
export function isToolExcluded(toolName, serverName, prefix, excludeTools, otherCurrentCandidates) {
|
|
260
|
+
return matchesToolSelector(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
|
|
261
|
+
}
|
|
262
|
+
export function isToolAllowed(toolName, serverName, prefix, includeTools, excludeTools, otherCurrentCandidates) {
|
|
263
|
+
return isToolIncluded(toolName, serverName, prefix, includeTools, otherCurrentCandidates) && !isToolExcluded(toolName, serverName, prefix, excludeTools, otherCurrentCandidates);
|
|
264
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
|
|
2
|
+
const RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
3
|
+
export function getToolUiResourceUri(tool) {
|
|
4
|
+
const meta = tool._meta;
|
|
5
|
+
let resourceUri = getNestedResourceUri(meta);
|
|
6
|
+
if (resourceUri === undefined) {
|
|
7
|
+
resourceUri = meta?.[RESOURCE_URI_META_KEY];
|
|
8
|
+
}
|
|
9
|
+
if (typeof resourceUri === "string" && resourceUri.startsWith("ui://")) {
|
|
10
|
+
return resourceUri;
|
|
11
|
+
}
|
|
12
|
+
if (resourceUri !== undefined) {
|
|
13
|
+
throw new Error(`Invalid UI resource URI: ${JSON.stringify(resourceUri)}`);
|
|
14
|
+
}
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
export function buildAllowAttribute(permissions) {
|
|
18
|
+
if (!permissions)
|
|
19
|
+
return "";
|
|
20
|
+
const allowed = [];
|
|
21
|
+
if (permissions.camera)
|
|
22
|
+
allowed.push("camera");
|
|
23
|
+
if (permissions.microphone)
|
|
24
|
+
allowed.push("microphone");
|
|
25
|
+
if (permissions.geolocation)
|
|
26
|
+
allowed.push("geolocation");
|
|
27
|
+
if (permissions.clipboardWrite)
|
|
28
|
+
allowed.push("clipboard-write");
|
|
29
|
+
return allowed.join("; ");
|
|
30
|
+
}
|
|
31
|
+
function getNestedResourceUri(meta) {
|
|
32
|
+
const ui = meta?.ui;
|
|
33
|
+
if (!ui || typeof ui !== "object")
|
|
34
|
+
return;
|
|
35
|
+
return ui.resourceUri;
|
|
36
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.js";
|
|
2
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
3
|
+
import {
|
|
4
|
+
getInputRequiredNeedsUiDetails,
|
|
5
|
+
InputRequiredNeedsUiError,
|
|
6
|
+
ResourceFetchError,
|
|
7
|
+
ResourceParseError
|
|
8
|
+
} from "./errors.js";
|
|
9
|
+
import { logger } from "./logger.js";
|
|
10
|
+
import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
|
|
11
|
+
import { isServerDisabled } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export class UiResourceHandler {
|
|
14
|
+
manager;
|
|
15
|
+
config;
|
|
16
|
+
log = logger.child({ component: "UiResourceHandler" });
|
|
17
|
+
constructor(manager, config = undefined) {
|
|
18
|
+
this.manager = manager;
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
async readUiResource(serverName, uri, options = {}) {
|
|
22
|
+
const log = this.log.child({ server: serverName, uri });
|
|
23
|
+
if (!uri.startsWith("ui://")) {
|
|
24
|
+
throw new ResourceParseError(uri, "URI must start with ui://", { server: serverName });
|
|
25
|
+
}
|
|
26
|
+
log.debug("Fetching UI resource");
|
|
27
|
+
let result;
|
|
28
|
+
try {
|
|
29
|
+
const config = options.config ?? this.config;
|
|
30
|
+
if (config && isServerDisabled(config.mcpServers[serverName])) {
|
|
31
|
+
throw new Error(`MCP server "${serverName}" is disabled`);
|
|
32
|
+
}
|
|
33
|
+
if (config) {
|
|
34
|
+
this.manager.touch(serverName);
|
|
35
|
+
this.manager.incrementInFlight(serverName);
|
|
36
|
+
try {
|
|
37
|
+
result = await withSessionRecovery({
|
|
38
|
+
manager: this.manager,
|
|
39
|
+
config,
|
|
40
|
+
...options.signal ? { signal: options.signal } : {},
|
|
41
|
+
...options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}
|
|
42
|
+
}, serverName, async (connection) => {
|
|
43
|
+
const refreshRead = await this.manager.prepareResourceUse?.(serverName, uri, connection);
|
|
44
|
+
const requestOptions = this.manager.getRequestOptions(serverName, options.signal);
|
|
45
|
+
return connection.client.readResource({ uri }, refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions);
|
|
46
|
+
});
|
|
47
|
+
} finally {
|
|
48
|
+
this.manager.decrementInFlight(serverName);
|
|
49
|
+
this.manager.touch(serverName);
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
result = await this.manager.readResource(serverName, uri, options.signal);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError)
|
|
56
|
+
throw error;
|
|
57
|
+
const inputRequired = getInputRequiredNeedsUiDetails(error, { server: serverName, resourceUri: uri });
|
|
58
|
+
if (inputRequired) {
|
|
59
|
+
throw new InputRequiredNeedsUiError(inputRequired, error instanceof Error ? error : undefined);
|
|
60
|
+
}
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
log.error("Failed to read resource", error instanceof Error ? error : undefined);
|
|
63
|
+
throw new ResourceFetchError(uri, message, {
|
|
64
|
+
server: serverName,
|
|
65
|
+
...error instanceof Error ? { cause: error } : {}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const content = selectContent(result, uri);
|
|
69
|
+
const mimeType = content.mimeType;
|
|
70
|
+
if (mimeType && !isHtmlMimeType(mimeType)) {
|
|
71
|
+
log.warn("Unsupported MIME type", { mimeType });
|
|
72
|
+
throw new ResourceParseError(uri, `unsupported MIME type "${mimeType}" (expected text/html or ${RESOURCE_MIME_TYPE})`, { server: serverName, mimeType });
|
|
73
|
+
}
|
|
74
|
+
const html = toHtml(content);
|
|
75
|
+
if (!html.trim()) {
|
|
76
|
+
log.warn("Resource content is empty");
|
|
77
|
+
throw new ResourceParseError(uri, "content is empty", { server: serverName });
|
|
78
|
+
}
|
|
79
|
+
const contentMeta = extractUiMeta(content._meta);
|
|
80
|
+
const listMeta = extractUiMeta(this.getListResourceMeta(serverName, uri));
|
|
81
|
+
log.debug("Resource loaded successfully", {
|
|
82
|
+
contentLength: html.length,
|
|
83
|
+
hasCsp: !!contentMeta.csp || !!listMeta.csp
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
uri: content.uri ?? uri,
|
|
87
|
+
html,
|
|
88
|
+
mimeType: mimeType ?? RESOURCE_MIME_TYPE,
|
|
89
|
+
meta: {
|
|
90
|
+
...(contentMeta.csp ?? listMeta.csp) !== undefined ? { csp: contentMeta.csp ?? listMeta.csp } : {},
|
|
91
|
+
...(contentMeta.permissions ?? listMeta.permissions) !== undefined ? { permissions: contentMeta.permissions ?? listMeta.permissions } : {},
|
|
92
|
+
...(contentMeta.domain ?? listMeta.domain) !== undefined ? { domain: contentMeta.domain ?? listMeta.domain } : {},
|
|
93
|
+
...(contentMeta.prefersBorder ?? listMeta.prefersBorder) !== undefined ? { prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder } : {}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
getListResourceMeta(serverName, uri) {
|
|
98
|
+
const connection = this.manager.getConnection(serverName);
|
|
99
|
+
if (!connection?.resources?.length)
|
|
100
|
+
return;
|
|
101
|
+
const resource = connection.resources.find((entry) => entry.uri === uri);
|
|
102
|
+
if (!resource || !resource._meta || typeof resource._meta !== "object")
|
|
103
|
+
return;
|
|
104
|
+
return resource._meta;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function selectContent(result, preferredUri) {
|
|
108
|
+
const contents = result.contents ?? [];
|
|
109
|
+
if (contents.length === 0) {
|
|
110
|
+
throw new Error(`No contents returned for UI resource: ${preferredUri}`);
|
|
111
|
+
}
|
|
112
|
+
const byUri = contents.find((content) => content.uri === preferredUri);
|
|
113
|
+
if (byUri)
|
|
114
|
+
return byUri;
|
|
115
|
+
const byHtmlMime = contents.find((content) => content.mimeType && isHtmlMimeType(content.mimeType));
|
|
116
|
+
if (byHtmlMime)
|
|
117
|
+
return byHtmlMime;
|
|
118
|
+
const firstContent = contents[0];
|
|
119
|
+
if (!firstContent) {
|
|
120
|
+
throw new Error(`No contents returned for UI resource: ${preferredUri}`);
|
|
121
|
+
}
|
|
122
|
+
return firstContent;
|
|
123
|
+
}
|
|
124
|
+
function isHtmlMimeType(mimeType) {
|
|
125
|
+
const normalized = mimeType.toLowerCase();
|
|
126
|
+
return normalized.startsWith("text/html") || normalized === RESOURCE_MIME_TYPE.toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
function toHtml(content) {
|
|
129
|
+
if (typeof content.text === "string") {
|
|
130
|
+
return content.text;
|
|
131
|
+
}
|
|
132
|
+
if (typeof content.blob === "string") {
|
|
133
|
+
return Buffer.from(content.blob, "base64").toString("utf-8");
|
|
134
|
+
}
|
|
135
|
+
throw new Error(`UI resource ${content.uri ?? "(unknown)"} did not include text or blob content`);
|
|
136
|
+
}
|
|
137
|
+
const OPENAI_CSP_FIELD_MAPPINGS = [
|
|
138
|
+
["resource_domains", "resourceDomains"],
|
|
139
|
+
["connect_domains", "connectDomains"],
|
|
140
|
+
["frame_domains", "frameDomains"]
|
|
141
|
+
];
|
|
142
|
+
const UI_CSP_DOMAIN_FIELDS = [
|
|
143
|
+
"resourceDomains",
|
|
144
|
+
"connectDomains",
|
|
145
|
+
"frameDomains",
|
|
146
|
+
"baseUriDomains"
|
|
147
|
+
];
|
|
148
|
+
function extractUiMeta(meta) {
|
|
149
|
+
if (!meta || typeof meta !== "object")
|
|
150
|
+
return {};
|
|
151
|
+
const ui = isRecord(meta.ui) ? meta.ui : undefined;
|
|
152
|
+
const out = {};
|
|
153
|
+
const openAiCsp = Object.hasOwn(meta, "openai/widgetCSP") ? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"]) : undefined;
|
|
154
|
+
const hasStandardCsp = !!ui && Object.hasOwn(ui, "csp");
|
|
155
|
+
const standardCspValue = hasStandardCsp ? ui.csp : undefined;
|
|
156
|
+
if (hasStandardCsp && !isRecord(standardCspValue)) {
|
|
157
|
+
out.csp = {};
|
|
158
|
+
} else {
|
|
159
|
+
const standardCsp = hasStandardCsp ? normalizeUiResourceCsp(standardCspValue) : undefined;
|
|
160
|
+
if (openAiCsp || standardCsp) {
|
|
161
|
+
out.csp = { ...openAiCsp, ...standardCsp };
|
|
162
|
+
if (isRecord(standardCspValue)) {
|
|
163
|
+
for (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {
|
|
164
|
+
if (Object.hasOwn(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
|
|
165
|
+
delete out.csp[standardField];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (ui && isRecord(ui.permissions)) {
|
|
172
|
+
out.permissions = ui.permissions;
|
|
173
|
+
}
|
|
174
|
+
if (ui && typeof ui.domain === "string") {
|
|
175
|
+
out.domain = ui.domain;
|
|
176
|
+
}
|
|
177
|
+
if (ui && typeof ui.prefersBorder === "boolean") {
|
|
178
|
+
out.prefersBorder = ui.prefersBorder;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
function normalizeUiResourceCsp(value) {
|
|
183
|
+
if (!isRecord(value))
|
|
184
|
+
return {};
|
|
185
|
+
const csp = {};
|
|
186
|
+
for (const field of UI_CSP_DOMAIN_FIELDS) {
|
|
187
|
+
const domains = copyStringArray(value[field]);
|
|
188
|
+
if (domains)
|
|
189
|
+
csp[field] = domains;
|
|
190
|
+
}
|
|
191
|
+
return csp;
|
|
192
|
+
}
|
|
193
|
+
function normalizeOpenAiWidgetCsp(value) {
|
|
194
|
+
if (!isRecord(value))
|
|
195
|
+
return {};
|
|
196
|
+
const csp = {};
|
|
197
|
+
for (const [sourceField, targetField] of OPENAI_CSP_FIELD_MAPPINGS) {
|
|
198
|
+
const domains = copyStringArray(value[sourceField]);
|
|
199
|
+
if (domains)
|
|
200
|
+
csp[targetField] = domains;
|
|
201
|
+
}
|
|
202
|
+
return csp;
|
|
203
|
+
}
|
|
204
|
+
function copyStringArray(value) {
|
|
205
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? [...value] : undefined;
|
|
206
|
+
}
|
|
207
|
+
function isRecord(value) {
|
|
208
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
209
|
+
}
|