@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/resource-tools.ts
CHANGED
|
@@ -1,4 +1,39 @@
|
|
|
1
|
-
|
|
1
|
+
import type { McpExtensionState } from "./state.ts";
|
|
2
|
+
import { isServerDisabled, isToolAllowed, resolveToolPrefix, type McpOperationContext, type McpToolCallIdentity } from "./types.ts";
|
|
3
|
+
import { executeCall, prepareMcpConnection } from "./proxy-modes.ts";
|
|
4
|
+
import { paginate } from "./search-ranking.ts";
|
|
5
|
+
import { throwIfAborted } from "./abort.ts";
|
|
6
|
+
import { combineAbortSignals } from "./runtime-owner.ts";
|
|
7
|
+
|
|
8
|
+
export async function executeResourceList(state: McpExtensionState, server: string, limit = 12, offset = 0, signal?: AbortSignal) {
|
|
9
|
+
throwIfAborted(combineAbortSignals(state.owner?.signal, signal));
|
|
10
|
+
const definition = state.config.mcpServers[server];
|
|
11
|
+
if (!definition || isServerDisabled(definition)) {
|
|
12
|
+
return { content: [{ type: "text" as const, text: `Server "${server}" is ${definition ? "disabled" : "not configured"}.` }], details: { mode: "resources", server, error: definition ? "server_disabled" : "server_not_found" } };
|
|
13
|
+
}
|
|
14
|
+
if (!state.toolMetadata.has(server) || state.manager.getConnection(server)?.status === "connected") {
|
|
15
|
+
const prepared = await prepareMcpConnection(state, server, signal);
|
|
16
|
+
if (prepared.error) return prepared.error;
|
|
17
|
+
}
|
|
18
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
19
|
+
const page = paginate((state.toolMetadata.get(server) ?? [])
|
|
20
|
+
.filter(tool => tool.resourceUri !== undefined && definition.exposeResources !== false
|
|
21
|
+
&& isToolAllowed(tool.originalName, server, prefix, definition.includeTools, definition.excludeTools))
|
|
22
|
+
.map(tool => tool.resourceDescriptor ?? { uri: tool.resourceUri!, description: tool.description }), offset, limit);
|
|
23
|
+
return { content: [{ type: "text" as const, text: JSON.stringify({ server, ...page }, null, 2) }], details: { mode: "resources", server, ...page } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function executeResourceRead(
|
|
27
|
+
state: McpExtensionState,
|
|
28
|
+
server: string,
|
|
29
|
+
uri: string,
|
|
30
|
+
signal?: AbortSignal,
|
|
31
|
+
identity: McpToolCallIdentity = {},
|
|
32
|
+
beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>,
|
|
33
|
+
raw = false,
|
|
34
|
+
) {
|
|
35
|
+
return executeCall(state, uri, {}, server, undefined, signal, identity, beforeDispatch, { resourceUri: uri, raw });
|
|
36
|
+
}
|
|
2
37
|
|
|
3
38
|
export function resourceNameToToolName(name: string): string {
|
|
4
39
|
let result = name
|
package/sampling-handler.ts
CHANGED
|
@@ -65,6 +65,9 @@ export async function handleSamplingRequest(
|
|
|
65
65
|
);
|
|
66
66
|
throwIfAborted(signal);
|
|
67
67
|
|
|
68
|
+
const supportsTemperature = model.compat && "supportsTemperature" in model.compat
|
|
69
|
+
? model.compat.supportsTemperature !== false
|
|
70
|
+
: !(model.id === "gpt-6-astra" && (model.api === "openai-responses" || model.api === "openai-codex-responses"));
|
|
68
71
|
const result = await options.modelRegistry.complete(
|
|
69
72
|
model,
|
|
70
73
|
{
|
|
@@ -73,7 +76,7 @@ export async function handleSamplingRequest(
|
|
|
73
76
|
},
|
|
74
77
|
{
|
|
75
78
|
maxTokens: params.maxTokens,
|
|
76
|
-
...(params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
79
|
+
...(supportsTemperature && params.temperature !== undefined ? { temperature: params.temperature } : {}),
|
|
77
80
|
...(params.metadata !== undefined ? { metadata: params.metadata as Record<string, unknown> } : {}),
|
|
78
81
|
...(signal ? { signal } : {}),
|
|
79
82
|
},
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { build } from "esbuild";
|
|
3
|
+
|
|
4
|
+
await build({
|
|
5
|
+
stdin: {
|
|
6
|
+
contents: 'export { AppBridge, PostMessageTransport } from "@modelcontextprotocol/ext-apps/app-bridge";',
|
|
7
|
+
resolveDir: process.cwd(),
|
|
8
|
+
},
|
|
9
|
+
bundle: true,
|
|
10
|
+
platform: "browser",
|
|
11
|
+
format: "esm",
|
|
12
|
+
target: "es2022",
|
|
13
|
+
minify: true,
|
|
14
|
+
outfile: process.argv[2] ?? "app-bridge.bundle.js",
|
|
15
|
+
});
|
package/scripts/build.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* Purpose: Produce the compiled runtime files that the Pi extension manifest loads.
|
|
4
|
-
* Responsibilities: Run TypeScript emit into
|
|
4
|
+
* Responsibilities: Run TypeScript emit and bundle the browser bridge into staging, copy runtime assets
|
|
5
5
|
* that modules resolve as dist-relative siblings, then atomically swap it into dist/
|
|
6
6
|
* so a failed TypeScript emit never destroys a previously working dist.
|
|
7
7
|
* Usage: `npm run build`; also invoked by scripts/prepare.mjs during install lifecycles.
|
|
@@ -25,7 +25,7 @@ const RENAME_RETRY_MS = 50;
|
|
|
25
25
|
const tscPath = join(process.cwd(), "node_modules", "typescript", "bin", "tsc");
|
|
26
26
|
// Runtime siblings resolved relative to the compiled module directory
|
|
27
27
|
// (mcp-auth.ts, mcp-code.ts, ui-server.ts).
|
|
28
|
-
const RUNTIME_ASSETS = ["mcp-keyring-helper.cjs", "mcp-script-worker.mjs"
|
|
28
|
+
const RUNTIME_ASSETS = ["mcp-keyring-helper.cjs", "mcp-script-worker.mjs"];
|
|
29
29
|
|
|
30
30
|
async function discardStaging(path) {
|
|
31
31
|
try {
|
|
@@ -75,6 +75,7 @@ async function compileToStaging(cwd, stagingDir) {
|
|
|
75
75
|
);
|
|
76
76
|
if (stdout) process.stdout.write(stdout);
|
|
77
77
|
if (stderr) process.stderr.write(stderr);
|
|
78
|
+
await execFile(process.execPath, [join(cwd, "scripts", "build-app-bridge.mjs"), join(stagingDir, "app-bridge.bundle.js")], { cwd });
|
|
78
79
|
for (const asset of RUNTIME_ASSETS) await copyFile(join(cwd, asset), join(stagingDir, asset));
|
|
79
80
|
} catch (error) {
|
|
80
81
|
if (error?.stdout) process.stdout.write(error.stdout);
|
package/scripts/prepare.mjs
CHANGED
|
@@ -20,6 +20,7 @@ const execFile = promisify(execFileCallback);
|
|
|
20
20
|
// several @earendil-works packages expose import-only "exports" maps and throw
|
|
21
21
|
// ERR_PACKAGE_PATH_NOT_EXPORTED even when installed.
|
|
22
22
|
const REQUIRED_SOURCE_BUILD_MODULES = [
|
|
23
|
+
"esbuild",
|
|
23
24
|
"typescript",
|
|
24
25
|
"typebox",
|
|
25
26
|
"@earendil-works/pi-ai",
|
package/search-ranking.ts
CHANGED
|
@@ -1,23 +1,11 @@
|
|
|
1
|
+
import MiniSearch, { type SearchOptions } from "minisearch";
|
|
1
2
|
import type { McpExtensionState } from "./state.ts";
|
|
2
3
|
import type { ToolMetadata } from "./types.ts";
|
|
3
|
-
import { getServerPrefix, isServerDisabled, resolveToolPrefix } from "./types.ts";
|
|
4
|
+
import { getServerPrefix, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.ts";
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* Shortest field token allowed to stem-match a longer query token.
|
|
7
|
-
* Real descriptions tokenize possessives into single letters ("project's" -> ["project", "s"]),
|
|
8
|
-
* which would otherwise make every query starting with that letter a match.
|
|
9
|
-
*/
|
|
10
|
-
const MIN_STEM_LENGTH = 4;
|
|
11
6
|
export const MAX_PAGE_SIZE = 100;
|
|
12
7
|
export const MAX_TOOL_NAME_LENGTH = 512;
|
|
13
8
|
|
|
14
|
-
const FIELD_WEIGHTS = {
|
|
15
|
-
name: 12,
|
|
16
|
-
originalName: 10,
|
|
17
|
-
server: 8,
|
|
18
|
-
description: 5,
|
|
19
|
-
} as const;
|
|
20
|
-
|
|
21
9
|
export interface RankedToolMatch {
|
|
22
10
|
server: string;
|
|
23
11
|
tool: ToolMetadata;
|
|
@@ -31,76 +19,78 @@ export function normalizeSearchText(value: string): string {
|
|
|
31
19
|
.toLowerCase();
|
|
32
20
|
}
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
function schemaSearchText(schema: unknown): string {
|
|
23
|
+
if (!schema || typeof schema !== "object") return "";
|
|
24
|
+
if (Array.isArray(schema)) return schema.map(schemaSearchText).join(" ");
|
|
25
|
+
return Object.entries(schema).flatMap(([key, value]) => {
|
|
26
|
+
if ((key === "description" || key === "title" || key === "const") && typeof value === "string") return value;
|
|
27
|
+
if (key === "enum" && Array.isArray(value)) return value.filter(item => typeof item === "string");
|
|
28
|
+
if (key === "properties" && value && typeof value === "object") return [Object.keys(value).join(" "), schemaSearchText(value)];
|
|
29
|
+
return schemaSearchText(value);
|
|
30
|
+
}).join(" ");
|
|
36
31
|
}
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
description: normalizeSearchText(tool.description),
|
|
48
|
-
};
|
|
49
|
-
let score = 0;
|
|
50
|
-
let phraseMatched = false;
|
|
51
|
-
let wholeFieldExact = false;
|
|
52
|
-
const matchedTokens = new Set<string>();
|
|
33
|
+
interface SearchDocument {
|
|
34
|
+
id: number;
|
|
35
|
+
server: string;
|
|
36
|
+
tool: ToolMetadata;
|
|
37
|
+
name: string;
|
|
38
|
+
originalName: string;
|
|
39
|
+
description: string;
|
|
40
|
+
schema: string;
|
|
41
|
+
}
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
phraseMatched = true;
|
|
67
|
-
}
|
|
43
|
+
const indexes = new WeakMap<McpExtensionState, {
|
|
44
|
+
metadata: Map<string, ToolMetadata[]>;
|
|
45
|
+
index: MiniSearch<SearchDocument>;
|
|
46
|
+
}>();
|
|
47
|
+
|
|
48
|
+
function getIndex(state: McpExtensionState): MiniSearch<SearchDocument> {
|
|
49
|
+
const cached = indexes.get(state);
|
|
50
|
+
// Catalog publishers replace each server's metadata array on discovery/reconnect.
|
|
51
|
+
if (cached && cached.metadata.size === state.toolMetadata.size
|
|
52
|
+
&& [...state.toolMetadata].every(([server, metadata]) => cached.metadata.get(server) === metadata)) {
|
|
53
|
+
return cached.index;
|
|
54
|
+
}
|
|
68
55
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
const index = new MiniSearch<SearchDocument>({
|
|
57
|
+
fields: ["name", "originalName", "description", "schema"],
|
|
58
|
+
storeFields: ["server", "tool"],
|
|
59
|
+
searchOptions: { fields: ["name", "description", "schema"], boost: { name: 2, description: 1, schema: 1 }, prefix: true },
|
|
60
|
+
});
|
|
61
|
+
let id = 0;
|
|
62
|
+
for (const [server, tools] of state.toolMetadata) {
|
|
63
|
+
for (const tool of tools) {
|
|
64
|
+
if (tool.resourceUri !== undefined) continue;
|
|
65
|
+
index.add({
|
|
66
|
+
id: id++, server, tool,
|
|
67
|
+
name: normalizeSearchText(tool.name),
|
|
68
|
+
originalName: normalizeSearchText(tool.originalName),
|
|
69
|
+
description: normalizeSearchText([tool.title, tool.description].filter(Boolean).join(" ")),
|
|
70
|
+
schema: normalizeSearchText(schemaSearchText(tool.inputSchema)),
|
|
71
|
+
});
|
|
80
72
|
}
|
|
81
73
|
}
|
|
74
|
+
indexes.set(state, { metadata: new Map(state.toolMetadata), index });
|
|
75
|
+
return index;
|
|
76
|
+
}
|
|
82
77
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
function searchCatalog(state: McpExtensionState, query: string, server?: string, options?: SearchOptions): RankedToolMatch[] {
|
|
79
|
+
return getIndex(state).search(normalizeSearchText(query), {
|
|
80
|
+
...options,
|
|
81
|
+
filter: result => {
|
|
82
|
+
if (server && result.server !== server) return false;
|
|
83
|
+
const definition = state.config.mcpServers[result.server];
|
|
84
|
+
if (!definition || isServerDisabled(definition)) return false;
|
|
85
|
+
const prefix = resolveToolPrefix(definition, state.config.settings?.toolPrefix);
|
|
86
|
+
return isToolAllowed(result.tool.originalName, result.server, prefix, definition.includeTools, definition.excludeTools);
|
|
87
|
+
},
|
|
88
|
+
}).map(({ server, tool, score }) => ({ server, tool, score }))
|
|
89
|
+
.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
91
90
|
}
|
|
92
91
|
|
|
93
92
|
export function rankToolMatches(state: McpExtensionState, query: string, server?: string): RankedToolMatch[] {
|
|
94
|
-
|
|
95
|
-
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
96
|
-
if (server && serverName !== server) continue;
|
|
97
|
-
if (isServerDisabled(state.config.mcpServers[serverName])) continue;
|
|
98
|
-
for (const tool of metadata) {
|
|
99
|
-
const score = scoreToolMatch(tool, serverName, query);
|
|
100
|
-
if (score !== null) matches.push({ server: serverName, tool, score });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return matches.sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name));
|
|
93
|
+
return searchCatalog(state, query, server);
|
|
104
94
|
}
|
|
105
95
|
|
|
106
96
|
export function paginate<T>(items: T[], offset: number, limit: number): { items: T[]; total: number; hasMore: boolean; nextOffset: number | null } {
|
|
@@ -117,40 +107,6 @@ export function paginate<T>(items: T[], offset: number, limit: number): { items:
|
|
|
117
107
|
};
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
function editDistance(a: string, b: string, maxDistance: number): number {
|
|
121
|
-
if (Math.abs(a.length - b.length) > maxDistance) return maxDistance + 1;
|
|
122
|
-
let previous = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
123
|
-
for (let i = 1; i <= a.length; i++) {
|
|
124
|
-
const current = [i];
|
|
125
|
-
let rowMin = i;
|
|
126
|
-
for (let j = 1; j <= b.length; j++) {
|
|
127
|
-
current[j] = Math.min(
|
|
128
|
-
current[j - 1]! + 1,
|
|
129
|
-
previous[j]! + 1,
|
|
130
|
-
previous[j - 1]! + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
131
|
-
);
|
|
132
|
-
rowMin = Math.min(rowMin, current[j]!);
|
|
133
|
-
}
|
|
134
|
-
if (rowMin > maxDistance) return maxDistance + 1;
|
|
135
|
-
previous = current;
|
|
136
|
-
}
|
|
137
|
-
return previous[b.length]!;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function rankClosest(tools: ToolMetadata[], query: string, limit: number, candidateName: (tool: ToolMetadata) => string): string[] {
|
|
141
|
-
const normalizedQuery = normalizeSearchText(query).replace(/\s+/g, "_");
|
|
142
|
-
const maxDistance = Math.max(2, Math.floor(normalizedQuery.length * 0.2));
|
|
143
|
-
return tools
|
|
144
|
-
.map(tool => ({
|
|
145
|
-
tool,
|
|
146
|
-
distance: editDistance(normalizedQuery, normalizeSearchText(candidateName(tool)).replace(/\s+/g, "_"), maxDistance),
|
|
147
|
-
}))
|
|
148
|
-
.filter(({ distance }) => distance <= maxDistance)
|
|
149
|
-
.sort((a, b) => a.distance - b.distance || a.tool.name.localeCompare(b.tool.name))
|
|
150
|
-
.slice(0, limit)
|
|
151
|
-
.map(({ tool }) => tool.name);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
110
|
export function rankSuggestions(state: McpExtensionState, name: string, limit: number, server?: string): string[] {
|
|
155
111
|
if (name.length > MAX_TOOL_NAME_LENGTH) return [];
|
|
156
112
|
|
|
@@ -165,32 +121,12 @@ export function rankSuggestions(state: McpExtensionState, name: string, limit: n
|
|
|
165
121
|
Boolean(candidate.prefix) && name.startsWith(`${candidate.prefix}_`))
|
|
166
122
|
.sort((a, b) => b.prefix.length - a.prefix.length)[0];
|
|
167
123
|
const matchedServer = server ?? inferred?.server;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.flatMap(([, metadata]) => metadata);
|
|
172
|
-
const closest = rankClosest(tools, name, limit, tool => tool.name);
|
|
173
|
-
return closest.length > 0 ? closest : rankToolMatches(state, name).slice(0, limit).map(match => match.tool.name);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const definition = state.config.mcpServers[matchedServer];
|
|
177
|
-
if (!definition || isServerDisabled(definition)) return [];
|
|
178
|
-
const prefix = inferred?.prefix ?? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix));
|
|
124
|
+
const definition = matchedServer ? state.config.mcpServers[matchedServer] : undefined;
|
|
125
|
+
if (matchedServer && (!definition || isServerDisabled(definition))) return [];
|
|
126
|
+
const prefix = inferred?.prefix ?? (matchedServer ? getServerPrefix(matchedServer, resolveToolPrefix(definition, globalPrefix)) : "");
|
|
179
127
|
const query = prefix && name.startsWith(`${prefix}_`) ? name.slice(prefix.length + 1) : name;
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const queryTokens = tokenize(query);
|
|
185
|
-
const tokenMatches = rankToolMatches(state, query, matchedServer)
|
|
186
|
-
.filter(({ tool }) => {
|
|
187
|
-
const candidateTokens = tokenize(tool.originalName);
|
|
188
|
-
return queryTokens.every(token => candidateTokens.some(candidate =>
|
|
189
|
-
candidate === token
|
|
190
|
-
|| candidate.startsWith(token)
|
|
191
|
-
|| (candidate.length >= MIN_STEM_LENGTH && token.startsWith(candidate))));
|
|
192
|
-
})
|
|
193
|
-
.slice(0, limit)
|
|
194
|
-
.map(match => match.tool.name);
|
|
195
|
-
return tokenMatches.length > 0 || !server ? tokenMatches : rankSuggestions(state, name, limit);
|
|
128
|
+
const suggestions = searchCatalog(state, query, matchedServer, {
|
|
129
|
+
fields: [matchedServer ? "originalName" : "name"], combineWith: "AND", fuzzy: 2,
|
|
130
|
+
}).slice(0, limit).map(match => match.tool.name);
|
|
131
|
+
return suggestions.length > 0 || !server ? suggestions : rankSuggestions(state, name, limit);
|
|
196
132
|
}
|
|
@@ -1,38 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: mcp-scripting
|
|
3
|
-
description: Write mcp_script JavaScript for discovering, inspecting, and calling MCP tools.
|
|
3
|
+
description: Write mcp_script JavaScript for discovering, inspecting, and calling MCP tools, resources, and saved results.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# MCP scripting
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use `mcp_script` for MCP calls with loops, filtering, chaining, fan-out, or other logic between calls. Pass ordinary JavaScript as its `code` argument. For a single call, use an already loaded typed tool. Use `mcp_search({ query, server?, limit?, offset? })` to discover and load typed tools for the next request, or explicit `mcp` actions for gateway-only workflows. Discovery never executes a search hit.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## Discover, inspect, then call
|
|
11
11
|
|
|
12
12
|
```js
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const found = await tools.search({ query: "search issues", server: "github" });
|
|
14
|
+
if (found.error) return found;
|
|
15
|
+
emit(found.coverage);
|
|
16
|
+
for (const item of found.items) {
|
|
17
|
+
emit(await tools.describe({ path: item.path, server: item.server }));
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Read the returned descriptors and choose the tool that matches the task. Do not call the first ranked hit without checking its identity and schema. In a subsequent script, use its exact returned path and schema-correct arguments:
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
const results = [];
|
|
25
|
+
for (const query of ["is:open label:bug", "is:open label:docs"]) {
|
|
26
|
+
const result = await tools.call("github_search_issues", { query });
|
|
27
|
+
if (!result.ok) return result;
|
|
28
|
+
results.push({ query, data: result.data });
|
|
29
|
+
}
|
|
30
|
+
return results;
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## API and return shapes
|
|
34
|
+
|
|
35
|
+
- `await tools.search({ query, server?, limit?, offset? })` returns `{ items, total, hasMore, nextOffset, coverage }`. Items carry `path`, `name`, `server`, optional `description`, and `score`. Discovery failures return `{ error, coverage }`. Script search defaults to **12** items, maximum **100**, with zero-based item offsets. The separate `mcp_search` loader defaults to **5**.
|
|
36
|
+
- `coverage` is `{ complete, knownServers, unknownServers }`. Global search covers known catalogs without starting every lazy server. Select an uncached server to discover it. Script search does not activate typed tools.
|
|
37
|
+
- `await tools.describe({ path, server? })` returns the complete tool descriptor with `path`, `server`, original `name`, and all server-supplied fields, including `inputSchema`, `outputSchema`, `title`, `annotations`, and `_meta` when present. It never substitutes `inputTypeScript`. Failures return `{ path, error: { code, message, suggestions? } }`. Supply `server` to disambiguate a name.
|
|
38
|
+
- `await tools.call(path, args, server?)` returns `{ ok: true, data, resultRef? }` or `{ ok: false, error: { code, message, ... }, data?, resultRef? }`. `data` is the **raw MCP result**, including raw error-result data when available. Tools usually return `{ content, structuredContent?, isError?, ... }`; resource reads return `{ contents, ... }`.
|
|
39
|
+
- If a flat path is ambiguous, call `tools.call(descriptor.name, args, descriptor.server)` with the exact original name and server returned by describe.
|
|
40
|
+
- `await tools.resources({ server, limit?, offset? })` returns `{ mode: "resources", server, items, total, hasMore, nextOffset }`, or `{ error: { code, message } }`. Items preserve resource descriptors, including the exact `uri`, name, title, MIME type, description, icons, and `_meta` when supplied; pages default to 12, maximum 100.
|
|
41
|
+
- `await tools.readResource({ server, uri })` returns the same call envelope as `tools.call`, with raw resource contents in `data.contents`. Resources are separate from typed functions. Legacy `read_<resource>` call aliases remain compatible, but prefer this URI-based API.
|
|
42
|
+
- `await tools.readResult({ ref, path?, fields?, offset?, limit? })` returns `{ content, details }`, not a call envelope. `content` contains readback text; `details` contains `ref`, `offset`, `nextOffset`, `totalCharacters`, and the selected `path` when supplied. Failures contain `details.error` and `details.message`.
|
|
43
|
+
- `emit(value)` adds model-visible output before the final `return`; `console` output is captured too.
|
|
44
|
+
|
|
45
|
+
Treat `structuredContent` as structured data even alongside text or image blocks. Only parse a text block as JSON when the server documents that format. Handle `ok: false`: ordinary failed calls do not stop a script automatically. A call-capture failure stops dependent work. An ambiguous outcome requires provider readback before considering another write; never blindly repeat a call or rerun its whole script.
|
|
16
46
|
|
|
17
|
-
|
|
18
|
-
if (details.error) return details;
|
|
47
|
+
## Saved-result readback
|
|
19
48
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
49
|
+
Structured, media, resource, oversized, and extra-field results retain a raw artifact with a model-visible result reference. Small plain-text-only results stay inline. To inspect an existing reference without another MCP call:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
const page = await tools.readResult({
|
|
53
|
+
ref: "/returned/result/path",
|
|
54
|
+
path: "/structuredContent/rows",
|
|
55
|
+
fields: ["id", "title"],
|
|
56
|
+
offset: 0,
|
|
57
|
+
limit: 12000,
|
|
58
|
+
});
|
|
59
|
+
return page;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`path` is an RFC 6901 JSON Pointer. `fields` retains immediate keys on the selected object or every object in a selected array. Selection precedes paging. `offset` and `limit` are **zero-based character positions in the selected, pretty-printed JSON**, not rows, lines, or bytes. The default limit is 12,000 characters; output byte/line caps still apply. Continue with `details.nextOffset` until it is `null`. References must identify existing adapter artifacts in the host's output directory. Readback never re-calls the server and is not a general filesystem API.
|
|
63
|
+
|
|
64
|
+
Gateway equivalents require explicit actions:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
mcp({ action: "search", query: "search issues", server: "github" })
|
|
68
|
+
mcp({ action: "describe", tool: "github_search_issues" })
|
|
69
|
+
mcp({ action: "call", tool: "github_search_issues", args: { query: "is:open" } })
|
|
70
|
+
mcp({ action: "resources", server: "docs" })
|
|
71
|
+
mcp({ action: "read-resource", server: "docs", uri: "docs://guide" })
|
|
72
|
+
mcp({ action: "read-result", ref: "/returned/result/path", path: "/structuredContent" })
|
|
24
73
|
```
|
|
25
74
|
|
|
26
|
-
|
|
75
|
+
Gateway search returns schemas without activating functions; use it when only `mcp` is allowed. New gateway calls use object `args`. Optional v5 mode fields and JSON-string arguments are only a stored-call ingress compatibility path.
|
|
27
76
|
|
|
28
|
-
|
|
29
|
-
2. Inspect the exact returned path with `await tools.describe({ path })`. Arguments are described by `inputTypeScript` when representable, otherwise by the original JSON Schema in `inputSchema`; only one is included.
|
|
30
|
-
3. Call it with `tools.call(path, args)`.
|
|
77
|
+
## Execution rules
|
|
31
78
|
|
|
32
|
-
|
|
79
|
+
`tools` is a non-enumerable proxy: `Object.keys(tools)` throws. Use `tools.search` for discovery. Known flat paths support `tools.github_search_issues(args)` or `tools["server_tool-name"](args)`. Proxy API and promise/serialization names are reserved (`search`, `describe`, `call`, `resources`, `listResources`, `readResource`, `readResult`, `then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`); use `tools.call("exact-path", args)` for collisions.
|
|
33
80
|
|
|
34
|
-
|
|
81
|
+
Await every operation, including all promises started with `Promise.all`, before returning. Calls use the shared authentication, approval, filtering, cancellation, and capture path. Resources honor `exposeResources` and include/exclude policy. Raw results are cloned into the worker before model-facing rendering; reduce them before emitting to avoid unnecessary context. Result details include a concise search/describe/call trace and saved-result references.
|
|
35
82
|
|
|
36
|
-
|
|
83
|
+
The default overall script timeout is 30 seconds unless the host overrides or disables it; an explicit per-script `timeoutMs` wins. Individual MCP request deadlines still apply. The local worker terminates on timeout or cancellation, including infinite loops. This is trusted agent-authored local orchestration, not provider-native programmatic tool calling or an isolation boundary; Codex native PTC is not implied.
|
|
37
84
|
|
|
38
|
-
|
|
85
|
+
The worker exposes no Node, filesystem, or network globals such as `process`, `Buffer`, or `fetch`. Use normal JavaScript loops and Promise utilities; fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` do not exist. Run Pi in an isolated environment if you need isolation.
|
package/state.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface McpExtensionState {
|
|
|
34
34
|
manager: McpServerManager;
|
|
35
35
|
lifecycle: McpLifecycleManager;
|
|
36
36
|
toolMetadata: Map<string, ToolMetadata[]>;
|
|
37
|
-
/**
|
|
37
|
+
/** Native resource counts, separate from callable tools. */
|
|
38
38
|
resourceCounts: Map<string, number>;
|
|
39
39
|
promptMetadata: Map<string, PromptMetadata[]>;
|
|
40
40
|
/** Servers whose prompt inventory came from successful live discovery. */
|