@herbertgao/pi-extensions 2026.8.1 → 2026.8.2

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 (162) hide show
  1. package/README.md +5 -2
  2. package/node_modules/@czottmann/pi-automode/README.md +16 -2
  3. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +35 -0
  4. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +18 -9
  5. package/node_modules/@czottmann/pi-automode/docs/defaults.md +6 -2
  6. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +6 -0
  7. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +11 -7
  8. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +100 -0
  9. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +32 -0
  10. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +78 -3
  11. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +16 -4
  12. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +23 -1
  13. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +22 -1
  14. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +18 -2
  15. package/node_modules/@czottmann/pi-automode/package.json +4 -4
  16. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +5 -0
  17. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +5 -0
  18. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +5 -0
  19. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +5 -0
  20. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +5 -0
  21. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +5 -0
  22. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +23 -0
  23. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +99 -0
  24. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +30 -0
  25. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +16 -0
  26. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +32 -0
  27. package/node_modules/@dietrichgebert/ponytail/LICENSE +21 -0
  28. package/node_modules/@dietrichgebert/ponytail/README.es.md +298 -0
  29. package/node_modules/@dietrichgebert/ponytail/README.ko.md +315 -0
  30. package/node_modules/@dietrichgebert/ponytail/README.md +351 -0
  31. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +21 -0
  32. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +62 -0
  33. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  34. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +115 -0
  35. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +27 -0
  36. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +27 -0
  37. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  38. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  39. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  40. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  41. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  42. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +44 -0
  43. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +21 -0
  44. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +96 -0
  45. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +169 -0
  46. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +98 -0
  47. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +130 -0
  48. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +85 -0
  49. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.ps1 +24 -0
  50. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-statusline.sh +18 -0
  51. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +77 -0
  52. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +26 -0
  53. package/node_modules/@dietrichgebert/ponytail/package.json +47 -0
  54. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +211 -0
  55. package/node_modules/@dietrichgebert/ponytail/pi-extension/package.json +8 -0
  56. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +60 -0
  57. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +120 -0
  58. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +41 -0
  59. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  60. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +50 -0
  61. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +71 -0
  62. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +57 -0
  63. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -3
  64. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -3
  65. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +10 -6
  66. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/agent-summary.ts +6 -2
  67. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +24 -83
  68. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/context.ts +1 -1
  69. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/markdown-enhance.ts +119 -0
  70. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +73 -9
  71. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/index.ts +2 -2
  72. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-hover.ts +130 -0
  73. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-interaction.ts +65 -542
  74. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-layout.ts +160 -0
  75. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-packets.ts +143 -0
  76. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse-scroll.ts +184 -0
  77. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-diff/diff-renderer.ts +2 -2
  78. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool-grouping.ts +9 -2
  79. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/transcript-refresh.ts +91 -0
  80. package/node_modules/@herbertgao/pi-cc-extensions/extensions/utils/agent-summary.ts +1 -1
  81. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  82. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-dark.json +1 -0
  83. package/node_modules/@herbertgao/pi-cc-extensions/themes/cc-light.json +2 -2
  84. package/node_modules/@herbertgao/pi-stash/README.md +9 -9
  85. package/node_modules/@herbertgao/pi-stash/package.json +1 -1
  86. package/node_modules/@herbertgao/pi-stash/src/index.ts +2 -1
  87. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +14 -0
  88. package/node_modules/@herbertgao/pi-subagents/README.md +6 -2
  89. package/node_modules/@herbertgao/pi-subagents/package.json +3 -3
  90. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +85 -11
  91. package/node_modules/@herbertgao/pi-subagents/src/index.ts +28 -4
  92. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +12 -0
  93. package/node_modules/@herbertgao/pi-subagents/src/types.ts +2 -0
  94. package/node_modules/@narumitw/pi-btw/LICENSE +21 -0
  95. package/node_modules/@narumitw/pi-btw/README.md +210 -0
  96. package/node_modules/@narumitw/pi-btw/package.json +53 -0
  97. package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +632 -0
  98. package/node_modules/@narumitw/pi-btw/src/btw.ts +996 -0
  99. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +339 -0
  100. package/node_modules/@narumitw/pi-btw/src/index.ts +1 -0
  101. package/node_modules/@narumitw/pi-btw/src/menu.ts +296 -0
  102. package/node_modules/@narumitw/pi-btw/src/settings.ts +237 -0
  103. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +238 -0
  104. package/node_modules/@narumitw/pi-btw/src/text.ts +10 -0
  105. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +620 -0
  106. package/node_modules/@pi-plugins/fast-mode/LICENSE +21 -0
  107. package/node_modules/@pi-plugins/fast-mode/README.md +92 -0
  108. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts +6 -0
  109. package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -0
  110. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +2026 -0
  111. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -0
  112. package/node_modules/@pi-plugins/fast-mode/package.json +73 -0
  113. package/node_modules/pi-mcp-adapter/CHANGELOG.md +59 -0
  114. package/node_modules/pi-mcp-adapter/README.md +62 -13
  115. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +377 -0
  116. package/node_modules/pi-mcp-adapter/commands.ts +9 -7
  117. package/node_modules/pi-mcp-adapter/config.ts +110 -49
  118. package/node_modules/pi-mcp-adapter/direct-tools.ts +34 -19
  119. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +37 -36
  120. package/node_modules/pi-mcp-adapter/errors.ts +21 -17
  121. package/node_modules/pi-mcp-adapter/host-html-template.ts +13 -7
  122. package/node_modules/pi-mcp-adapter/index.ts +37 -14
  123. package/node_modules/pi-mcp-adapter/init.ts +9 -7
  124. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +2 -2
  125. package/node_modules/pi-mcp-adapter/lifecycle.ts +8 -8
  126. package/node_modules/pi-mcp-adapter/logger.ts +1 -1
  127. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +36 -22
  128. package/node_modules/pi-mcp-adapter/mcp-auth.ts +120 -2
  129. package/node_modules/pi-mcp-adapter/mcp-code.ts +13 -11
  130. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +19 -13
  131. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +15 -11
  132. package/node_modules/pi-mcp-adapter/mcp-panel.ts +34 -15
  133. package/node_modules/pi-mcp-adapter/mcp-probe.ts +131 -34
  134. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +2 -2
  135. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +7 -4
  136. package/node_modules/pi-mcp-adapter/mcp-trace.ts +59 -64
  137. package/node_modules/pi-mcp-adapter/metadata-cache.ts +34 -26
  138. package/node_modules/pi-mcp-adapter/npx-resolver.ts +6 -4
  139. package/node_modules/pi-mcp-adapter/oauth-handler.ts +1 -1
  140. package/node_modules/pi-mcp-adapter/onboarding-state.ts +19 -11
  141. package/node_modules/pi-mcp-adapter/package.json +13 -8
  142. package/node_modules/pi-mcp-adapter/panel-keys.ts +20 -4
  143. package/node_modules/pi-mcp-adapter/prompts.ts +2 -2
  144. package/node_modules/pi-mcp-adapter/proxy-modes.ts +48 -11
  145. package/node_modules/pi-mcp-adapter/sampling-handler.ts +22 -15
  146. package/node_modules/pi-mcp-adapter/search-ranking.ts +2 -1
  147. package/node_modules/pi-mcp-adapter/server-manager.ts +165 -113
  148. package/node_modules/pi-mcp-adapter/session-recovery.ts +11 -12
  149. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +3 -3
  150. package/node_modules/pi-mcp-adapter/state.ts +3 -1
  151. package/node_modules/pi-mcp-adapter/tool-approval.ts +67 -4
  152. package/node_modules/pi-mcp-adapter/tool-metadata.ts +6 -5
  153. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +14 -2
  154. package/node_modules/pi-mcp-adapter/ts-shape.ts +9 -2
  155. package/node_modules/pi-mcp-adapter/types.ts +109 -22
  156. package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +40 -0
  157. package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +28 -19
  158. package/node_modules/pi-mcp-adapter/ui-server.ts +90 -58
  159. package/node_modules/pi-mcp-adapter/ui-session.ts +16 -14
  160. package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +4 -4
  161. package/node_modules/pi-mcp-adapter/utils.ts +23 -9
  162. package/package.json +22 -7
