@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +25 -1
  2. package/OAUTH.md +3 -3
  3. package/README.md +153 -85
  4. package/app-bridge.bundle.js +75 -33
  5. package/commands.ts +9 -6
  6. package/direct-tools.ts +25 -358
  7. package/dist/app-bridge.bundle.js +75 -33
  8. package/dist/commands.js +9 -6
  9. package/dist/direct-tools.d.ts +2 -4
  10. package/dist/direct-tools.js +21 -319
  11. package/dist/gateway-arguments.d.ts +33 -0
  12. package/dist/gateway-arguments.js +63 -0
  13. package/dist/index.js +181 -276
  14. package/dist/init.js +6 -20
  15. package/dist/mcp-code.d.ts +2 -1
  16. package/dist/mcp-code.js +87 -41
  17. package/dist/mcp-output-guard.d.ts +57 -2
  18. package/dist/mcp-output-guard.js +129 -24
  19. package/dist/mcp-panel.d.ts +1 -1
  20. package/dist/mcp-panel.js +34 -97
  21. package/dist/mcp-script-worker.mjs +5 -2
  22. package/dist/mcp-status.js +2 -1
  23. package/dist/metadata-cache.d.ts +1 -1
  24. package/dist/metadata-cache.js +25 -88
  25. package/dist/prompts.d.ts +1 -1
  26. package/dist/prompts.js +1 -1
  27. package/dist/proxy-modes.d.ts +40 -6
  28. package/dist/proxy-modes.js +270 -485
  29. package/dist/resource-tools.d.ts +15 -0
  30. package/dist/resource-tools.js +26 -1
  31. package/dist/sampling-handler.js +4 -1
  32. package/dist/search-ranking.d.ts +0 -2
  33. package/dist/search-ranking.js +66 -137
  34. package/dist/state.d.ts +1 -1
  35. package/dist/tool-loader.d.ts +32 -0
  36. package/dist/tool-loader.js +152 -0
  37. package/dist/tool-metadata.d.ts +67 -0
  38. package/dist/tool-metadata.js +23 -17
  39. package/dist/tool-registrar.d.ts +4 -4
  40. package/dist/tool-registrar.js +18 -13
  41. package/dist/tool-result-renderer.d.ts +3 -0
  42. package/dist/tool-result-renderer.js +8 -3
  43. package/dist/types.d.ts +11 -36
  44. package/dist/utils.js +2 -4
  45. package/gateway-arguments.ts +73 -0
  46. package/index.ts +166 -299
  47. package/init.ts +6 -20
  48. package/mcp-code.ts +71 -45
  49. package/mcp-output-guard.ts +145 -27
  50. package/mcp-panel.ts +38 -109
  51. package/mcp-script-worker.mjs +5 -2
  52. package/mcp-status.ts +2 -1
  53. package/metadata-cache.ts +31 -93
  54. package/package.json +14 -9
  55. package/prompts.ts +1 -1
  56. package/proxy-modes.ts +265 -517
  57. package/resource-tools.ts +36 -1
  58. package/sampling-handler.ts +4 -1
  59. package/scripts/build-app-bridge.mjs +15 -0
  60. package/scripts/build.mjs +3 -2
  61. package/scripts/prepare.mjs +1 -0
  62. package/search-ranking.ts +71 -135
  63. package/skills/mcp-scripting/SKILL.md +67 -20
  64. package/state.ts +1 -1
  65. package/tool-loader.ts +153 -0
  66. package/tool-metadata.ts +22 -17
  67. package/tool-registrar.ts +18 -13
  68. package/tool-result-renderer.ts +10 -2
  69. package/types.ts +15 -40
  70. package/utils.ts +2 -4
  71. package/dist/ts-shape.d.ts +0 -2
  72. package/dist/ts-shape.js +0 -147
  73. package/ts-shape.ts +0 -145
@@ -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;
@@ -1,4 +1,29 @@
1
- // resource-tools.ts - MCP resource name utilities
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, "_")
@@ -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
  });
@@ -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[];
@@ -1,98 +1,75 @@
1
- import { getServerPrefix, isServerDisabled, resolveToolPrefix } from "./types.js";
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
- export function tokenize(value) {
23
- return normalizeSearchText(value).split(/[^a-z0-9]+/).filter(Boolean);
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
- export function scoreToolMatch(tool, server, query) {
26
- const normalizedQuery = normalizeSearchText(query).trim();
27
- const queryTokens = tokenize(query);
28
- if (queryTokens.length === 0)
29
- return null;
30
- const fields = {
31
- name: normalizeSearchText(tool.name),
32
- originalName: normalizeSearchText(tool.originalName),
33
- server: normalizeSearchText(server),
34
- description: normalizeSearchText(tool.description),
35
- };
36
- let score = 0;
37
- let phraseMatched = false;
38
- let wholeFieldExact = false;
39
- const matchedTokens = new Set();
40
- for (const [field, value] of Object.entries(fields)) {
41
- const weight = FIELD_WEIGHTS[field];
42
- const fieldTokens = tokenize(value);
43
- if (value === normalizedQuery) {
44
- score += weight * 14;
45
- phraseMatched = true;
46
- wholeFieldExact = true;
47
- }
48
- else if (value.startsWith(normalizedQuery)) {
49
- score += weight * 9;
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
- const coverage = matchedTokens.size / queryTokens.length;
72
- if (!phraseMatched && (queryTokens.length <= 2 ? coverage !== 1 : coverage < 0.6))
73
- return null;
74
- score += coverage === 1 ? 25 : Math.round(coverage * 10);
75
- const firstQueryToken = queryTokens[0];
76
- if (firstQueryToken !== undefined && tokenize(fields.name).includes(firstQueryToken))
77
- score += 8;
78
- if (wholeFieldExact)
79
- score += 20;
80
- return score;
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
- const matches = [];
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
- if (!matchedServer) {
154
- const tools = [...state.toolMetadata.entries()]
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 tools = state.toolMetadata.get(matchedServer) ?? [];
166
- const closest = rankClosest(tools, query, limit, tool => tool.originalName);
167
- if (closest.length > 0)
168
- return closest;
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
- /** Resource counts retained separately because tool metadata includes resource tools. */
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
+ }
@@ -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;