@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
package/resource-tools.ts CHANGED
@@ -1,4 +1,39 @@
1
- // resource-tools.ts - MCP resource name utilities
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
@@ -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 a staging directory, copy runtime assets
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", "app-bridge.bundle.js"];
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);
@@ -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
- export function tokenize(value: string): string[] {
35
- return normalizeSearchText(value).split(/[^a-z0-9]+/).filter(Boolean);
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
- export function scoreToolMatch(tool: ToolMetadata, server: string, query: string): number | null {
39
- const normalizedQuery = normalizeSearchText(query).trim();
40
- const queryTokens = tokenize(query);
41
- if (queryTokens.length === 0) return null;
42
-
43
- const fields = {
44
- name: normalizeSearchText(tool.name),
45
- originalName: normalizeSearchText(tool.originalName),
46
- server: normalizeSearchText(server),
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
- for (const [field, value] of Object.entries(fields) as Array<[keyof typeof FIELD_WEIGHTS, string]>) {
55
- const weight = FIELD_WEIGHTS[field];
56
- const fieldTokens = tokenize(value);
57
- if (value === normalizedQuery) {
58
- score += weight * 14;
59
- phraseMatched = true;
60
- wholeFieldExact = true;
61
- } else if (value.startsWith(normalizedQuery)) {
62
- score += weight * 9;
63
- phraseMatched = true;
64
- } else if (value.includes(normalizedQuery)) {
65
- score += weight * 6;
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
- for (const token of queryTokens) {
70
- if (fieldTokens.includes(token)) {
71
- score += weight * 4;
72
- matchedTokens.add(token);
73
- } else if (fieldTokens.some(fieldToken => fieldToken.startsWith(token) || (fieldToken.length >= MIN_STEM_LENGTH && token.startsWith(fieldToken)))) {
74
- score += weight * 2;
75
- matchedTokens.add(token);
76
- } else if (value.includes(token)) {
77
- score += weight;
78
- matchedTokens.add(token);
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
- const coverage = matchedTokens.size / queryTokens.length;
84
- if (!phraseMatched && (queryTokens.length <= 2 ? coverage !== 1 : coverage < 0.6)) return null;
85
-
86
- score += coverage === 1 ? 25 : Math.round(coverage * 10);
87
- const firstQueryToken = queryTokens[0];
88
- if (firstQueryToken !== undefined && tokenize(fields.name).includes(firstQueryToken)) score += 8;
89
- if (wholeFieldExact) score += 20;
90
- return score;
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
- const matches: RankedToolMatch[] = [];
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
- if (!matchedServer) {
169
- const tools = [...state.toolMetadata.entries()]
170
- .filter(([candidateServer]) => !isServerDisabled(state.config.mcpServers[candidateServer]))
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 tools = state.toolMetadata.get(matchedServer) ?? [];
181
- const closest = rankClosest(tools, query, limit, tool => tool.originalName);
182
- if (closest.length > 0) return closest;
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
- For multi-call MCP work, write ordinary JavaScript with loops, filtering, chaining, fan-out, or other logic between calls. Run that source with `mcp_script`; it is the primary MCP orchestration surface. For a single MCP search, describe, status check, auth action, or tool call, use `mcp` instead.
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
- Write the source naturally, then pass it as `mcp_script`'s `code` argument:
10
+ ## Discover, inspect, then call
11
11
 
12
12
  ```js
13
- const { items } = await tools.search({ query: "search issues", server: "github" });
14
- const candidate = items[0];
15
- if (!candidate) return { error: "No matching tool" };
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
- const details = await tools.describe({ path: candidate.path });
18
- if (details.error) return details;
47
+ ## Saved-result readback
19
48
 
20
- const result = await tools.call(details.path, { query: "is:open label:bug" });
21
- if (!result.ok) return result;
22
- emit({ tool: details.path, completed: true });
23
- return result.data?.structuredContent ?? result.data;
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
- ## Workflow
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
- 1. Find candidate tools with `await tools.search({ query, server?, limit?, offset? })`. `limit` defaults to 12 and is capped at 100.
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
- Calls resolve to `{ ok: true, data }` or `{ ok: false, error }`; handle failed calls instead of expecting them to stop the script. Successful `data` is the raw MCP result: tool calls usually return `{ content, structuredContent? }`, while resource reads return `{ contents }`. Prefer `structuredContent` when present, and only parse a text block when that server documents JSON text. `emit(value)` adds user-visible output before the final `return` value. `console` output is captured too.
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
- `tools` is a non-enumerable proxy: `Object.keys(tools)` throws. Always use `tools.search` for discovery. When a known flat path is a valid identifier, direct calls such as `tools.github_search_issues(args)` are supported; use bracket syntax for hyphenated names: `tools["server_tool-name"](args)`. `search`, `call`, `describe`, and promise/serialization names (`then`, `catch`, `finally`, `toJSON`, `toString`, `valueOf`) are reserved on the proxy; if a flat path collides with one, call it via `tools.call("exact-path", args)`.
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
- `tools.search` and `tools.describe` are asynchronous and must be awaited. The default script timeout is 30 seconds unless the SDK host overrides or disables it (see the registered `timeoutMs` description); explicit per-script timeouts still apply, and the worker is terminated on timeout or cancellation, including for infinite loops. Every invocation still uses normal lazy connection, authentication, output guarding, and approval gates. Raw call results are cloned into the worker so scripts can reduce them before the model-facing guard; request scoped data instead of unnecessarily large payloads. Result details contain a concise `calls` trace with every search, describe, and call operation; each entry includes its query or path, outcome, and duration.
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
- Use plain JavaScript loops and Promise utilities for composition. The sandbox has no Node, filesystem, or network globals such as `process`, `Buffer`, or `fetch`. Fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided.
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
- /** Resource counts retained separately because tool metadata includes resource tools. */
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. */