@fitchmultz/pi-mcp-adapter 5.0.0 → 6.0.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 +25 -1
- package/OAUTH.md +3 -3
- package/README.md +153 -85
- package/app-bridge.bundle.js +75 -33
- package/commands.ts +9 -6
- package/direct-tools.ts +25 -358
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/mcp-code.d.ts +2 -1
- package/dist/mcp-code.js +87 -41
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +1 -1
- package/dist/mcp-panel.js +34 -97
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-status.js +2 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +40 -6
- package/dist/proxy-modes.js +270 -485
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +4 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +66 -137
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +3 -0
- package/dist/tool-result-renderer.js +8 -3
- package/dist/types.d.ts +11 -36
- package/dist/utils.js +2 -4
- package/gateway-arguments.ts +73 -0
- package/index.ts +166 -299
- package/init.ts +6 -20
- package/mcp-code.ts +71 -45
- package/mcp-output-guard.ts +145 -27
- package/mcp-panel.ts +38 -109
- package/mcp-script-worker.mjs +5 -2
- package/mcp-status.ts +2 -1
- package/metadata-cache.ts +31 -93
- package/package.json +14 -9
- package/prompts.ts +1 -1
- package/proxy-modes.ts +265 -517
- package/resource-tools.ts +36 -1
- package/sampling-handler.ts +4 -1
- package/scripts/build-app-bridge.mjs +15 -0
- package/scripts/build.mjs +3 -2
- package/scripts/prepare.mjs +1 -0
- package/search-ranking.ts +71 -135
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/state.ts +1 -1
- package/tool-loader.ts +153 -0
- package/tool-metadata.ts +22 -17
- package/tool-registrar.ts +18 -13
- package/tool-result-renderer.ts +10 -2
- package/types.ts +15 -40
- package/utils.ts +2 -4
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/ts-shape.ts +0 -145
package/dist/resource-tools.d.ts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
+
import type { McpExtensionState } from "./state.ts";
|
|
2
|
+
import { type McpOperationContext, type McpToolCallIdentity } from "./types.ts";
|
|
3
|
+
export declare function executeResourceList(state: McpExtensionState, server: string, limit?: number, offset?: number, signal?: AbortSignal): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>> | {
|
|
4
|
+
content: {
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}[];
|
|
8
|
+
details: {
|
|
9
|
+
mode: string;
|
|
10
|
+
error: string;
|
|
11
|
+
server: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
}>;
|
|
15
|
+
export declare function executeResourceRead(state: McpExtensionState, server: string, uri: string, signal?: AbortSignal, identity?: McpToolCallIdentity, beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>, raw?: boolean): Promise<import("@earendil-works/pi-agent-core").AgentToolResult<Record<string, unknown>>>;
|
|
1
16
|
export declare function resourceNameToToolName(name: string): string;
|
package/dist/resource-tools.js
CHANGED
|
@@ -1,4 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
2
|
+
import { executeCall, prepareMcpConnection } from "./proxy-modes.js";
|
|
3
|
+
import { paginate } from "./search-ranking.js";
|
|
4
|
+
import { throwIfAborted } from "./abort.js";
|
|
5
|
+
import { combineAbortSignals } from "./runtime-owner.js";
|
|
6
|
+
export async function executeResourceList(state, server, limit = 12, offset = 0, signal) {
|
|
7
|
+
throwIfAborted(combineAbortSignals(state.owner?.signal, signal));
|
|
8
|
+
const definition = state.config.mcpServers[server];
|
|
9
|
+
if (!definition || isServerDisabled(definition)) {
|
|
10
|
+
return { content: [{ type: "text", text: `Server "${server}" is ${definition ? "disabled" : "not configured"}.` }], details: { mode: "resources", server, error: definition ? "server_disabled" : "server_not_found" } };
|
|
11
|
+
}
|
|
12
|
+
if (!state.toolMetadata.has(server) || state.manager.getConnection(server)?.status === "connected") {
|
|
13
|
+
const prepared = await prepareMcpConnection(state, server, signal);
|
|
14
|
+
if (prepared.error)
|
|
15
|
+
return prepared.error;
|
|
16
|
+
}
|
|
17
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
18
|
+
const page = paginate((state.toolMetadata.get(server) ?? [])
|
|
19
|
+
.filter(tool => tool.resourceUri !== undefined && definition.exposeResources !== false
|
|
20
|
+
&& isToolAllowed(tool.originalName, server, prefix, definition.includeTools, definition.excludeTools))
|
|
21
|
+
.map(tool => tool.resourceDescriptor ?? { uri: tool.resourceUri, description: tool.description }), offset, limit);
|
|
22
|
+
return { content: [{ type: "text", text: JSON.stringify({ server, ...page }, null, 2) }], details: { mode: "resources", server, ...page } };
|
|
23
|
+
}
|
|
24
|
+
export async function executeResourceRead(state, server, uri, signal, identity = {}, beforeDispatch, raw = false) {
|
|
25
|
+
return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
|
|
26
|
+
}
|
|
2
27
|
export function resourceNameToToolName(name) {
|
|
3
28
|
let result = name
|
|
4
29
|
.replace(/[^a-zA-Z0-9]/g, "_")
|
package/dist/sampling-handler.js
CHANGED
|
@@ -28,12 +28,15 @@ export async function handleSamplingRequest(options, request) {
|
|
|
28
28
|
throwIfAborted(signal);
|
|
29
29
|
await confirmSampling(options, "Approve MCP sampling request", formatRequestApproval(options.serverName, `${model.provider}/${model.id}`, params.systemPrompt, messages));
|
|
30
30
|
throwIfAborted(signal);
|
|
31
|
+
const supportsTemperature = model.compat && "supportsTemperature" in model.compat
|
|
32
|
+
? model.compat.supportsTemperature !== false
|
|
33
|
+
: !(model.id === "gpt-6-astra" && (model.api === "openai-responses" || model.api === "openai-codex-responses"));
|
|
31
34
|
const result = await options.modelRegistry.complete(model, {
|
|
32
35
|
...(params.systemPrompt !== undefined ? { systemPrompt: params.systemPrompt } : {}),
|
|
33
36
|
messages,
|
|
34
37
|
}, {
|
|
35
38
|
maxTokens: params.maxTokens,
|
|
36
|
-
...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
39
|
+
...(supportsTemperature && params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
37
40
|
...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
|
|
38
41
|
...(signal ? { signal } : {}),
|
|
39
42
|
});
|
package/dist/search-ranking.d.ts
CHANGED
|
@@ -8,8 +8,6 @@ export interface RankedToolMatch {
|
|
|
8
8
|
score: number;
|
|
9
9
|
}
|
|
10
10
|
export declare function normalizeSearchText(value: string): string;
|
|
11
|
-
export declare function tokenize(value: string): string[];
|
|
12
|
-
export declare function scoreToolMatch(tool: ToolMetadata, server: string, query: string): number | null;
|
|
13
11
|
export declare function rankToolMatches(state: McpExtensionState, query: string, server?: string): RankedToolMatch[];
|
|
14
12
|
export declare function paginate<T>(items: T[], offset: number, limit: number): {
|
|
15
13
|
items: T[];
|
package/dist/search-ranking.js
CHANGED
|
@@ -1,98 +1,75 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
* Shortest field token allowed to stem-match a longer query token.
|
|
4
|
-
* Real descriptions tokenize possessives into single letters ("project's" -> ["project", "s"]),
|
|
5
|
-
* which would otherwise make every query starting with that letter a match.
|
|
6
|
-
*/
|
|
7
|
-
const MIN_STEM_LENGTH = 4;
|
|
1
|
+
import MiniSearch from "minisearch";
|
|
2
|
+
import { getServerPrefix, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
|
|
8
3
|
export const MAX_PAGE_SIZE = 100;
|
|
9
4
|
export const MAX_TOOL_NAME_LENGTH = 512;
|
|
10
|
-
const FIELD_WEIGHTS = {
|
|
11
|
-
name: 12,
|
|
12
|
-
originalName: 10,
|
|
13
|
-
server: 8,
|
|
14
|
-
description: 5,
|
|
15
|
-
};
|
|
16
5
|
export function normalizeSearchText(value) {
|
|
17
6
|
return value
|
|
18
7
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
19
8
|
.replace(/[_./:-]+/g, " ")
|
|
20
9
|
.toLowerCase();
|
|
21
10
|
}
|
|
22
|
-
|
|
23
|
-
|
|
11
|
+
function schemaSearchText(schema) {
|
|
12
|
+
if (!schema || typeof schema !== "object")
|
|
13
|
+
return "";
|
|
14
|
+
if (Array.isArray(schema))
|
|
15
|
+
return schema.map(schemaSearchText).join(" ");
|
|
16
|
+
return Object.entries(schema).flatMap(([key, value]) => {
|
|
17
|
+
if ((key === "description" || key === "title" || key === "const") && typeof value === "string")
|
|
18
|
+
return value;
|
|
19
|
+
if (key === "enum" && Array.isArray(value))
|
|
20
|
+
return value.filter(item => typeof item === "string");
|
|
21
|
+
if (key === "properties" && value && typeof value === "object")
|
|
22
|
+
return [Object.keys(value).join(" "), schemaSearchText(value)];
|
|
23
|
+
return schemaSearchText(value);
|
|
24
|
+
}).join(" ");
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
phraseMatched = true;
|
|
51
|
-
}
|
|
52
|
-
else if (value.includes(normalizedQuery)) {
|
|
53
|
-
score += weight * 6;
|
|
54
|
-
phraseMatched = true;
|
|
55
|
-
}
|
|
56
|
-
for (const token of queryTokens) {
|
|
57
|
-
if (fieldTokens.includes(token)) {
|
|
58
|
-
score += weight * 4;
|
|
59
|
-
matchedTokens.add(token);
|
|
60
|
-
}
|
|
61
|
-
else if (fieldTokens.some(fieldToken => fieldToken.startsWith(token) || (fieldToken.length >= MIN_STEM_LENGTH && token.startsWith(fieldToken)))) {
|
|
62
|
-
score += weight * 2;
|
|
63
|
-
matchedTokens.add(token);
|
|
64
|
-
}
|
|
65
|
-
else if (value.includes(token)) {
|
|
66
|
-
score += weight;
|
|
67
|
-
matchedTokens.add(token);
|
|
68
|
-
}
|
|
26
|
+
const indexes = new WeakMap();
|
|
27
|
+
function getIndex(state) {
|
|
28
|
+
const cached = indexes.get(state);
|
|
29
|
+
// Catalog publishers replace each server's metadata array on discovery/reconnect.
|
|
30
|
+
if (cached && cached.metadata.size === state.toolMetadata.size
|
|
31
|
+
&& [...state.toolMetadata].every(([server, metadata]) => cached.metadata.get(server) === metadata)) {
|
|
32
|
+
return cached.index;
|
|
33
|
+
}
|
|
34
|
+
const index = new MiniSearch({
|
|
35
|
+
fields: ["name", "originalName", "description", "schema"],
|
|
36
|
+
storeFields: ["server", "tool"],
|
|
37
|
+
searchOptions: { fields: ["name", "description", "schema"], boost: { name: 2, description: 1, schema: 1 }, prefix: true },
|
|
38
|
+
});
|
|
39
|
+
let id = 0;
|
|
40
|
+
for (const [server, tools] of state.toolMetadata) {
|
|
41
|
+
for (const tool of tools) {
|
|
42
|
+
if (tool.resourceUri !== undefined)
|
|
43
|
+
continue;
|
|
44
|
+
index.add({
|
|
45
|
+
id: id++, server, tool,
|
|
46
|
+
name: normalizeSearchText(tool.name),
|
|
47
|
+
originalName: normalizeSearchText(tool.originalName),
|
|
48
|
+
description: normalizeSearchText([tool.title, tool.description].filter(Boolean).join(" ")),
|
|
49
|
+
schema: normalizeSearchText(schemaSearchText(tool.inputSchema)),
|
|
50
|
+
});
|
|
69
51
|
}
|
|
70
52
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
53
|
+
indexes.set(state, { metadata: new Map(state.toolMetadata), index });
|
|
54
|
+
return index;
|
|
55
|
+
}
|
|
56
|
+
function searchCatalog(state, query, server, options) {
|
|
57
|
+
return getIndex(state).search(normalizeSearchText(query), {
|
|
58
|
+
...options,
|
|
59
|
+
filter: result => {
|
|
60
|
+
if (server && result.server !== server)
|
|
61
|
+
return false;
|
|
62
|
+
const definition = state.config.mcpServers[result.server];
|
|
63
|
+
if (!definition || isServerDisabled(definition))
|
|
64
|
+
return false;
|
|
65
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
66
|
+
return isToolAllowed(result.tool.originalName, result.server, prefix, definition.includeTools, definition.excludeTools);
|
|
67
|
+
},
|
|
68
|
+
}).map(({ server, tool, score }) => ({ server, tool, score }))
|
|
69
|
+
.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
81
70
|
}
|
|
82
71
|
export function rankToolMatches(state, query, server) {
|
|
83
|
-
|
|
84
|
-
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
85
|
-
if (server && serverName !== server)
|
|
86
|
-
continue;
|
|
87
|
-
if (isServerDisabled(state.config.mcpServers[serverName]))
|
|
88
|
-
continue;
|
|
89
|
-
for (const tool of metadata) {
|
|
90
|
-
const score = scoreToolMatch(tool, serverName, query);
|
|
91
|
-
if (score !== null)
|
|
92
|
-
matches.push({ server: serverName, tool, score });
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return matches.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
72
|
+
return searchCatalog(state, query, server);
|
|
96
73
|
}
|
|
97
74
|
export function paginate(items, offset, limit) {
|
|
98
75
|
const safeOffset = Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0;
|
|
@@ -107,36 +84,6 @@ export function paginate(items, offset, limit) {
|
|
|
107
84
|
nextOffset: nextOffset < total ? nextOffset : null,
|
|
108
85
|
};
|
|
109
86
|
}
|
|
110
|
-
function editDistance(a, b, maxDistance) {
|
|
111
|
-
if (Math.abs(a.length - b.length) > maxDistance)
|
|
112
|
-
return maxDistance + 1;
|
|
113
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
114
|
-
for (let i = 1; i <= a.length; i++) {
|
|
115
|
-
const current = [i];
|
|
116
|
-
let rowMin = i;
|
|
117
|
-
for (let j = 1; j <= b.length; j++) {
|
|
118
|
-
current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
119
|
-
rowMin = Math.min(rowMin, current[j]);
|
|
120
|
-
}
|
|
121
|
-
if (rowMin > maxDistance)
|
|
122
|
-
return maxDistance + 1;
|
|
123
|
-
previous = current;
|
|
124
|
-
}
|
|
125
|
-
return previous[b.length];
|
|
126
|
-
}
|
|
127
|
-
function rankClosest(tools, query, limit, candidateName) {
|
|
128
|
-
const normalizedQuery = normalizeSearchText(query).replace(/\s+/g, "_");
|
|
129
|
-
const maxDistance = Math.max(2, Math.floor(normalizedQuery.length * 0.2));
|
|
130
|
-
return tools
|
|
131
|
-
.map(tool => ({
|
|
132
|
-
tool,
|
|
133
|
-
distance: editDistance(normalizedQuery, normalizeSearchText(candidateName(tool)).replace(/\s+/g, "_"), maxDistance),
|
|
134
|
-
}))
|
|
135
|
-
.filter(({ distance }) => distance <= maxDistance)
|
|
136
|
-
.sort((a, b) => a.distance - b.distance || a.tool.name.localeCompare(b.tool.name))
|
|
137
|
-
.slice(0, limit)
|
|
138
|
-
.map(({ tool }) => tool.name);
|
|
139
|
-
}
|
|
140
87
|
export function rankSuggestions(state, name, limit, server) {
|
|
141
88
|
if (name.length > MAX_TOOL_NAME_LENGTH)
|
|
142
89
|
return [];
|
|
@@ -150,31 +97,13 @@ export function rankSuggestions(state, name, limit, server) {
|
|
|
150
97
|
.filter((candidate) => Boolean(candidate.prefix) && name.startsWith(`${candidate.prefix}_`))
|
|
151
98
|
.sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
152
99
|
const matchedServer = server ?? inferred?.server;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
.filter(([candidateServer]) => !isServerDisabled(state.config.mcpServers[candidateServer]))
|
|
156
|
-
.flatMap(([, metadata]) => metadata);
|
|
157
|
-
const closest = rankClosest(tools, name, limit, tool => tool.name);
|
|
158
|
-
return closest.length > 0 ? closest : rankToolMatches(state, name).slice(0, limit).map(match => match.tool.name);
|
|
159
|
-
}
|
|
160
|
-
const definition = state.config.mcpServers[matchedServer];
|
|
161
|
-
if (!definition || isServerDisabled(definition))
|
|
100
|
+
const definition = matchedServer ? state.config.mcpServers[matchedServer] : undefined;
|
|
101
|
+
if (matchedServer && (!definition || isServerDisabled(definition)))
|
|
162
102
|
return [];
|
|
163
|
-
const prefix = inferred?.prefix ?? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix));
|
|
103
|
+
const prefix = inferred?.prefix ?? (matchedServer ? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix)) : "");
|
|
164
104
|
const query = prefix && name.startsWith(`${prefix}_`) ? name.slice(prefix.length + 1) : name;
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const queryTokens = tokenize(query);
|
|
170
|
-
const tokenMatches = rankToolMatches(state, query, matchedServer)
|
|
171
|
-
.filter(({ tool }) => {
|
|
172
|
-
const candidateTokens = tokenize(tool.originalName);
|
|
173
|
-
return queryTokens.every(token => candidateTokens.some(candidate => candidate === token
|
|
174
|
-
|| candidate.startsWith(token)
|
|
175
|
-
|| (candidate.length >= MIN_STEM_LENGTH && token.startsWith(candidate))));
|
|
176
|
-
})
|
|
177
|
-
.slice(0, limit)
|
|
178
|
-
.map(match => match.tool.name);
|
|
179
|
-
return tokenMatches.length > 0 || !server ? tokenMatches : rankSuggestions(state, name, limit);
|
|
105
|
+
const suggestions = searchCatalog(state, query, matchedServer, {
|
|
106
|
+
fields: [matchedServer ? "originalName" : "name"], combineWith: "AND", fuzzy: 2,
|
|
107
|
+
}).slice(0, limit).map(match => match.tool.name);
|
|
108
|
+
return suggestions.length > 0 || !server ? suggestions : rankSuggestions(state, name, limit);
|
|
180
109
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface McpExtensionState {
|
|
|
33
33
|
manager: McpServerManager;
|
|
34
34
|
lifecycle: McpLifecycleManager;
|
|
35
35
|
toolMetadata: Map<string, ToolMetadata[]>;
|
|
36
|
-
/**
|
|
36
|
+
/** Native resource counts, separate from callable tools. */
|
|
37
37
|
resourceCounts: Map<string, number>;
|
|
38
38
|
promptMetadata: Map<string, PromptMetadata[]>;
|
|
39
39
|
/** Servers whose prompt inventory came from successful live discovery. */
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { AgentToolResult, ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type TSchema } from "typebox";
|
|
3
|
+
import { type MetadataCache } from "./metadata-cache.ts";
|
|
4
|
+
import type { McpExtensionState } from "./state.ts";
|
|
5
|
+
import { type DirectToolSpec, type McpAdapterOptions, type McpConfig, type ToolMetadata } from "./types.ts";
|
|
6
|
+
type Reference = {
|
|
7
|
+
name: string;
|
|
8
|
+
namespace?: string;
|
|
9
|
+
};
|
|
10
|
+
type SearchDefinition<TParams extends TSchema, TDetails> = Omit<ToolDefinition<TParams, TDetails>, "execute"> & {
|
|
11
|
+
execute: (...args: Parameters<ToolDefinition<TParams, TDetails>["execute"]>) => Promise<AgentToolResult<TDetails> & {
|
|
12
|
+
tools: Reference[];
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export type SearchAPI = ExtensionAPI & {
|
|
16
|
+
registerToolSearch?: <TParams extends TSchema, TDetails>(definition: SearchDefinition<TParams, TDetails>) => void;
|
|
17
|
+
getActiveToolReferences?: () => Reference[];
|
|
18
|
+
setActiveToolReferences?: (tools: Reference[]) => void;
|
|
19
|
+
};
|
|
20
|
+
export declare function supportsNativeAsync(ctx: ExtensionContext): boolean;
|
|
21
|
+
export declare function createToolLoader(pi: SearchAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
22
|
+
native: boolean;
|
|
23
|
+
restore: (ctx: ExtensionContext) => void;
|
|
24
|
+
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, asyncSupported?: boolean, registerGateways?: (pins: DirectToolSpec[]) => void) => DirectToolSpec[];
|
|
25
|
+
activate: (matches: Array<{
|
|
26
|
+
server: string;
|
|
27
|
+
tool: ToolMetadata;
|
|
28
|
+
}>) => Reference[];
|
|
29
|
+
persist: () => void;
|
|
30
|
+
selectedServers: () => string[];
|
|
31
|
+
};
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { createDirectToolExecutor, directToolSelection } from "./direct-tools.js";
|
|
3
|
+
import { isServerCacheValid, reconstructToolMetadata } from "./metadata-cache.js";
|
|
4
|
+
import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-result-renderer.js";
|
|
5
|
+
import { isServerDisabled } from "./types.js";
|
|
6
|
+
import { normalizeDirectToolInputSchema } from "./utils.js";
|
|
7
|
+
import { isToolCallApprovalRequired } from "./tool-approval.js";
|
|
8
|
+
const ENTRY = "mcp-tool-selection";
|
|
9
|
+
const key = (value) => JSON.stringify([value.server, value.tool]);
|
|
10
|
+
const refKey = (value) => JSON.stringify([value.namespace ?? null, value.name]);
|
|
11
|
+
const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "mcp", "mcp_search", "mcp_script"]);
|
|
12
|
+
export function supportsNativeAsync(ctx) {
|
|
13
|
+
const compat = ctx.model?.compat;
|
|
14
|
+
return "getPendingToolCalls" in ctx && typeof ctx.getPendingToolCalls === "function"
|
|
15
|
+
&& !!compat && "supportsAsyncTools" in compat && compat.supportsAsyncTools === true;
|
|
16
|
+
}
|
|
17
|
+
export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
18
|
+
const native = typeof pi.registerToolSearch === "function"
|
|
19
|
+
&& typeof pi.getActiveToolReferences === "function"
|
|
20
|
+
&& typeof pi.setActiveToolReferences === "function";
|
|
21
|
+
pi.registerEntryRenderer(ENTRY, () => undefined);
|
|
22
|
+
const registered = new Map();
|
|
23
|
+
let available = new Set();
|
|
24
|
+
let selected = new Map();
|
|
25
|
+
let inactive = new Map();
|
|
26
|
+
let restoring = true;
|
|
27
|
+
const active = () => native ? pi.getActiveToolReferences() : pi.getActiveTools().map(name => ({ name }));
|
|
28
|
+
const setActive = (refs) => {
|
|
29
|
+
const unique = [...new Map(refs.map(ref => [refKey(ref), ref])).values()];
|
|
30
|
+
if (JSON.stringify(active()) === JSON.stringify(unique))
|
|
31
|
+
return;
|
|
32
|
+
if (native)
|
|
33
|
+
pi.setActiveToolReferences(unique);
|
|
34
|
+
else
|
|
35
|
+
pi.setActiveTools(unique.map(ref => ref.name));
|
|
36
|
+
};
|
|
37
|
+
function capture() {
|
|
38
|
+
const current = new Set(active().map(refKey));
|
|
39
|
+
for (const [id, { selection, ref, pinned }] of registered) {
|
|
40
|
+
if (!available.has(id))
|
|
41
|
+
continue;
|
|
42
|
+
if (current.has(refKey(ref))) {
|
|
43
|
+
if (!pinned)
|
|
44
|
+
selected.set(id, selection);
|
|
45
|
+
inactive.delete(id);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
selected.delete(id);
|
|
49
|
+
if (pinned)
|
|
50
|
+
inactive.set(id, selection);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function persist() {
|
|
55
|
+
capture();
|
|
56
|
+
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()] });
|
|
57
|
+
}
|
|
58
|
+
function restore(ctx) {
|
|
59
|
+
selected = new Map();
|
|
60
|
+
inactive = new Map();
|
|
61
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
62
|
+
if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
|
|
63
|
+
continue;
|
|
64
|
+
const data = entry.data;
|
|
65
|
+
for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]]) {
|
|
66
|
+
target.clear();
|
|
67
|
+
for (const selection of Array.isArray(field) ? field : []) {
|
|
68
|
+
if (typeof selection?.server === "string" && typeof selection.tool === "string")
|
|
69
|
+
target.set(key(selection), selection);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
restoring = true;
|
|
74
|
+
}
|
|
75
|
+
function sync(config, cache, env, asyncSupported = false, registerGateways) {
|
|
76
|
+
if (!restoring)
|
|
77
|
+
capture();
|
|
78
|
+
const previousRefs = new Set([...registered.values()].map(item => refKey(item.ref)));
|
|
79
|
+
const existing = new Set(pi.getAllTools().map(tool => refKey(tool)));
|
|
80
|
+
const specs = [];
|
|
81
|
+
for (const [server, definition] of Object.entries(env === null ? {} : config.mcpServers)) {
|
|
82
|
+
if (isServerDisabled(definition))
|
|
83
|
+
continue;
|
|
84
|
+
const entry = cache?.servers[server];
|
|
85
|
+
const metadata = getState()?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
86
|
+
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
87
|
+
const selection = directToolSelection(config, server, env ?? undefined);
|
|
88
|
+
for (const { name: prefixedName, ...tool } of metadata) {
|
|
89
|
+
if (tool.resourceUri)
|
|
90
|
+
continue;
|
|
91
|
+
const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
|
|
92
|
+
const ref = native && !pinned ? { namespace: `mcp_${server}`, name: tool.originalName } : { name: prefixedName };
|
|
93
|
+
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, ref, pinned });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const counts = new Map();
|
|
97
|
+
for (const { ref } of specs)
|
|
98
|
+
counts.set(refKey(ref), (counts.get(refKey(ref)) ?? 0) + 1);
|
|
99
|
+
available = new Set();
|
|
100
|
+
const enabled = [];
|
|
101
|
+
for (const { spec, selection, ref, pinned } of specs) {
|
|
102
|
+
const id = key(selection);
|
|
103
|
+
const refId = refKey(ref);
|
|
104
|
+
if ((!ref.namespace && reserved.has(ref.name)) || counts.get(refId) > 1 || (existing.has(refId) && !previousRefs.has(refId)))
|
|
105
|
+
continue;
|
|
106
|
+
available.add(id);
|
|
107
|
+
const asyncTool = asyncSupported && !beforeExecute && !spec.uiResourceUri && !isToolCallApprovalRequired(config, spec.serverName, spec);
|
|
108
|
+
const fingerprint = JSON.stringify({ spec, ref, asyncTool });
|
|
109
|
+
if (registered.get(id)?.fingerprint !== fingerprint) {
|
|
110
|
+
pi.registerTool({
|
|
111
|
+
...ref,
|
|
112
|
+
...(asyncTool ? { async: true } : {}),
|
|
113
|
+
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
114
|
+
label: spec.title ?? `MCP: ${spec.originalName}`,
|
|
115
|
+
description: spec.description || "(no description)",
|
|
116
|
+
parameters: Type.Unsafe(normalizeDirectToolInputSchema(spec.inputSchema)),
|
|
117
|
+
execute: createDirectToolExecutor(getState, getInitPromise, spec, beforeExecute),
|
|
118
|
+
renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
|
|
119
|
+
renderResult: renderMcpToolResult,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
registered.set(id, { selection, ref, fingerprint, pinned });
|
|
123
|
+
if (selected.has(id) || (pinned && !inactive.has(id)))
|
|
124
|
+
enabled.push(ref);
|
|
125
|
+
}
|
|
126
|
+
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
127
|
+
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(refKey(item.ref))).map(item => item.spec);
|
|
128
|
+
registerGateways?.(pins);
|
|
129
|
+
// Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
|
|
130
|
+
const ownedRefs = new Set([...previousRefs, ...[...registered.values()].map(item => refKey(item.ref))]);
|
|
131
|
+
setActive([...active().filter(ref => !ownedRefs.has(refKey(ref))), ...enabled]);
|
|
132
|
+
restoring = false;
|
|
133
|
+
return pins;
|
|
134
|
+
}
|
|
135
|
+
function activate(matches) {
|
|
136
|
+
const requested = matches.flatMap(({ server, tool }) => {
|
|
137
|
+
const id = key({ server, tool: tool.originalName });
|
|
138
|
+
const registration = available.has(id) ? registered.get(id) : undefined;
|
|
139
|
+
return registration ? [registration] : [];
|
|
140
|
+
});
|
|
141
|
+
// No await between reading and extending the current loadout: parallel searches union their selections.
|
|
142
|
+
setActive([...active(), ...requested.map(item => item.ref)]);
|
|
143
|
+
const current = new Set(active().map(refKey));
|
|
144
|
+
const tools = requested.filter(item => current.has(refKey(item.ref))).map(({ selection, ref }) => {
|
|
145
|
+
selected.set(key(selection), selection);
|
|
146
|
+
return ref;
|
|
147
|
+
});
|
|
148
|
+
persist();
|
|
149
|
+
return tools;
|
|
150
|
+
}
|
|
151
|
+
return { native, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
152
|
+
}
|
package/dist/tool-metadata.d.ts
CHANGED
|
@@ -5,5 +5,72 @@ export declare function buildToolMetadata(tools: McpTool[], resources: McpResour
|
|
|
5
5
|
failedTools: string[];
|
|
6
6
|
};
|
|
7
7
|
export declare function totalToolCount(state: McpExtensionState): number;
|
|
8
|
+
export declare function toToolDescriptor(server: string, tool: ToolMetadata): {
|
|
9
|
+
server: string;
|
|
10
|
+
path: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
_meta?: {
|
|
14
|
+
[x: string]: unknown;
|
|
15
|
+
} | undefined;
|
|
16
|
+
icons?: {
|
|
17
|
+
src: string;
|
|
18
|
+
mimeType?: string | undefined;
|
|
19
|
+
sizes?: string[] | undefined;
|
|
20
|
+
theme?: "light" | "dark" | undefined;
|
|
21
|
+
}[] | undefined;
|
|
22
|
+
title?: string | undefined;
|
|
23
|
+
inputSchema?: {
|
|
24
|
+
[x: string]: unknown;
|
|
25
|
+
type: "object";
|
|
26
|
+
properties?: {
|
|
27
|
+
[x: string]: string | number | boolean | {
|
|
28
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
29
|
+
} | (string | number | boolean | {
|
|
30
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
31
|
+
} | (string | number | boolean | {
|
|
32
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
33
|
+
} | (string | number | boolean | {
|
|
34
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
35
|
+
} | (string | number | boolean | {
|
|
36
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
37
|
+
} | (string | number | boolean | {
|
|
38
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
39
|
+
} | (string | number | boolean | {
|
|
40
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
41
|
+
} | (string | number | boolean | {
|
|
42
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null;
|
|
43
|
+
} | (string | number | boolean | {
|
|
44
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null)[] | null;
|
|
45
|
+
} | (string | number | boolean | {
|
|
46
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null)[] | null;
|
|
47
|
+
} | (string | number | boolean | {
|
|
48
|
+
[x: string]: string | number | boolean | /*elided*/ any | (string | number | boolean | /*elided*/ any | /*elided*/ any | null)[] | null;
|
|
49
|
+
} | (string | number | boolean | {
|
|
50
|
+
[x: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
|
|
51
|
+
} | /*elided*/ any | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null)[] | null;
|
|
52
|
+
} | undefined;
|
|
53
|
+
required?: string[] | undefined;
|
|
54
|
+
};
|
|
55
|
+
outputSchema?: {
|
|
56
|
+
[x: string]: unknown;
|
|
57
|
+
$schema?: string | undefined;
|
|
58
|
+
} | undefined;
|
|
59
|
+
annotations?: {
|
|
60
|
+
title?: string | undefined;
|
|
61
|
+
readOnlyHint?: boolean | undefined;
|
|
62
|
+
destructiveHint?: boolean | undefined;
|
|
63
|
+
idempotentHint?: boolean | undefined;
|
|
64
|
+
openWorldHint?: boolean | undefined;
|
|
65
|
+
} | undefined;
|
|
66
|
+
execution?: {
|
|
67
|
+
taskSupport?: "optional" | "required" | "forbidden" | undefined;
|
|
68
|
+
} | undefined;
|
|
69
|
+
};
|
|
70
|
+
export declare function catalogCoverage(state: McpExtensionState, server?: string): {
|
|
71
|
+
complete: boolean;
|
|
72
|
+
knownServers: string[];
|
|
73
|
+
unknownServers: string[];
|
|
74
|
+
};
|
|
8
75
|
export declare function findToolByName(metadata: ToolMetadata[] | undefined, toolName: string): ToolMetadata | undefined;
|
|
9
76
|
export declare function formatSchema(schema: unknown, indent?: string): string;
|