@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
@@ -1,4 +1,4 @@
1
- import { getToolUiResourceUri } from "@modelcontextprotocol/ext-apps/app-bridge";
1
+ import { getToolUiResourceUri } from "./ui-app-bridge-helpers.ts";
2
2
  import type { McpExtensionState } from "./state.ts";
3
3
  import type { ToolMetadata, McpTool, McpResource, ServerEntry, ToolPrefix } from "./types.ts";
4
4
  import { formatToolName, isToolAllowed, resolveToolPrefix } from "./types.ts";
@@ -44,14 +44,15 @@ export function buildToolMetadata(
44
44
  } catch {
45
45
  failedTools.push(tool.name);
46
46
  }
47
+ const uiStreamMode = extractToolUiStreamMode(tool._meta);
47
48
  metadata.push({
48
49
  name,
49
50
  originalName: tool.name,
50
51
  description: tool.description ?? "",
51
- inputSchema: tool.inputSchema,
52
- uiResourceUri,
53
- uiVisibility,
54
- uiStreamMode: extractToolUiStreamMode(tool._meta),
52
+ ...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
53
+ ...(uiResourceUri !== undefined ? { uiResourceUri } : {}),
54
+ ...(uiVisibility !== undefined ? { uiVisibility } : {}),
55
+ ...(uiStreamMode !== undefined ? { uiStreamMode } : {}),
55
56
  });
56
57
  }
57
58
 
@@ -41,6 +41,7 @@ class CollapsibleText implements Component {
41
41
  private readonly fullText: Text;
42
42
  private readonly footerText: Text;
43
43
  private collapsedText: { charBudget: number; fullyIncluded: boolean; text: Text } | null = null;
44
+ private collapsedRender: { width: number; charBudget: number; lines: string[] } | null = null;
44
45
 
45
46
  constructor(
46
47
  private readonly text: string,
@@ -70,18 +71,29 @@ class CollapsibleText implements Component {
70
71
  fullyIncluded: prefix === this.text,
71
72
  text: new Text(prefix, 0, 0),
72
73
  };
74
+ this.collapsedRender = null;
73
75
  }
74
76
 
75
77
  const lines = this.collapsedText.text.render(width);
76
78
  if (!this.preTruncated && this.collapsedText.fullyIncluded && lines.length <= this.maxCollapsedLines) return lines;
79
+ if (this.collapsedRender?.width === width && this.collapsedRender.charBudget === charBudget) {
80
+ return this.collapsedRender.lines;
81
+ }
77
82
 
78
- return [
83
+ const rendered = [
79
84
  ...lines.slice(0, this.maxCollapsedLines),
80
85
  ...this.footerText.render(width),
81
86
  ];
87
+ this.collapsedRender = { width, charBudget, lines: rendered };
88
+ return rendered;
82
89
  }
83
90
 
84
- invalidate(): void {}
91
+ invalidate(): void {
92
+ this.fullText.invalidate();
93
+ this.footerText.invalidate();
94
+ this.collapsedText?.text.invalidate();
95
+ this.collapsedRender = null;
96
+ }
85
97
  }
86
98
 
