@fitchmultz/pi-mcp-adapter 5.0.0 → 7.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 (129) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/OAUTH.md +3 -3
  3. package/README.md +160 -88
  4. package/dist/app-bridge.bundle.js +75 -33
  5. package/dist/commands.js +9 -6
  6. package/dist/config.js +41 -16
  7. package/dist/direct-tools.d.ts +2 -4
  8. package/dist/direct-tools.js +21 -319
  9. package/dist/gateway-arguments.d.ts +33 -0
  10. package/dist/gateway-arguments.js +63 -0
  11. package/dist/index.js +181 -276
  12. package/dist/init.js +6 -20
  13. package/dist/lifecycle.d.ts +2 -2
  14. package/dist/lifecycle.js +2 -2
  15. package/dist/mcp-auth-flow.js +2 -2
  16. package/dist/mcp-auth.d.ts +1 -1
  17. package/dist/mcp-auth.js +3 -3
  18. package/dist/mcp-code.d.ts +7 -6
  19. package/dist/mcp-code.js +98 -51
  20. package/dist/mcp-oauth-provider.d.ts +5 -5
  21. package/dist/mcp-oauth-provider.js +5 -5
  22. package/dist/mcp-output-guard.d.ts +57 -2
  23. package/dist/mcp-output-guard.js +129 -24
  24. package/dist/mcp-panel.d.ts +3 -3
  25. package/dist/mcp-panel.js +36 -99
  26. package/dist/mcp-script-worker.mjs +5 -2
  27. package/dist/mcp-setup-panel.d.ts +5 -4
  28. package/dist/mcp-setup-panel.js +15 -7
  29. package/dist/mcp-status.js +2 -1
  30. package/dist/mcp-trace.d.ts +1 -1
  31. package/dist/mcp-trace.js +1 -1
  32. package/dist/metadata-cache.d.ts +1 -1
  33. package/dist/metadata-cache.js +25 -88
  34. package/dist/npx-resolver.js +8 -4
  35. package/dist/prompts.d.ts +1 -1
  36. package/dist/prompts.js +1 -1
  37. package/dist/proxy-modes.d.ts +41 -7
  38. package/dist/proxy-modes.js +275 -490
  39. package/dist/resource-tools.d.ts +15 -0
  40. package/dist/resource-tools.js +26 -1
  41. package/dist/sampling-handler.js +5 -1
  42. package/dist/search-ranking.d.ts +0 -2
  43. package/dist/search-ranking.js +70 -137
  44. package/dist/server-manager.d.ts +2 -2
  45. package/dist/server-manager.js +44 -27
  46. package/dist/session-recovery.d.ts +7 -4
  47. package/dist/session-recovery.js +76 -10
  48. package/dist/state.d.ts +1 -1
  49. package/dist/tool-loader.d.ts +32 -0
  50. package/dist/tool-loader.js +152 -0
  51. package/dist/tool-metadata.d.ts +67 -0
  52. package/dist/tool-metadata.js +23 -17
  53. package/dist/tool-registrar.d.ts +4 -4
  54. package/dist/tool-registrar.js +18 -13
  55. package/dist/tool-result-renderer.d.ts +6 -3
  56. package/dist/tool-result-renderer.js +11 -6
  57. package/dist/types.d.ts +11 -36
  58. package/dist/ui-resource-handler.d.ts +2 -2
  59. package/dist/ui-resource-handler.js +2 -2
  60. package/dist/ui-server.js +4 -3
  61. package/dist/ui-stream-types.d.ts +8 -8
  62. package/dist/unix-socket-transport.d.ts +1 -1
  63. package/dist/unix-socket-transport.js +1 -1
  64. package/dist/utils.d.ts +1 -0
  65. package/dist/utils.js +16 -4
  66. package/package.json +37 -96
  67. package/skills/mcp-scripting/SKILL.md +67 -20
  68. package/abort.ts +0 -34
  69. package/agent-dir.ts +0 -24
  70. package/app-bridge.bundle.js +0 -67
  71. package/banner.png +0 -0
  72. package/checkpoint.ts +0 -55
  73. package/commands.ts +0 -619
  74. package/config.ts +0 -1204
  75. package/consent-manager.ts +0 -25
  76. package/direct-tools.ts +0 -408
  77. package/dist/ts-shape.d.ts +0 -2
  78. package/dist/ts-shape.js +0 -147
  79. package/elicitation-handler.ts +0 -349
  80. package/error-signal.ts +0 -21
  81. package/glimpse-ui.ts +0 -80
  82. package/host-html-template.ts +0 -456
  83. package/index.ts +0 -900
  84. package/init.ts +0 -627
  85. package/legacy-migration.ts +0 -47
  86. package/lifecycle.ts +0 -165
  87. package/logger.ts +0 -127
  88. package/mcp-auth-flow.ts +0 -975
  89. package/mcp-auth.ts +0 -793
  90. package/mcp-callback-server.ts +0 -450
  91. package/mcp-code.ts +0 -386
  92. package/mcp-keyring-helper.cjs +0 -89
  93. package/mcp-oauth-provider.ts +0 -774
  94. package/mcp-output-guard.ts +0 -412
  95. package/mcp-panel.ts +0 -978
  96. package/mcp-script-worker.mjs +0 -128
  97. package/mcp-setup-panel.ts +0 -666
  98. package/mcp-status.ts +0 -109
  99. package/mcp-trace.ts +0 -295
  100. package/metadata-cache.ts +0 -333
  101. package/npx-resolver.ts +0 -488
  102. package/onboarding-state.ts +0 -58
  103. package/panel-keys.ts +0 -37
  104. package/prompts.ts +0 -337
  105. package/proxy-modes.ts +0 -1361
  106. package/resource-tools.ts +0 -17
  107. package/runtime-owner.ts +0 -123
  108. package/sampling-handler.ts +0 -273
  109. package/scripts/build.mjs +0 -131
  110. package/scripts/prepare.mjs +0 -102
  111. package/search-ranking.ts +0 -196
  112. package/server-manager.ts +0 -1320
  113. package/session-recovery.ts +0 -316
  114. package/state.ts +0 -64
  115. package/tool-approval.ts +0 -78
  116. package/tool-metadata.ts +0 -235
  117. package/tool-registrar.ts +0 -69
  118. package/tool-result-renderer.ts +0 -286
  119. package/ts-shape.ts +0 -145
  120. package/tsconfig.build.json +0 -13
  121. package/tsconfig.json +0 -16
  122. package/types.ts +0 -810
  123. package/ui-resource-handler.ts +0 -249
  124. package/ui-server.ts +0 -808
  125. package/ui-session.ts +0 -568
  126. package/ui-stream-types.ts +0 -89
  127. package/ui-tool-visibility.ts +0 -25
  128. package/unix-socket-transport.ts +0 -85
  129. package/utils.ts +0 -309