@@ -17,8 +17,8 @@ export interface McpUiErrorContext {
17
17
  export class McpUiError extends Error {
18
18
  readonly code: string;
19
19
  readonly context: McpUiErrorContext;
20
- readonly recoveryHint?: string;
21
- readonly cause?: Error;
20
+ readonly recoveryHint: string | undefined;
21
+ readonly cause: Error | undefined;
22
22
 
23
23
  constructor(
24
24
  message: string,
@@ -65,9 +65,9 @@ export class ResourceFetchError extends McpUiError {
65
65
  ) {
66
66
  super(`Failed to fetch UI resource "${uri}": ${reason}`, {
67
67
  code: "RESOURCE_FETCH_ERROR",
68
- context: { uri, server: options?.server },
68
+ context: { uri, ...(options?.server !== undefined ? { server: options.server } : {}) },
69
69
  recoveryHint: "Check that the MCP server is connected and the resource URI is valid.",
70
- cause: options?.cause,
70
+ ...(options?.cause !== undefined ? { cause: options.cause } : {}),
71
71
  });
72
72
  this.name = "ResourceFetchError";
73
73
  }
@@ -84,7 +84,11 @@ export class ResourceParseError extends McpUiError {
84
84
  ) {
85
85
  super(`Invalid UI resource "${uri}": ${reason}`, {
86
86
  code: "RESOURCE_PARSE_ERROR",
87
- context: { uri, server: options?.server, mimeType: options?.mimeType },
87
+ context: {
88
+ uri,
89
+ ...(options?.server !== undefined ? { server: options.server } : {}),
90
+ ...(options?.mimeType !== undefined ? { mimeType: options.mimeType } : {}),
91
+ },
88
92
  recoveryHint: "Ensure the resource returns valid HTML with the correct MIME type.",
89
93
  });
90
94
  this.name = "ResourceParseError";
@@ -98,9 +102,9 @@ export class BridgeConnectionError extends McpUiError {
98
102
  constructor(reason: string, options?: { session?: string; cause?: Error }) {
99
103
  super(`AppBridge connection failed: ${reason}`, {
100
104
  code: "BRIDGE_CONNECTION_ERROR",
101
- context: { session: options?.session },
105
+ context: options?.session !== undefined ? { session: options.session } : {},
102
106
  recoveryHint: "Check browser console for detailed errors. The iframe may have failed to load.",
103
- cause: options?.cause,
107
+ ...(options?.cause !== undefined ? { cause: options.cause } : {}),
104
108
  });
105
109
  this.name = "BridgeConnectionError";
106
110
  }
@@ -142,9 +146,9 @@ export class SessionError extends McpUiError {
142
146
  ) {
143
147
  super(`Session error: ${reason}`, {
144
148
  code: "SESSION_ERROR",
145
- context: { session: options?.session },
149
+ context: options?.session !== undefined ? { session: options.session } : {},
146
150
  recoveryHint: "The session may have expired or been closed. Try opening the UI again.",
147
- cause: options?.cause,
151
+ ...(options?.cause !== undefined ? { cause: options.cause } : {}),
148
152
  });
149
153
  this.name = "SessionError";
150
154
  }
@@ -160,9 +164,9 @@ export class ServerError extends McpUiError {
160
164
  ) {
161
165
  super(`UI server error: ${reason}`, {
162
166
  code: "SERVER_ERROR",
163
- context: { port: options?.port },
167
+ context: options?.port !== undefined ? { port: options.port } : {},
164
168
  recoveryHint: "Check if the port is available. Another process may be using it.",
165
- cause: options?.cause,
169
+ ...(options?.cause !== undefined ? { cause: options.cause } : {}),
166
170
  });
167
171
  this.name = "ServerError";
168
172
  }
@@ -179,9 +183,9 @@ export class McpServerError extends McpUiError {
179
183
  ) {
180
184
  super(`MCP server "${server}" error: ${reason}`, {
181
185
  code: "MCP_SERVER_ERROR",
182
- context: { server, tool: options?.tool },
186
+ context: { server, ...(options?.tool !== undefined ? { tool: options.tool } : {}) },
183
187
  recoveryHint: "Check that the MCP server is running and responsive.",
184
- cause: options?.cause,
188
+ ...(options?.cause !== undefined ? { cause: options.cause } : {}),
185
189
  });
186
190
  this.name = "McpServerError";
187
191
  }
@@ -196,8 +200,8 @@ export function wrapError(error: unknown, context?: McpUiErrorContext): McpUiErr
196
200
  return new McpUiError(error.message, {
197
201
  code: error.code,
198
202
  context: { ...error.context, ...context },
199
- recoveryHint: error.recoveryHint,
200
- cause: error.cause,
203
+ ...(error.recoveryHint !== undefined ? { recoveryHint: error.recoveryHint } : {}),
204
+ ...(error.cause !== undefined ? { cause: error.cause } : {}),
201
205
  });
202
206
  }
203
207
 
@@ -206,8 +210,8 @@ export function wrapError(error: unknown, context?: McpUiErrorContext): McpUiErr
206
210
 
207
211
  return new McpUiError(message, {
208
212
  code: "UNKNOWN_ERROR",
209
- context,
210
- cause,
213
+ ...(context !== undefined ? { context } : {}),
214
+ ...(cause !== undefined ? { cause } : {}),
211
215
  });
212
216
  }
213
217
 
@@ -2,9 +2,11 @@ import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts
2
2
 
3
3
  // Use locally bundled AppBridge to avoid CDN Zod bundling issues
4
4
  const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
5
+ const APP_SANDBOX = "allow-scripts allow-forms allow-modals allow-popups allow-downloads";
5
6
 
6
7
  export interface HostHtmlTemplateInput {
7
8
  sessionToken: string;
9
+ uiResourceToken: string;
8
10
  serverName: string;
9
11
  toolName: string;
10
12
  toolArgs: Record<string, unknown>;
@@ -20,6 +22,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
20
22
  const hostContext = input.hostContext ?? {};
21
23
 
22
24
  const sessionToken = safeInlineJSON(input.sessionToken);
25
+ const uiResourceToken = safeInlineJSON(input.uiResourceToken);
23
26
  const toolArgs = safeInlineJSON(input.toolArgs);
24
27
  const serverName = safeInlineJSON(input.serverName);
25
28
  const toolName = safeInlineJSON(input.toolName);
@@ -109,7 +112,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
109
112
  </div>
110
113
  </header>
111
114
  <main>
112
- <iframe id="mcp-app" sandbox="allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads" referrerpolicy="no-referrer"></iframe>
115
+ <iframe id="mcp-app" sandbox="${APP_SANDBOX}" referrerpolicy="no-referrer"></iframe>
113
116
  </main>
114
117
  <div class="overlay" id="error-overlay">
115
118
  <div class="panel">
@@ -127,6 +130,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
127
130
  import { AppBridge, PostMessageTransport } from ${moduleUrl};
128
131
 
129
132
  const SESSION_TOKEN = ${sessionToken};
133
+ const UI_RESOURCE_TOKEN = ${uiResourceToken};
130
134
  const SERVER_NAME = ${serverName};
131
135
  const TOOL_NAME = ${toolName};
132
136
  const TOOL_ARGS = ${toolArgs};
@@ -222,10 +226,10 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
222
226
  }
223
227
  const result = await post("/proxy/tools/call", params);
224
228
  // Notify agent about the tool call
225
- await post("/proxy/ui/message", {
226
- type: "intent",
227
- intent: "call_tool",
228
- params: { tool: params.name, arguments: params.arguments, isError: result.isError }
229
+ await post("/proxy/ui/generated-tool-call-intent", {
230
+ tool: params.name,
231
+ arguments: params.arguments,
232
+ isError: result.isError
229
233
  }).catch(() => {});
230
234
  return result;
231
235
  };
@@ -236,6 +240,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
236
240
  // Also listen for raw postMessage events with custom types (notify, prompt, intent, etc.)
237
241
  // These bypass the AppBridge protocol but are used by some MCP UI implementations
238
242
  window.addEventListener("message", async (event) => {
243
+ if (event.source !== iframe.contentWindow) return;
239
244
  const data = event.data;
240
245
  if (!data || typeof data !== "object") return;
241
246
 
@@ -300,7 +305,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
300
305
 
301
306
  // Connect bridge BEFORE loading iframe to ensure we're listening when the app sends ui/initialize
302
307
  try {
303
- const transport = new PostMessageTransport(iframe.contentWindow, null);
308
+ const transport = new PostMessageTransport(iframe.contentWindow, iframe.contentWindow);
304
309
  await bridge.connect(transport);
305
310
  } catch (error) {
306
311
  console.error("[host] Bridge connection failed:", error);
@@ -310,7 +315,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
310
315
  const iframeLoaded = new Promise((resolve) => {
311
316
  iframe.onload = resolve;
312
317
  });
313
- iframe.src = "/ui-app?session=" + encodeURIComponent(SESSION_TOKEN);
318
+ iframe.src = "/ui-app?resource=" + encodeURIComponent(UI_RESOURCE_TOKEN);
314
319
  await iframeLoaded;
315
320
 
316
321
  const eventSource = new EventSource("/events?session=" + encodeURIComponent(SESSION_TOKEN));
@@ -399,6 +404,7 @@ export function buildCspMetaContent(csp: UiResourceCsp | undefined): string {
399
404
 
400
405
  return [
401
406
  "default-src 'none'",
407
+ `sandbox ${APP_SANDBOX}`,
402
408
  toDirective("script-src", ["'self'", "'unsafe-inline'"], resourceDomains),
403
409
  toDirective("style-src", ["'self'", "'unsafe-inline'"], resourceDomains),
404
410
  toDirective("font-src", ["'self'"], resourceDomains),
@@ -1,8 +1,9 @@
1
- import type { ExtensionAPI, ExtensionContext, ToolInfo } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolUpdateCallback, ExtensionAPI, ExtensionContext, ToolInfo } from "@earendil-works/pi-coding-agent";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import type { DirectToolSpec, McpAdapterOptions, McpConfig, PromptMetadata } from "./types.ts";
4
4
  import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
5
5
  import { Type } from "typebox";
6
+ import type { TSchema } from "typebox";
6
7
  import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.ts";
7
8
  import { cloneMcpConfig, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.ts";
8
9
  import { buildProxyDescription, createDirectToolExecutor, getMissingConfiguredDirectToolServers, resolveDirectTools } from "./direct-tools.ts";
@@ -23,9 +24,14 @@ export type { McpAdapterOptions } from "./types.ts";
23
24
  export {
24
25
  MCP_STATUS_EVENT,
25
26
  MCP_STATUS_SNAPSHOT_VERSION,
27
+ MCP_TOOL_APPROVAL_REQUEST_EVENT,
26
28
  type McpServerRuntimeStatus,
27
29
  type McpServerStatusSnapshot,
28
30
  type McpStatusSnapshot,
31
+ type McpToolApprovalDecision,
32
+ type McpToolApprovalHandler,
33
+ type McpToolApprovalOrigin,
34
+ type McpToolApprovalRequest,
29
35
  } from "./types.ts";
30
36
 
31
37
  const INIT_WAIT_TIMEOUT_MS = 30_000;
@@ -46,6 +52,18 @@ async function awaitWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Prom
46
52
  }
47
53
  }
48
54
 
55
+ // TypeBox 1.x annotates raw objects passed to Type.Optional with an enumerable
56
+ // "~optional" key that survives serialization into provider tool schemas (Gemini
57
+ // rejects it with 400 INVALID_ARGUMENT). Prefer a real Type.Number schema; fall
58
+ // back to a plain raw schema for host TypeBox shims that omit Type.Number, since
59
+ // a property left out of `required` is optional by default.
60
+ function optionalNumber(options: { minimum?: number; description: string }): TSchema {
61
+ const number = (Type as { Number?: (opts: typeof options) => TSchema }).Number;
62
+ return typeof number === "function"
63
+ ? Type.Optional(number(options))
64
+ : ({ type: "number", ...options } as unknown as TSchema);
65
+ }
66
+
49
67
  function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
50
68
  const sessionConfig = options.config !== undefined ? cloneMcpConfig(options.config) : undefined;
51
69
  const programmaticConfig = sessionConfig !== undefined;
@@ -259,7 +277,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
259
277
  function startInitialization(ctx: ExtensionContext, owner: McpRuntimeOwner, oauthRuntime: McpOAuthRuntime, generation: number, staleReason: string): Promise<void> {
260
278
  const promise = initializeMcp(pi, ctx, owner, {
261
279
  ...(programmaticConfig || options.configPath !== undefined
262
- ? { configPath: earlyConfigPath, config: sessionConfig }
280
+ ? {
281
+ ...(earlyConfigPath !== undefined ? { configPath: earlyConfigPath } : {}),
282
+ ...(sessionConfig !== undefined ? { config: sessionConfig } : {}),
283
+ }
263
284
  : {}),
264
285
  oauthRuntime,
265
286
  statusEvents: pi.events,
@@ -427,7 +448,11 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
427
448
  }
428
449
 
429
450
  const [, subcommand, argumentPrefix] = argumentMatch;
430
- if ((subcommand !== "reconnect" && subcommand !== "logout" && subcommand !== "disable" && subcommand !== "enable") || !state) return null;
451
+ if (
452
+ (subcommand !== "reconnect" && subcommand !== "logout" && subcommand !== "disable" && subcommand !== "enable")
453
+ || argumentPrefix === undefined
454
+ || !state
455
+ ) return null;
431
456
 
432
457
  const servers = Object.keys(state.config.mcpServers)
433
458
  .filter((serverName) => serverName.startsWith(argumentPrefix.trimStart()))
@@ -609,17 +634,16 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
609
634
 
610
635
  if (earlyConfig.settings?.scriptMode !== false) {
611
636
  (pi.registerTool as (tool: unknown) => unknown)({
612
- name: "mcp_script",
637
+ name: "mcpScript",
613
638
  label: "MCP Script",
614
639
  description: "Run trusted JavaScript that makes multiple MCP tool calls in one request — loop, filter, chain, or fan out between calls. For a single MCP call, search, describe, status check, or auth action, use the mcp tool instead. Discover with await tools.search({ query }) — resolves to { items: [{ path, name, server, description? }], total, hasMore, nextOffset }, not an { ok, data } envelope. Inspect with await tools.describe({ path }) — resolves to the tool descriptor with inputTypeScript, or { path, error: { code, message, suggestions } }. Then call tools.call(path, args) — resolves to { ok: true, data } or { ok: false, error: { code, message } } — or use direct flat calls when the name is already known; use emit(value) for user-visible output. Load the mcp-scripting skill for the full workflow guide.",
615
640
  promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
616
641
  parameters: Type.Object({
617
642
  code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
618
- // Raw JSON schema: host TypeBox shims may omit Type.Number (see index-lifecycle shim test).
619
- timeoutMs: Type.Optional({ type: "number", minimum: 1, description: "Execution timeout in milliseconds (default: 30000)" } as any),
643
+ timeoutMs: optionalNumber({ minimum: 1, description: "Execution timeout in milliseconds (default: 30000)" }),
620
644
  }),
621
645
  renderResult: renderMcpToolResult,
622
- async execute(_toolCallId, params: { code: string; timeoutMs?: number }, signal) {
646
+ async execute(_toolCallId: string, params: { code: string; timeoutMs?: number }, signal: AbortSignal | undefined) {
623
647
  const executeOwner = currentOwner;
624
648
  if (!state && initPromise) {
625
649
  try {
@@ -675,14 +699,13 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
675
699
  search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
676
700
  regex: Type.Optional(Type.Boolean({ description: "Treat search as regex (default: substring match)" })),
677
701
  includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
678
- // Raw JSON schema: host TypeBox shims may omit Type.Number (see index-lifecycle shim test).
679
- limit: Type.Optional({ type: "number", minimum: 1, description: "Maximum search results to return (default: 12)" } as any),
680
- offset: Type.Optional({ type: "number", minimum: 0, description: "Search result offset (default: 0)" } as any),
702
+ limit: optionalNumber({ minimum: 1, description: "Maximum search results to return (default: 12)" }),
703
+ offset: optionalNumber({ minimum: 0, description: "Search result offset (default: 0)" }),
681
704
  server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
682
705
  action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
683
706
  }),
684
707
  renderResult: renderMcpToolResult,
685
- async execute(_toolCallId, params: {
708
+ async execute(_toolCallId: string, params: {
686
709
  tool?: string;
687
710
  args?: string | Record<string, unknown>;
688
711
  connect?: string;
@@ -695,7 +718,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
695
718
  offset?: number;
696
719
  server?: string;
697
720
  action?: string;
698
- }, signal, _onUpdate, _ctx) {
721
+ }, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, _ctx: ExtensionContext) {
699
722
  const executeOwner = currentOwner;
700
723
  let parsedArgs: Record<string, unknown> | undefined;
701
724
  if (params.args !== undefined && params.args !== "") {
@@ -849,8 +872,8 @@ export function createMcpAdapter(options: McpAdapterOptions = {}) {
849
872
  const factoryConfig = options.config !== undefined ? cloneMcpConfig(options.config) : undefined;
850
873
  return function mcpAdapter(pi: ExtensionAPI) {
851
874
  installMcpAdapter(pi, {
852
- configPath: options.configPath,
853
- config: factoryConfig !== undefined ? cloneMcpConfig(factoryConfig) : undefined,
875
+ ...(options.configPath !== undefined ? { configPath: options.configPath } : {}),
876
+ ...(factoryConfig !== undefined ? { config: cloneMcpConfig(factoryConfig) } : {}),
854
877
  });
855
878
  };
856
879
  }
@@ -124,7 +124,7 @@ export async function initializeMcp(
124
124
  if (config.settings?.sampling !== false && (hasUI || samplingAutoApprove)) {
125
125
  manager.setSamplingConfig({
126
126
  autoApprove: samplingAutoApprove,
127
- ui,
127
+ ...(ui !== undefined ? { ui } : {}),
128
128
  modelRegistry,
129
129
  getCurrentModel: () => owner.isActive() ? ctx.model : undefined,
130
130
  getSignal: () => owner.isActive()
@@ -166,6 +166,7 @@ export async function initializeMcp(
166
166
  failureTracker,
167
167
  failureMessages,
168
168
  approvedToolCalls,
169
+ approvalEvents: pi.events,
169
170
  uiResourceHandler,
170
171
  consentManager,
171
172
  uiServer: null,
@@ -175,12 +176,12 @@ export async function initializeMcp(
175
176
  await openUrl(pi, url, process.env.BROWSER, owner.signal);
176
177
  owner.throwIfInactive();
177
178
  },
178
- ui,
179
+ ...(ui !== undefined ? { ui } : {}),
179
180
  sendMessage: (message, options) => {
180
181
  if (!owner.isActive()) return;
181
182
  pi.sendMessage(message as unknown as Parameters<typeof pi.sendMessage>[0], options);
182
183
  },
183
- statusEvents: options.statusEvents,
184
+ ...(options.statusEvents !== undefined ? { statusEvents: options.statusEvents } : {}),
184
185
  };
185
186
  if (ownsOAuthRuntime) owner.addCleanup(() => shutdownOAuth(oauthRuntime));
186
187
  manager.setMetadataListChangedListener?.((serverName, reason) => {
@@ -345,8 +346,9 @@ export async function initializeMcp(
345
346
  missingCacheServers.filter(name => !results.some(r => r.name === name && r.connection)),
346
347
  10,
347
348
  async (name) => {
348
- const definition = config.mcpServers[name];
349
349
  try {
350
+ const definition = config.mcpServers[name];
351
+ if (!definition) throw new Error(`MCP server "${name}" is not configured`);
350
352
  const connection = await manager.connect(name, definition, runtimeSignal);
351
353
  if (connection.status === "needs-auth") {
352
354
  return { name, ok: false };
@@ -412,8 +414,8 @@ export async function initializeMcp(
412
414
 
413
415
  export function markKeepAliveAfterConnect(state: McpExtensionState, serverName: string): void {
414
416
  const definition = state.config.mcpServers[serverName];
415
- if (isServerDisabled(definition)) return;
416
- if ((definition?.lifecycle ?? "lazy") === "lazy-keep-alive") {
417
+ if (!definition || isServerDisabled(definition)) return;
418
+ if ((definition.lifecycle ?? "lazy") === "lazy-keep-alive") {
417
419
  state.lifecycle.markKeepAlive(serverName, definition);
418
420
  }
419
421
  }
@@ -485,7 +487,7 @@ export function updateMetadataCache(
485
487
  tools,
486
488
  resources,
487
489
  ...(prompts !== undefined ? { prompts } : {}),
488
- instructions: connection.instructions,
490
+ ...(connection.instructions !== undefined ? { instructions: connection.instructions } : {}),
489
491
  cachedAt: Date.now(),
490
492
  };
491
493
 
@@ -1,12 +1,12 @@
1
1
  import { Ajv } from "ajv";
2
2
  import Ajv2020Import from "ajv/dist/2020.js";
3
3
  import addFormatsImport from "ajv-formats";
4
- import { AjvJsonSchemaValidator } from "@modelcontextprotocol/sdk/validation/ajv";
4
+ import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
5
5
  import type {
6
6
  JsonSchemaType,
7
7
  JsonSchemaValidator,
8
8
  jsonSchemaValidator as JsonSchemaValidatorProvider,
9
- } from "@modelcontextprotocol/sdk/validation/types.js";
9
+ } from "@modelcontextprotocol/client";
10
10
 
11
11
  // ajv-formats types target its bundled ajv; the runtime accepts both instances.
12
12
  const addFormats = addFormatsImport as unknown as (instance: Ajv) => void;
@@ -12,15 +12,15 @@ export class McpLifecycleManager {
12
12
  private allServers = new Map<string, ServerDefinition>();
13
13
  private serverSettings = new Map<string, { idleTimeout?: number }>();
14
14
  private globalIdleTimeout = 10 * 60 * 1000;
15
- private healthCheckInterval?: NodeJS.Timeout;
16
- private onReconnect?: ReconnectCallback;
17
- private onReconnectFailure?: ReconnectFailureCallback;
18
- private onIdleShutdown?: (serverName: string) => void;
19
- private activeHealthCheck?: Promise<void>;
20
- private shutdownPromise?: Promise<void>;
15
+ private healthCheckInterval: NodeJS.Timeout | undefined;
16
+ private onReconnect: ReconnectCallback | undefined;
17
+ private onReconnectFailure: ReconnectFailureCallback | undefined;
18
+ private onIdleShutdown: ((serverName: string) => void) | undefined;
19
+ private activeHealthCheck: Promise<void> | undefined;
20
+ private shutdownPromise: Promise<void> | undefined;
21
21
  private stopped = false;
22
- private healthSignal?: AbortSignal;
23
- private removeHealthAbortListener?: () => void;
22
+ private healthSignal: AbortSignal | undefined;
23
+ private removeHealthAbortListener: (() => void) | undefined;
24
24
 
25
25
  constructor(
26
26
  private readonly manager: McpServerManager,
@@ -69,7 +69,7 @@ class Logger {
69
69
  level,
70
70
  message,
71
71
  context: { ...this.defaultContext, ...context },
72
- error,
72
+ ...(error !== undefined ? { error } : {}),
73
73
  timestamp: new Date(),
74
74
  };
75
75
 
@@ -7,9 +7,10 @@
7
7
  import {
8
8
  auth as runSdkAuth,
9
9
  extractWWWAuthenticateParams,
10
+ LATEST_PROTOCOL_VERSION,
10
11
  UnauthorizedError,
11
- } from "@modelcontextprotocol/sdk/client/auth.js"
12
- import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
12
+ type AuthOptions,
13
+ } from "@modelcontextprotocol/client"
13
14
  import open from "open"
14
15
  import { McpOAuthProvider, type McpOAuthConfig } from "./mcp-oauth-provider.ts"
15
16
  import {
@@ -50,15 +51,17 @@ export interface AuthenticateOptions {
50
51
  authStorageOptions?: AuthStorageOptions
51
52
  signal?: AbortSignal
52
53
  runtime?: McpOAuthRuntime
54
+ skipIssuerMetadataValidation?: boolean
53
55
  }
54
56
 
55
- type AuthDiscovery = {
56
- resourceMetadataUrl?: URL
57
- scope?: string
58
- }
57
+ type AuthDiscovery = Pick<AuthOptions, "resourceMetadataUrl" | "scope" | "skipIssuerMetadataValidation">
59
58
 
60
- function applyConfiguredScope(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
61
- return config.scope !== undefined ? { ...discovery, scope: config.scope } : discovery
59
+ function applyOAuthConfig(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
60
+ return {
61
+ ...discovery,
62
+ ...(config.scope !== undefined ? { scope: config.scope } : {}),
63
+ ...(config.skipIssuerMetadataValidation === true ? { skipIssuerMetadataValidation: true } : {}),
64
+ }
62
65
  }
63
66
 
64
67
  type PendingAuth = {
@@ -208,6 +211,12 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
208
211
  }
209
212
  config.clientUri = clientUri
210
213
  }
214
+ if (definition.oauth?.skipIssuerMetadataValidation !== undefined) {
215
+ if (typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
216
+ throw new Error("OAuth skipIssuerMetadataValidation must be a boolean")
217
+ }
218
+ config.skipIssuerMetadataValidation = definition.oauth.skipIssuerMetadataValidation
219
+ }
211
220
  return config
212
221
  }
213
222
 
@@ -239,7 +248,7 @@ async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, s
239
248
  clientInfo: { name: "pi-mcp-adapter", version: "2.11.0" },
240
249
  },
241
250
  }),
242
- signal: discoverySignal,
251
+ ...(discoverySignal ? { signal: discoverySignal } : {}),
243
252
  })
244
253
  const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response)
245
254
  await response.body?.cancel().catch(() => {})
@@ -320,7 +329,7 @@ export async function startAuth(
320
329
  },
321
330
  }, authStorageOptions, runtime.signal)
322
331
  try {
323
- const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
332
+ const discovery = applyOAuthConfig(await probeAuthDiscovery(serverUrl, definition, signal), config)
324
333
  throwIfAborted(signal)
325
334
  const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
326
335
  throwIfAborted(signal)
@@ -386,7 +395,7 @@ export async function startAuth(
386
395
 
387
396
  throwIfAborted(signal)
388
397
 
389
- const discovery = applyConfiguredScope(await probeAuthDiscovery(serverUrl, definition, signal), config)
398
+ const discovery = applyOAuthConfig(await probeAuthDiscovery(serverUrl, definition, signal), config)
390
399
  throwIfAborted(signal)
391
400
  const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
392
401
  throwIfAborted(signal)
@@ -606,6 +615,7 @@ export async function completeAuth(
606
615
  const result = await abortable(runSdkAuth(pendingAuth.authProvider, {
607
616
  serverUrl: pendingAuth.serverUrl,
608
617
  authorizationCode: code,
618
+ ...(iss !== undefined ? { iss } : {}),
609
619
  ...pendingAuth.discovery,
610
620
  }), signal)
611
621
  throwIfAborted(signal)
@@ -657,10 +667,12 @@ export async function authenticate(
657
667
  }
658
668
 
659
669
  const operation = (async (): Promise<AuthStatus> => {
660
- // Start auth flow
661
- const { authorizationUrl } = await startAuth(serverName, serverUrl, definition, { ...options, signal, runtime })
670
+ const { authorizationUrl } = await startAuth(serverName, serverUrl, definition, {
671
+ ...options,
672
+ ...(signal ? { signal } : {}),
673
+ runtime,
674
+ })
662
675
 
663
- // If no auth URL needed, already authenticated
664
676
  if (!authorizationUrl) {
665
677
  return "authenticated"
666
678
  }
@@ -694,14 +706,16 @@ export async function authenticate(
694
706
  console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
695
707
  }
696
708
 
697
- // Wait for callback
698
709
  const callbackResult = await abortable(callbackPromise, signal)
699
710
 
700
711
  // The callback server accepted only the flow-local reserved state.
701
712
  throwIfAborted(signal)
702
713
 
703
- // Complete the auth
704
- return await completeAuth(serverName, callbackResult, { ...options, signal, runtime })
714
+ return await completeAuth(serverName, callbackResult, {
715
+ ...options,
716
+ ...(signal ? { signal } : {}),
717
+ runtime,
718
+ })
705
719
  } catch (error) {
706
720
  if (oauthState) cancelPendingCallback(oauthState)
707
721
  try {
@@ -740,25 +754,21 @@ export async function getValidToken(
740
754
  const authStorageOptions = options.authStorageOptions ?? {}
741
755
  const signal = combineAbortSignals(runtime.signal, options.signal)
742
756
  throwIfAborted(signal)
743
- // Check if we have valid tokens
744
757
  const entry = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
745
758
  throwIfAborted(signal)
746
759
  if (!entry?.tokens) {
747
760
  return null
748
761
  }
749
762
 
750
- // Check expiration
751
763
  const expired = await isTokenExpired(serverName, authStorageOptions)
752
764
  if (expired === false) {
753
765
  return entry.tokens
754
766
  }
755
767
 
756
768
  if (expired === true && entry.tokens.refreshToken) {
757
- // Token is expired, try to refresh
758
769
  console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
759
770
 
760
771
  try {
761
- // Create auth provider for token refresh
762
772
  const authProvider = new McpOAuthProvider(serverName, serverUrl, {}, {
763
773
  onRedirect: async () => {},
764
774
  }, authStorageOptions, runtime.signal)
@@ -773,7 +783,11 @@ export async function getValidToken(
773
783
 
774
784
  const discovery = await probeAuthDiscovery(serverUrl, undefined, signal)
775
785
  throwIfAborted(signal)
776
- const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery }), signal)
786
+ const result = await abortable(runSdkAuth(authProvider, {
787
+ serverUrl,
788
+ ...discovery,
789
+ ...(options.skipIssuerMetadataValidation === true ? { skipIssuerMetadataValidation: true } : {}),
790
+ }), signal)
777
791
  throwIfAborted(signal)
778
792
  if (result !== "AUTHORIZED") {
779
793
  return null