87
99
  function truncateText(value: string, maxChars: number): string {
@@ -39,7 +39,10 @@ export function renderTsShape(inputSchema: unknown): string | null {
39
39
  if (typeof schema.$ref !== "string") return null;
40
40
  const match = schema.$ref.match(/^#\/(\$defs|definitions)\/([^/]+)$/);
41
41
  if (!match) return null;
42
- const definitionKey = `${match[1]}/${decodePointerToken(match[2])}`;
42
+ const definitionGroup = match[1];
43
+ const definitionName = match[2];
44
+ if (definitionGroup === undefined || definitionName === undefined) return null;
45
+ const definitionKey = `${definitionGroup}/${decodePointerToken(definitionName)}`;
43
46
  if (!definitions.has(definitionKey)) return null;
44
47
  return aliasFor(definitionKey);
45
48
  }
@@ -108,7 +111,11 @@ function isSchema(value: unknown): value is Schema {
108
111
  }
109
112
 
110
113
  function hasUnsupportedKeyword(schema: Schema): boolean {
111
- return UNSUPPORTED_KEYWORDS.some(keyword => Object.hasOwn(schema, keyword));
114
+ return UNSUPPORTED_KEYWORDS.some(keyword => {
115
+ if (!Object.hasOwn(schema, keyword)) return false;
116
+ // `additionalProperties: false` is a closed-object constraint, not a shape that this renderer needs to understand.
117
+ return keyword !== "additionalProperties" || schema.additionalProperties !== false;
118
+ });
112
119
  }
113
120
 
114
121
  function decodePointerToken(token: string): string {
@@ -1,6 +1,11 @@
1
1
  // types.ts - Core type definitions
2
- import type { Transport as McpTransport } from "@modelcontextprotocol/sdk/shared/transport.js";
3
- import type { ContentBlock as McpContentBlock } from "@modelcontextprotocol/sdk/types.js";
2
+ import type {
3
+ ContentBlock as McpContentBlock,
4
+ ListPromptsResult,
5
+ ListResourcesResult,
6
+ ListToolsResult,
7
+ Transport as McpTransport,
8
+ } from "@modelcontextprotocol/client";
4
9
  import type { TextContent, ImageContent } from "@earendil-works/pi-ai";
5
10
  import type { UiStreamMode } from "./ui-stream-types.ts";
6
11
  import type { UiToolVisibility } from "./ui-tool-visibility.ts";
@@ -48,36 +53,41 @@ export type ImportKind =
48
53
  | "windsurf"
49
54
  | "vscode";
50
55
 
51
- // Tool definition from MCP server
56
+ type SdkTool = ListToolsResult["tools"][number];
57
+ type SdkResource = ListResourcesResult["resources"][number];
58
+ type SdkPrompt = ListPromptsResult["prompts"][number];
59
+ type SdkPromptArgument = NonNullable<SdkPrompt["arguments"]>[number];
60
+
61
+ // MCP wire definitions derive their field types from the installed SDK while
62
+ // retaining the adapter's deliberately smaller public surface.
52
63
  export interface McpTool {
53
- name: string;
54
- title?: string;
55
- description?: string;
56
- inputSchema?: unknown; // JSON Schema
57
- _meta?: Record<string, unknown>;
64
+ name: SdkTool["name"];
65
+ title?: SdkTool["title"];
66
+ description?: SdkTool["description"];
67
+ inputSchema?: SdkTool["inputSchema"]; // JSON Schema
68
+ _meta?: SdkTool["_meta"];
58
69
  }
59
70
 
60
- // Resource definition from MCP server
61
71
  export interface McpResource {
62
- uri: string;
63
- name: string;
64
- description?: string;
65
- mimeType?: string;
66
- _meta?: Record<string, unknown>;
72
+ uri: SdkResource["uri"];
73
+ name: SdkResource["name"];
74
+ description?: SdkResource["description"];
75
+ mimeType?: SdkResource["mimeType"];
76
+ _meta?: SdkResource["_meta"];
67
77
  }
68
78
 
69
79
  export interface McpPromptArgument {
70
- name: string;
71
- description?: string;
72
- required?: boolean;
80
+ name: SdkPromptArgument["name"];
81
+ description?: SdkPromptArgument["description"];
82
+ required?: SdkPromptArgument["required"];
73
83
  }
74
84
 
75
85
  export interface McpPrompt {
76
- name: string;
77
- title?: string;
78
- description?: string;
79
- arguments?: McpPromptArgument[];
80
- _meta?: Record<string, unknown>;
86
+ name: SdkPrompt["name"];
87
+ title?: SdkPrompt["title"];
88
+ description?: SdkPrompt["description"];
89
+ arguments?: SdkPrompt["arguments"];
90
+ _meta?: SdkPrompt["_meta"];
81
91
  }
82
92
 
83
93
  export interface UiResourceMeta {
@@ -340,6 +350,8 @@ export interface OAuthConfig {
340
350
  clientName?: string;
341
351
  /** Client homepage URI for dynamic registration */
342
352
  clientUri?: string;
353
+ /** Security-weakening escape hatch for known-misconfigured authorization servers. */
354
+ skipIssuerMetadataValidation?: boolean;
343
355
  }
344
356
 
345
357
  // Server configuration
@@ -387,6 +399,20 @@ export interface ServerEntry {
387
399
  debug?: boolean; // Show server stderr (default: false)
388
400
  /** Enable metadata-only JSONL protocol tracing for this server. */
389
401
  trace?: boolean;
402
+ /** Force a specific HTTP MCP transport. Used by Agent Plugins, whose `type` declares the transport and forbids client fallback. */
403
+ httpTransport?: "streamable-http" | "sse";
404
+ /** Client-managed persistent data directory for Agent Plugin stdio servers. */
405
+ pluginDataDir?: string;
406
+ /** Treat env values as already resolved literals. Used for Agent Plugin env rules. */
407
+ literalEnv?: boolean;
408
+ /**
409
+ * MCP protocol era negotiation for this server. Defaults to `"legacy"`
410
+ * (byte-equivalent to pre-2026 behavior — no `versionNegotiation` is sent).
411
+ * `"auto"` offers the SDK's default 2026-07-28+ modern versions with
412
+ * legacy fallback; `"2026-07-28"` pins the connection to that revision
413
+ * with no fallback. `auto` and `2026-07-28` must be set explicitly.
414
+ */
415
+ protocolVersion?: "legacy" | "auto" | "2026-07-28";
390
416
  // Keep configuration visible without allowing connections or execution.
391
417
  disabled?: boolean;
392
418
  }
@@ -422,6 +448,23 @@ export interface McpTraceSettings {
422
448
  maxEvents?: number;
423
449
  }
424
450
 
451
+ export const MCP_TOOL_APPROVAL_REQUEST_EVENT = "pi-mcp-adapter:tool-approval-request" as const;
452
+
453
+ export type McpToolApprovalOrigin = "proxy" | "direct" | "script" | "resource" | "iframe";
454
+ export type McpToolApprovalDecision = "allow_once" | "allow_for_session" | "deny" | "abstain";
455
+ export type McpToolApprovalHandler = () => McpToolApprovalDecision | Promise<McpToolApprovalDecision>;
456
+
457
+ export interface McpToolApprovalRequest {
458
+ requestId: string;
459
+ serverName: string;
460
+ originalToolName: string;
461
+ prefixedToolName: string;
462
+ args: Record<string, unknown>;
463
+ origin: McpToolApprovalOrigin;
464
+ signal?: AbortSignal;
465
+ claim(handler: McpToolApprovalHandler): boolean;
466
+ }
467
+
425
468
  export interface McpSettings {
426
469
  toolPrefix?: ToolPrefix;
427
470
  /** Show the plug prefix in MCP status and connection text (default: true). Set to false to disable it. */
@@ -430,6 +473,8 @@ export interface McpSettings {
430
473
  mcpFooterStatus?: McpFooterStatus;
431
474
  /** Discover detected host-specific MCP configs only when explicitly enabled. */
432
475
  hostConfigDiscovery?: HostConfigDiscovery;
476
+ /** Agent Plugin package directories to load MCP servers from. */
477
+ agentPluginPaths?: string[];
433
478
  idleTimeout?: number; // minutes, default 10, 0 to disable
434
479
  requestTimeoutMs?: number; // milliseconds, overrides the SDK request timeout when > 0
435
480
  directTools?: boolean;
@@ -622,6 +667,48 @@ export function resolveToolPrefix(
622
667
  return definition?.toolPrefix ?? globalPrefix ?? "server";
623
668
  }
624
669
 
670
+
671
+ /**
672
+ * Resolve a configured MCP server name from a prefixed tool name.
673
+ *
674
+ * When the proxy tool is addressed with a fully-qualified name such as
675
+ * `searxng_searxng_web_search`, downstream policy systems (for example a
676
+ * permission gate) need to recover the owning server so they can evaluate
677
+ * server-scoped rules against the bare server name. This performs the inverse
678
+ * of {@link getServerPrefix}: it finds the longest configured server prefix
679
+ * that the tool name starts with and returns that server's name.
680
+ *
681
+ * @param toolName - the tool name as passed to the proxy `mcp({ tool })` call.
682
+ * @param serverNames - the configured MCP server names (keys of `mcpServers`).
683
+ * @param prefix - the active tool-prefix mode.
684
+ * @returns the resolved server name, or `undefined` when no prefix matches or
685
+ * the prefix mode is `"none"`.
686
+ */
687
+ export function resolveServerFromToolName(
688
+ toolName: string,
689
+ serverNames: Iterable<string>,
690
+ prefix: ToolPrefix,
691
+ ): string | undefined {
692
+ if (prefix === "none") return undefined;
693
+ const candidates: { name: string; prefix: string }[] = [];
694
+ for (const name of serverNames) {
695
+ const p = getServerPrefix(name, prefix);
696
+ if (p && toolName.startsWith(p + "_")) candidates.push({ name, prefix: p });
697
+ }
698
+ if (candidates.length === 0) return undefined;
699
+ candidates.sort((a, b) => b.prefix.length - a.prefix.length);
700
+ const best = candidates[0];
701
+ // Fail safe: two distinct server names can normalize to the same prefix
702
+ // (e.g. my-server and my_server both -> my_server under "server" mode).
703
+ // When that happens the owning server is ambiguous; return undefined so a
704
+ // downstream permission gate falls back to its existing wildcard path rather
705
+ // than enforcing a rule against the wrong server.
706
+ if (candidates.some((c) => c.prefix === best!.prefix && c.name !== best!.name)) {
707
+ return undefined;
708
+ }
709
+ return best?.name;
710
+ }
711
+
625
712
  export function sanitizePromptName(name: string): string {
626
713
  const cleaned = name.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^[_-]+|[_-]+$/g, "");
627
714
  if (!cleaned) return "prompt";
@@ -0,0 +1,40 @@
1
+ import type { UiResourcePermissions } from "./types.ts";
2
+
3
+ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
4
+
5
+ const RESOURCE_URI_META_KEY = "ui/resourceUri";
6
+
7
+ export function getToolUiResourceUri(tool: { _meta?: Record<string, unknown> | undefined }): string | undefined {
8
+ const meta = tool._meta;
9
+ let resourceUri = getNestedResourceUri(meta);
10
+ if (resourceUri === undefined) {
11
+ resourceUri = meta?.[RESOURCE_URI_META_KEY];
12
+ }
13
+
14
+ if (typeof resourceUri === "string" && resourceUri.startsWith("ui://")) {
15
+ return resourceUri;
16
+ }
17
+
18
+ if (resourceUri !== undefined) {
19
+ throw new Error(`Invalid UI resource URI: ${JSON.stringify(resourceUri)}`);
20
+ }
21
+
22
+ return undefined;
23
+ }
24
+
25
+ export function buildAllowAttribute(permissions: UiResourcePermissions | undefined): string {
26
+ if (!permissions) return "";
27
+
28
+ const allowed: string[] = [];
29
+ if (permissions.camera) allowed.push("camera");
30
+ if (permissions.microphone) allowed.push("microphone");
31
+ if (permissions.geolocation) allowed.push("geolocation");
32
+ if (permissions.clipboardWrite) allowed.push("clipboard-write");
33
+ return allowed.join("; ");
34
+ }
35
+
36
+ function getNestedResourceUri(meta: Record<string, unknown> | undefined): unknown {
37
+ const ui = meta?.ui;
38
+ if (!ui || typeof ui !== "object") return undefined;
39
+ return (ui as Record<string, unknown>).resourceUri;
40
+ }
@@ -1,10 +1,10 @@
1
- import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/app-bridge";
2
- import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
1
+ import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
2
+ import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/client";
3
3
  import { ResourceFetchError, ResourceParseError } from "./errors.ts";
4
4
  import { logger } from "./logger.ts";
5
5
  import { SessionRecoveryAuthRequiredError, withSessionRecovery, type SessionRecoveryDeps } from "./session-recovery.ts";
6
6
  import type { McpServerManager } from "./server-manager.ts";
7
- import { isServerDisabled, type McpConfig, type UiResourceContent, type UiResourceCsp, type UiResourceMeta } from "./types.ts";
7
+ import { isServerDisabled, type McpConfig, type UiResourceContent, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions } from "./types.ts";
8
8
 
9
9
  interface ResourceContentRecord {
10
10
  uri?: string;
@@ -23,7 +23,7 @@ interface ReadUiResourceOptions {
23
23
  export class UiResourceHandler {
24
24
  private log = logger.child({ component: "UiResourceHandler" });
25
25
 
26
- constructor(private manager: McpServerManager, private config?: McpConfig) {}
26
+ constructor(private manager: McpServerManager, private config: McpConfig | undefined = undefined) {}
27
27
 
28
28
  async readUiResource(serverName: string, uri: string, options: ReadUiResourceOptions = {}): Promise<UiResourceContent> {
29
29
  const log = this.log.child({ server: serverName, uri });
@@ -45,7 +45,12 @@ export class UiResourceHandler {
45
45
  this.manager.incrementInFlight(serverName);
46
46
  try {
47
47
  result = await withSessionRecovery(
48
- { manager: this.manager, config, signal: options.signal, onNeedsAuth: options.onNeedsAuth },
48
+ {
49
+ manager: this.manager,
50
+ config,
51
+ ...(options.signal ? { signal: options.signal } : {}),
52
+ ...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
53
+ },
49
54
  serverName,
50
55
  (connection) => connection.client.readResource({ uri }, this.manager.getRequestOptions(serverName, options.signal)),
51
56
  );
@@ -62,7 +67,7 @@ export class UiResourceHandler {
62
67
  log.error("Failed to read resource", error instanceof Error ? error : undefined);
63
68
  throw new ResourceFetchError(uri, message, {
64
69
  server: serverName,
65
- cause: error instanceof Error ? error : undefined,
70
+ ...(error instanceof Error ? { cause: error } : {}),
66
71
  });
67
72
  }
68
73
 
@@ -97,10 +102,14 @@ export class UiResourceHandler {
97
102
  html,
98
103
  mimeType: mimeType ?? RESOURCE_MIME_TYPE,
99
104
  meta: {
100
- csp: contentMeta.csp ?? listMeta.csp,
101
- permissions: contentMeta.permissions ?? listMeta.permissions,
102
- domain: contentMeta.domain ?? listMeta.domain,
103
- prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder,
105
+ ...((contentMeta.csp ?? listMeta.csp) !== undefined ? { csp: contentMeta.csp ?? listMeta.csp } : {}),
106
+ ...((contentMeta.permissions ?? listMeta.permissions) !== undefined
107
+ ? { permissions: contentMeta.permissions ?? listMeta.permissions }
108
+ : {}),
109
+ ...((contentMeta.domain ?? listMeta.domain) !== undefined ? { domain: contentMeta.domain ?? listMeta.domain } : {}),
110
+ ...((contentMeta.prefersBorder ?? listMeta.prefersBorder) !== undefined
111
+ ? { prefersBorder: contentMeta.prefersBorder ?? listMeta.prefersBorder }
112
+ : {}),
104
113
  },
105
114
  };
106
115
  }
@@ -128,7 +137,11 @@ function selectContent(result: ReadResourceResult, preferredUri: string): Resour
128
137
  );
129
138
  if (byHtmlMime) return byHtmlMime;
130
139
 
131
- return contents[0];
140
+ const firstContent = contents[0];
141
+ if (!firstContent) {
142
+ throw new Error(`No contents returned for UI resource: ${preferredUri}`);
143
+ }
144
+ return firstContent;
132
145
  }
133
146
 
134
147
  function isHtmlMimeType(mimeType: string): boolean {
@@ -166,10 +179,10 @@ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMet
166
179
 
167
180
  const ui = isRecord(meta.ui) ? meta.ui : undefined;
168
181
  const out: UiResourceMeta = {};
169
- const openAiCsp = hasOwnProperty(meta, "openai/widgetCSP")
182
+ const openAiCsp = Object.hasOwn(meta, "openai/widgetCSP")
170
183
  ? normalizeOpenAiWidgetCsp(meta["openai/widgetCSP"])
171
184
  : undefined;
172
- const hasStandardCsp = !!ui && hasOwnProperty(ui, "csp");
185
+ const hasStandardCsp = !!ui && Object.hasOwn(ui, "csp");
173
186
  const standardCspValue = hasStandardCsp ? ui.csp : undefined;
174
187
 
175
188
  if (hasStandardCsp && !isRecord(standardCspValue)) {
@@ -183,7 +196,7 @@ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMet
183
196
  out.csp = { ...openAiCsp, ...standardCsp };
184
197
  if (isRecord(standardCspValue)) {
185
198
  for (const [, standardField] of OPENAI_CSP_FIELD_MAPPINGS) {
186
- if (hasOwnProperty(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
199
+ if (Object.hasOwn(standardCspValue, standardField) && !copyStringArray(standardCspValue[standardField])) {
187
200
  delete out.csp[standardField];
188
201
  }
189
202
  }
@@ -192,7 +205,7 @@ function extractUiMeta(meta: Record<string, unknown> | undefined): UiResourceMet
192
205
  }
193
206
 
194
207
  if (ui && isRecord(ui.permissions)) {
195
- out.permissions = ui.permissions as UiResourceMeta["permissions"];
208
+ out.permissions = ui.permissions as UiResourcePermissions;
196
209
  }
197
210
  if (ui && typeof ui.domain === "string") {
198
211
  out.domain = ui.domain;
@@ -232,10 +245,6 @@ function copyStringArray(value: unknown): string[] | undefined {
232
245
  : undefined;
233
246
  }
234
247
 
235
- function hasOwnProperty(record: Record<string, unknown>, property: string): boolean {
236
- return Object.prototype.hasOwnProperty.call(record, property);
237
- }
238
-
239
248
  function isRecord(value: unknown): value is Record<string, unknown> {
240
249
  return typeof value === "object" && value !== null && !Array.isArray(value);
241
250
  }