@@ -21,7 +21,7 @@
21
21
  // - treat generic -32000/ConnectionClosed errors as session expiry
22
22
  // - treat AbortError/cancellation as a session failure
23
23
  import { AsyncLocalStorage } from "node:async_hooks";
24
- import { SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
24
+ import { SdkError, SdkHttpError, SdkErrorCode, ProtocolError, InsufficientScopeError, UnauthorizedError } from "@modelcontextprotocol/client";
25
25
  import { logger } from "./logger.js";
26
26
  import { abortable, throwIfAborted } from "./abort.js";
27
27
  import { isServerDisabled, isNonInteractiveOAuth } from "./types.js";
@@ -30,18 +30,62 @@ const toolTransportFailures = new WeakSet();
30
30
  export function isToolTransportFailure(error) {
31
31
  return error instanceof Error && toolTransportFailures.has(error);
32
32
  }
33
+ const interruptedToolCalls = new WeakSet();
34
+ export function isInterruptedToolCall(error) {
35
+ return error instanceof Error && interruptedToolCalls.has(error);
36
+ }
37
+ const toolCallOutcome = new AsyncLocalStorage();
38
+ /** Interrupted outcome reporting is separate from the transport retry policy. */
39
+ export async function trackToolCallOutcome(fn, signal) {
40
+ const outcome = { pending: false, cleanup: new Set() };
41
+ return toolCallOutcome.run(outcome, async () => {
42
+ try {
43
+ return await fn();
44
+ }
45
+ catch (error) {
46
+ if (outcome.pending && error instanceof Error
47
+ && (signal?.aborted || (error instanceof SdkError
48
+ && (error.code === SdkErrorCode.RequestTimeout || error.code === SdkErrorCode.ConnectionClosed)))) {
49
+ // Abort reasons can be shared by calls that never reached dispatch.
50
+ const interrupted = new Error(error.message, { cause: error });
51
+ interruptedToolCalls.add(interrupted);
52
+ throw interrupted;
53
+ }
54
+ throw error;
55
+ }
56
+ finally {
57
+ for (const cleanup of outcome.cleanup)
58
+ cleanup();
59
+ }
60
+ });
61
+ }
33
62
  const toolSend = new AsyncLocalStorage();
34
- /** Retain fetch/body-error origin without parsing or buffering the SSE response. */
63
+ /** Retain fetch/body-error origin without parsing or buffering the response. */
35
64
  export const trackToolTransportFailure = async (input, init) => {
36
65
  const pending = toolSend.getStore();
37
66
  if (pending)
38
67
  pending.bodyFailure = undefined;
68
+ let toolPost = false;
69
+ if (pending && init?.method === "POST" && typeof init.body === "string") {
70
+ try {
71
+ const request = JSON.parse(init.body);
72
+ toolPost = request.method === "tools/call" && request.id === pending.id;
73
+ }
74
+ catch { }
75
+ }
76
+ if (toolPost && pending?.outcome)
77
+ pending.outcome.pending = true;
39
78
  try {
40
79
  const response = await fetch(input, init);
41
- if (!pending || !response.ok || !response.body
42
- || response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "text/event-stream")
80
+ if (toolPost && !response.ok && response.status < 500 && pending?.outcome)
81
+ pending.outcome.pending = false;
82
+ const mediaType = response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
83
+ const observeBody = response.ok
84
+ ? mediaType === "text/event-stream" || (toolPost && mediaType === "application/json")
85
+ : toolPost && response.status >= 500;
86
+ if (!pending || !response.body || !observeBody)
43
87
  return response;
44
- pending.streaming = true;
88
+ pending.streaming = response.ok && mediaType === "text/event-stream";
45
89
  // The native end hook has no error argument. Only a failed body read proves loss.
46
90
  const reader = response.body.getReader();
47
91
  return new Response(new ReadableStream({
@@ -59,8 +103,11 @@ export const trackToolTransportFailure = async (input, init) => {
59
103
  // Native intentional closure suppresses onRequestStreamEnd.
60
104
  if (init?.signal?.aborted)
61
105
  pending.finishSend();
62
- else if (error instanceof Error)
106
+ else if (error instanceof Error) {
63
107
  pending.bodyFailure = error;
108
+ if (!pending.streaming)
109
+ interruptedToolCalls.add(error);
110
+ }
64
111
  controller.error(error);
65
112
  reader.releaseLock();
66
113
  }
@@ -77,12 +124,16 @@ export const trackToolTransportFailure = async (input, init) => {
77
124
  }
78
125
  };
79
126
  /** Observe only the originating tool send, not OAuth or catalog requests. */
80
- export function trackToolHttpFailures(transport) {
127
+ export function trackToolTransport(transport, http = false) {
128
+ const requests = new Map();
81
129
  const onmessage = transport.onmessage;
82
130
  transport.onmessage = (message, extra) => {
83
- const pending = toolSend.getStore();
131
+ const pending = ("id" in message && message.id !== undefined ? requests.get(message.id) : undefined) ?? toolSend.getStore();
84
132
  if (pending && "id" in message && message.id === pending.id && ("result" in message || "error" in message)) {
85
133
  pending.responseReceived = true;
134
+ if (pending.outcome)
135
+ pending.outcome.pending = false;
136
+ requests.delete(pending.id);
86
137
  pending.finishSend();
87
138
  }
88
139
  onmessage?.(message, extra);
@@ -98,15 +149,24 @@ export function trackToolHttpFailures(transport) {
98
149
  const send = transport.send.bind(transport);
99
150
  transport.send = async (message, options) => {
100
151
  try {
101
- if (!("method" in message && message.method === "tools/call" && "id" in message && options?.requestSignal)) {
152
+ if (!("method" in message && message.method === "tools/call" && "id" in message)) {
102
153
  return await toolSend.run(undefined, () => send(message, options));
103
154
  }
104
155
  const pending = {
105
- id: message.id, streaming: false, responseReceived: false,
156
+ id: message.id, outcome: toolCallOutcome.getStore(), streaming: false, responseReceived: false,
106
157
  bodyFailure: undefined, otherFailure: undefined,
107
158
  finishSend: () => { },
108
159
  };
160
+ if (pending.outcome) {
161
+ requests.set(message.id, pending);
162
+ pending.outcome.cleanup.add(() => requests.delete(message.id));
163
+ if (!http)
164
+ pending.outcome.pending = true;
165
+ }
109
166
  try {
167
+ if (!options?.requestSignal) {
168
+ return await toolSend.run(pending, () => send(message, options));
169
+ }
110
170
  await abortable(new Promise((resolve, reject) => {
111
171
  pending.finishSend = resolve;
112
172
  void toolSend.run(pending, () => send(message, {
@@ -126,6 +186,12 @@ export function trackToolHttpFailures(transport) {
126
186
  resolve(); }, reject);
127
187
  }), options.requestSignal);
128
188
  }
189
+ catch (error) {
190
+ // Local send failures are distinct from a sent request waiting for its reply.
191
+ if (!http && pending.outcome)
192
+ pending.outcome.pending = false;
193
+ throw error;
194
+ }
129
195
  finally {
130
196
  pending.finishSend = () => { };
131
197
  }
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
+ outputSchema?: {
10
+ [x: string]: unknown;
11
+ $schema?: string | undefined;
12
+ } | undefined;
13
+ annotations?: {
14
+ title?: string | undefined;
15
+ readOnlyHint?: boolean | undefined;
16
+ destructiveHint?: boolean | undefined;
17
+ idempotentHint?: boolean | undefined;
18
+ openWorldHint?: boolean | undefined;
19
+ } | undefined;
20
+ execution?: {
21
+ taskSupport?: "forbidden" | "optional" | "required" | undefined;
22
+ } | undefined;
23
+ _meta?: {
24
+ [x: string]: unknown;
25
+ } | undefined;
26
+ icons?: {
27
+ src: string;
28
+ mimeType?: string | undefined;
29
+ sizes?: string[] | undefined;
30
+ theme?: "dark" | "light" | undefined;
31
+ }[] | undefined;
32
+ title?: string | undefined;
33
+ inputSchema?: {
34
+ [x: string]: unknown;
35
+ type: "object";
36
+ properties?: {
37
+ [x: string]: string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | (string | number | boolean | /*elided*/ any | {
38
+ [x: string]: string | number | boolean | /*elided*/ any | /*elided*/ any | null;
39
+ } | null)[] | {
40
+ [x: string]: string | number | boolean | any | any | null;
41
+ } | null)[] | {
42
+ [x: string]: string | number | boolean | any | any | null;
43
+ } | null)[] | {
44
+ [x: string]: string | number | boolean | any | any | null;
45
+ } | null)[] | {
46
+ [x: string]: string | number | boolean | any | any | null;
47
+ } | null)[] | {
48
+ [x: string]: string | number | boolean | any | any | null;
49
+ } | null)[] | {
50
+ [x: string]: string | number | boolean | any | any | null;
51
+ } | null)[] | {
52
+ [x: string]: string | number | boolean | any | any | null;
53
+ } | null)[] | {
54
+ [x: string]: string | number | boolean | any | any | null;
55
+ } | null)[] | {
56
+ [x: string]: string | number | boolean | any | any | null;
57
+ } | null)[] | {
58
+ [x: string]: string | number | boolean | any | any | null;
59
+ } | null)[] | {
60
+ [x: string]: string | number | boolean | any | any | null;
61
+ } | null;
62
+ } | undefined;
63
+ required?: string[] | undefined;
64
+ };
65
+ description: string;
66
+ server: string;
67
+ path: string;
68
+ name: string;
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;
@@ -1,12 +1,13 @@
1
1
  import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
2
- import { formatToolName, isToolAllowed, resolveToolPrefix } from "./types.js";
2
+ import { formatToolName, isServerDisabled, isToolAllowed, resolveToolPrefix } from "./types.js";
3
3
  import { resourceNameToToolName } from "./resource-tools.js";
4
4
  import { extractToolUiStreamMode } from "./utils.js";
5
5
  import { extractUiToolVisibility, isUiToolVisibleToModel } from "./ui-tool-visibility.js";
6
6
  export function buildToolMetadata(tools, resources, definition, serverName, prefix) {
7
7
  const metadata = [];
8
8
  const failedTools = [];
9
- const seenNames = new Set();
9
+ if (isServerDisabled(definition))
10
+ return { metadata, failedTools };
10
11
  const effectivePrefix = resolveToolPrefix(definition, prefix);
11
12
  for (const tool of tools) {
12
13
  if (!tool?.name) {
@@ -17,14 +18,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
17
18
  continue;
18
19
  }
19
20
  const name = formatToolName(tool.name, serverName, effectivePrefix);
20
- if (seenNames.has(name)) {
21
- continue;
22
- }
23
21
  const uiVisibility = extractUiToolVisibility(tool._meta);
24
22
  if (!isUiToolVisibleToModel(uiVisibility)) {
25
23
  continue;
26
24
  }
27
- seenNames.add(name);
28
25
  let uiResourceUri;
29
26
  try {
30
27
  uiResourceUri = getToolUiResourceUri({ _meta: tool._meta });
@@ -34,11 +31,10 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
34
31
  }
35
32
  const uiStreamMode = extractToolUiStreamMode(tool._meta);
36
33
  metadata.push({
34
+ ...tool,
37
35
  name,
38
36
  originalName: tool.name,
39
37
  description: tool.description ?? "",
40
- ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
41
- ...(tool.annotations !== undefined ? { annotations: tool.annotations } : {}),
42
38
  ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
43
39
  ...(uiVisibility !== undefined ? { uiVisibility } : {}),
44
40
  ...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
@@ -46,20 +42,19 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
46
42
  }
47
43
  if (definition.exposeResources !== false) {
48
44
  for (const resource of resources) {
45
+ if (!resource?.name || !resource?.uri)
46
+ continue;
49
47
  const baseName = `read_${resourceNameToToolName(resource.name)}`;
50
48
  if (!isToolAllowed(baseName, serverName, effectivePrefix, definition.includeTools, definition.excludeTools)) {
51
49
  continue;
52
50
  }
53
51
  const name = formatToolName(baseName, serverName, effectivePrefix);
54
- if (seenNames.has(name)) {
55
- continue;
56
- }
57
- seenNames.add(name);
58
52
  metadata.push({
59
53
  name,
60
54
  originalName: baseName,
61
55
  description: resource.description ?? `Read resource: ${resource.uri}`,
62
56
  resourceUri: resource.uri,
57
+ resourceDescriptor: resource,
63
58
  });
64
59
  }
65
60
  }
@@ -68,18 +63,29 @@ export function buildToolMetadata(tools, resources, definition, serverName, pref
68
63
  export function totalToolCount(state) {
69
64
  let count = 0;
70
65
  for (const metadata of state.toolMetadata.values()) {
71
- count += metadata.length;
66
+ count += metadata.filter(tool => tool.resourceUri === undefined).length;
72
67
  }
73
68
  return count;
74
69
  }
70
+ export function toToolDescriptor(server, tool) {
71
+ const { name: path, originalName: name, resourceUri: _resourceUri, resourceDescriptor: _resourceDescriptor, uiResourceUri: _uiResourceUri, uiVisibility: _uiVisibility, uiStreamMode: _uiStreamMode, ...descriptor } = tool;
72
+ return { ...descriptor, server, path, name };
73
+ }
74
+ export function catalogCoverage(state, server) {
75
+ const servers = Object.keys(state.config.mcpServers).filter(name => (!server || name === server) && !isServerDisabled(state.config.mcpServers[name]));
76
+ const knownServers = servers.filter(name => state.toolMetadata.has(name));
77
+ const unknownServers = servers.filter(name => !state.toolMetadata.has(name));
78
+ return { complete: unknownServers.length === 0, knownServers, unknownServers };
79
+ }
75
80
  export function findToolByName(metadata, toolName) {
76
81
  if (!metadata)
77
82
  return undefined;
78
- const exact = metadata.find(m => m.name === toolName);
79
- if (exact)
80
- return exact;
83
+ const exact = metadata.filter(m => m.name === toolName);
84
+ if (exact.length > 0)
85
+ return exact.length === 1 ? exact[0] : undefined;
81
86
  const normalized = toolName.replace(/-/g, "_");
82
- return metadata.find(m => m.name.replace(/-/g, "_") === normalized);
87
+ const matches = metadata.filter(m => m.name.replace(/-/g, "_") === normalized);
88
+ return matches.length === 1 ? matches[0] : undefined;
83
89
  }
84
90
  export function formatSchema(schema, indent = " ") {
85
91
  if (!schema || typeof schema !== "object" || Array.isArray(schema)) {
@@ -1,10 +1,10 @@
1
1
  import type { McpContent, ContentBlock } from "./types.ts";
2
+ import { type McpPayloadFile } from "./mcp-output-guard.ts";
2
3
  /**
3
4
  * Transform MCP content types to Pi content blocks.
4
5
  */
5
6
  export declare function transformMcpContent(content: McpContent[]): ContentBlock[];
6
- /**
7
- * Resolve a tool result's content blocks, falling back to structuredContent
8
- * when content is empty.
9
- */
7
+ /** Human content and structured content are complementary protocol fields. */
10
8
  export declare function resolveMcpResultContent(result: Record<string, unknown>): ContentBlock[];
9
+ /** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
10
+ export declare function renderMcpResultContent(result: Record<string, unknown>, payloadFiles?: McpPayloadFile[]): ContentBlock[];
@@ -1,6 +1,5 @@
1
- // tool-registrar.ts - MCP content transformation
2
- // NOTE: Tools are NOT registered with Pi - only the unified `mcp` proxy tool is registered.
3
- // This keeps the LLM context small (1 tool instead of 100s).
1
+ // MCP protocol content to model-visible content.
2
+ import { formatMcpPayloadFile } from "./mcp-output-guard.js";
4
3
  /**
5
4
  * Transform MCP content types to Pi content blocks.
6
5
  */
@@ -18,7 +17,7 @@ export function transformMcpContent(content) {
18
17
  }
19
18
  if (c.type === "resource") {
20
19
  const resourceUri = c.resource?.uri ?? "(no URI)";
21
- const resourceContent = c.resource?.text ?? (c.resource ? JSON.stringify(c.resource) : "(no content)");
20
+ const resourceContent = c.resource?.text ?? (c.resource?.blob !== undefined ? "[Binary content; not rendered]" : "(no content)");
22
21
  return {
23
22
  type: "text",
24
23
  text: `[Resource: ${resourceUri}]\n${resourceContent}`,
@@ -35,24 +34,30 @@ export function transformMcpContent(content) {
35
34
  if (c.type === "audio") {
36
35
  return {
37
36
  type: "text",
38
- text: `[Audio content: ${c.mimeType ?? "audio/*"}]`,
37
+ text: `[Audio content: ${c.mimeType ?? "audio/*"}; not rendered]`,
39
38
  };
40
39
  }
41
40
  return { type: "text", text: JSON.stringify(c) };
42
41
  });
43
42
  }
44
- /**
45
- * Resolve a tool result's content blocks, falling back to structuredContent
46
- * when content is empty.
47
- */
43
+ /** Human content and structured content are complementary protocol fields. */
48
44
  export function resolveMcpResultContent(result) {
49
45
  const blocks = transformMcpContent((Array.isArray(result.content) ? result.content : []));
50
- if (blocks.length > 0)
51
- return blocks;
52
46
  if (result.structuredContent !== undefined) {
53
- return [{ type: "text", text: stringifyStructuredContent(result.structuredContent) }];
47
+ blocks.push({ type: "text", text: stringifyStructuredContent(result.structuredContent) });
54
48
  }
55
- return [];
49
+ return blocks;
50
+ }
51
+ /** Persist content the model cannot consume; never imply an audio/blob payload was rendered. */
52
+ export function renderMcpResultContent(result, payloadFiles = []) {
53
+ const content = Array.isArray(result.contents)
54
+ ? result.contents.map(resource => typeof resource.text === "string" ? { type: "text", text: resource.text } : { type: "resource", resource })
55
+ : Array.isArray(result.content) ? result.content : [];
56
+ const rendered = content.map((block, index) => {
57
+ const file = payloadFiles.find(file => file.index === index);
58
+ return file ? { type: "text", text: formatMcpPayloadFile(file) } : block;
59
+ });
60
+ return resolveMcpResultContent({ ...result, content: rendered });
56
61
  }
57
62
  function stringifyStructuredContent(value) {
58
63
  try {
@@ -10,6 +10,9 @@ export interface McpProxyToolCallInput {
10
10
  connect?: string;
11
11
  describe?: string;
12
12
  search?: string;
13
+ query?: string;
14
+ uri?: string;
15
+ ref?: string;
13
16
  includeSchemas?: boolean;
14
17
  server?: string;
15
18
  action?: string;
@@ -22,15 +25,15 @@ export interface McpToolResultDisplay {
22
25
  truncated: boolean;
23
26
  }
24
27
  declare class CollapsibleText implements Component {
28
+ private readonly fullText;
29
+ private readonly footerText;
30
+ private collapsedText;
25
31
  private readonly text;
26
32
  private readonly expanded;
27
33
  private readonly maxCollapsedLines;
28
34
  private readonly ellipsis;
29
35
  private readonly expandHint;
30
36
  private readonly preTruncated;
31
- private readonly fullText;
32
- private readonly footerText;
33
- private collapsedText;
34
37
  constructor(text: string, expanded: boolean, maxCollapsedLines: number, ellipsis: string, expandHint: string, preTruncated?: boolean);
35
38
  render(width: number): string[];
36
39
  invalidate(): void;
@@ -9,15 +9,15 @@ const DEFAULT_MAX_COLLAPSED_LINES = 3;
9
9
  const DEFAULT_MAX_COLLAPSED_CHARS = 8000;
10
10
  const COLLAPSED_RENDER_CHAR_SLACK = 8;
11
11
  class CollapsibleText {
12
+ fullText;
13
+ footerText;
14
+ collapsedText = null;
12
15
  text;
13
16
  expanded;
14
17
  maxCollapsedLines;
15
18
  ellipsis;
16
19
  expandHint;
17
20
  preTruncated;
18
- fullText;
19
- footerText;
20
- collapsedText = null;
21
21
  constructor(text, expanded, maxCollapsedLines, ellipsis, expandHint, preTruncated = false) {
22
22
  this.text = text;
23
23
  this.expanded = expanded;
@@ -79,8 +79,15 @@ function hasUsefulObjectContent(value) {
79
79
  return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length > 0;
80
80
  }
81
81
  export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {
82
- if (args.action === "ui-messages")
82
+ if (args.action === "ui-messages" || args.action === "status")
83
83
  return [`mcp ${args.action}`];
84
+ if (args.action) {
85
+ const target = args.tool ?? args.query ?? args.uri ?? args.ref;
86
+ let line = `mcp ${args.action}${target ? ` ${target}` : ""}${args.server ? `${target ? " @" : ""} ${args.server}` : ""}`;
87
+ if (args.action === "search" && args.includeSchemas === false)
88
+ line += " (schemas hidden)";
89
+ return args.action === "call" && args.args ? [line, formatJsonish(args.args, maxInputChars)] : [line];
90
+ }
84
91
  if (args.tool) {
85
92
  const target = args.server ? `${args.tool} @ ${args.server}` : args.tool;
86
93
  const lines = [`mcp call ${target}`];
@@ -102,8 +109,6 @@ export function formatMcpProxyToolCallLines(args, maxInputChars = DEFAULT_MAX_CA
102
109
  }
103
110
  if (args.server)
104
111
  return [`mcp list ${args.server}`];
105
- if (args.action)
106
- return [`mcp ${args.action}`];
107
112
  return ["mcp status"];
108
113
  }
109
114
  export function formatMcpDirectToolCallLines(displayName, args, maxInputChars = DEFAULT_MAX_CALL_INPUT_CHARS) {