@humain/terminal 0.0.13 → 0.0.15

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 (44) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/bundle/chunks/{chunk-V5XRWLQF.js → chunk-QMTWJZBT.js} +252 -40
  3. package/dist/bundle/cli.js +1 -1
  4. package/dist/bundle/index.js +1 -1
  5. package/dist/bundle/rpc-entry.js +1 -1
  6. package/dist/core/tools/subagent.d.ts.map +1 -1
  7. package/dist/core/tools/subagent.js +30 -9
  8. package/dist/core/tools/subagent.js.map +1 -1
  9. package/dist/humain/agent-execution-runtime.d.ts.map +1 -1
  10. package/dist/humain/agent-execution-runtime.js +3 -1
  11. package/dist/humain/agent-execution-runtime.js.map +1 -1
  12. package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +27 -0
  13. package/dist/humain/mcp-adapter/vendor/README.md +13 -13
  14. package/dist/humain/mcp-adapter/vendor/commands.ts +58 -21
  15. package/dist/humain/mcp-adapter/vendor/config.ts +18 -4
  16. package/dist/humain/mcp-adapter/vendor/direct-tools.ts +26 -6
  17. package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +4 -0
  18. package/dist/humain/mcp-adapter/vendor/dist/config.js +13 -4
  19. package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
  20. package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +11 -3
  21. package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
  22. package/dist/humain/mcp-adapter/vendor/error-signal.ts +15 -4
  23. package/dist/humain/mcp-adapter/vendor/errors.ts +141 -0
  24. package/dist/humain/mcp-adapter/vendor/host-html-template.ts +58 -5
  25. package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +1017 -160
  26. package/dist/humain/mcp-adapter/vendor/index.ts +2 -3
  27. package/dist/humain/mcp-adapter/vendor/init.ts +5 -0
  28. package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +30 -9
  29. package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +66 -28
  30. package/dist/humain/mcp-adapter/vendor/mcp-status.ts +2 -0
  31. package/dist/humain/mcp-adapter/vendor/package.json +3 -2
  32. package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +66 -13
  33. package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +217 -0
  34. package/dist/humain/mcp-adapter/vendor/server-manager.ts +337 -6
  35. package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +1 -0
  36. package/dist/humain/mcp-adapter/vendor/types.ts +18 -3
  37. package/dist/humain/mcp-adapter/vendor/ui-resource-handler.ts +18 -2
  38. package/dist/humain/mcp-adapter/vendor/ui-server.ts +179 -54
  39. package/dist/humain/mcp-adapter/vendor/ui-session.ts +28 -8
  40. package/npm-shrinkwrap.json +2 -2
  41. package/package.json +1 -1
  42. package/src/humain/mcp-adapter/UPSTREAM.md +1 -1
  43. package/src/humain/mcp-adapter/upstream.json +4 -4
  44. package/src/humain/mcp-adapter/vendor/CHANGELOG.md +27 -0
@@ -1106,12 +1106,18 @@ import stripJsonComments2 from "strip-json-comments";
1106
1106
  function getPiGlobalConfigPath(overridePath) {
1107
1107
  return overridePath ? resolve4(overridePath) : getAgentPath("mcp.json");
1108
1108
  }
1109
+ function getGenericGlobalConfigPath() {
1110
+ return GENERIC_GLOBAL_CONFIG_PATH;
1111
+ }
1109
1112
  function getProjectConfigPath(cwd = process.cwd()) {
1110
1113
  return resolve4(cwd, PROJECT_CONFIG_NAME);
1111
1114
  }
1112
1115
  function getProjectPiConfigPath(cwd = process.cwd()) {
1113
1116
  return resolve4(cwd, getConfigDirName(), PROJECT_PI_CONFIG_NAME);
1114
1117
  }
1118
+ function getSharedConfigPath(target, cwd = process.cwd()) {
1119
+ return target === "project" ? getProjectConfigPath(cwd) : getGenericGlobalConfigPath();
1120
+ }
1115
1121
  function getConfigSourceSummaries(sourceSpecs) {
1116
1122
  return sourceSpecs.map((source) => {
1117
1123
  const loaded = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`);
@@ -1897,13 +1903,13 @@ function buildStarterProjectConfig() {
1897
1903
  mcpServers: {}
1898
1904
  };
1899
1905
  }
1900
- function previewStarterProjectConfig(cwd = process.cwd()) {
1901
- const targetPath = getProjectConfigPath(cwd);
1906
+ function previewStarterSharedConfig(target, cwd = process.cwd()) {
1907
+ const targetPath = getSharedConfigPath(target, cwd);
1902
1908
  const nextRaw = { mcpServers: buildStarterProjectConfig().mcpServers };
1903
1909
  return buildConfigWritePreview(targetPath, nextRaw);
1904
1910
  }
1905
- function writeStarterProjectConfig(cwd = process.cwd()) {
1906
- const targetPath = getProjectConfigPath(cwd);
1911
+ function writeStarterSharedConfig(target, cwd = process.cwd()) {
1912
+ const targetPath = getSharedConfigPath(target, cwd);
1907
1913
  const raw = { mcpServers: buildStarterProjectConfig().mcpServers };
1908
1914
  writeRawConfigObject(targetPath, raw);
1909
1915
  return targetPath;
@@ -2529,6 +2535,7 @@ var init_mcp_setup_panel = __esm({
2529
2535
  this.actionCursor = 0;
2530
2536
  this.importCursor = 0;
2531
2537
  this.pathCursor = 0;
2538
+ this.sharedConfigTarget = "project";
2532
2539
  this.selectedImports = /* @__PURE__ */ new Set();
2533
2540
  this.busy = false;
2534
2541
  this.notice = null;
@@ -2566,9 +2573,13 @@ var init_mcp_setup_panel = __esm({
2566
2573
  if (this.discovery.imports.length > 0) {
2567
2574
  actions.push({ id: "adopt-imports", label: "Adopt detected compatibility imports", description: `Choose which host-specific MCP configs Pi should import into its own override file. ${this.discovery.imports.length} source${this.discovery.imports.length === 1 ? "" : "s"} found.` });
2568
2575
  }
2569
- actions.push({ id: "view-example", label: "View example `.mcp.json`", description: "Preview a working shared MCP config you can paste or adapt." });
2570
- if (!this.discovery.sources.some((source) => source.id === "shared-project" && source.exists)) {
2571
- actions.push({ id: "scaffold-project", label: "Scaffold project `.mcp.json`", description: "Write a minimal project config using the standard shared MCP file path, then reload Pi." });
2576
+ actions.push(
2577
+ { id: "select-shared-target", label: `${this.sharedConfigTarget === "project" ? "\u25CF" : "\u25CB"} Add to this project (.mcp.json)`, description: "Write new shared MCP servers to the project/team config.", target: "project" },
2578
+ { id: "select-shared-target", label: `${this.sharedConfigTarget === "global" ? "\u25CF" : "\u25CB"} Add globally (~/.config/mcp/mcp.json)`, description: "Write new shared MCP servers to your all-projects config.", target: "global" }
2579
+ );
2580
+ actions.push({ id: "view-example", label: "View example shared config", description: "Preview a working shared MCP config you can paste or adapt." });
2581
+ if (!this.selectedSharedConfigExists()) {
2582
+ actions.push({ id: "scaffold-shared-config", label: `Scaffold ${this.sharedTargetLabel()}`, description: "Write a minimal config at the selected normal MCP setup path, then reload Pi." });
2572
2583
  }
2573
2584
  actions.push({ id: "show-precedence", label: "Explain config precedence", description: "Show the read order and where Pi writes compatibility settings." });
2574
2585
  if (this.getDetectedPaths().length > 0) {
@@ -2578,7 +2589,7 @@ var init_mcp_setup_panel = __esm({
2578
2589
  actions.push({ id: "add-known-server", label: preset.name, description: preset.summary, preset });
2579
2590
  }
2580
2591
  if (!this.discovery.repoPrompt.configured && this.discovery.repoPrompt.executablePath && this.discovery.repoPrompt.targetPath && this.discovery.repoPrompt.entry && this.discovery.repoPrompt.serverName) {
2581
- actions.push({ id: "add-repoprompt", label: "Add RepoPrompt to shared MCP config", description: "Write a standard MCP entry for RepoPrompt to the recommended shared target, then reload MCP in-session." });
2592
+ actions.push({ id: "add-repoprompt", label: "Add RepoPrompt to selected shared config", description: "Write a standard MCP entry for RepoPrompt to the selected normal setup path, then reload MCP in-session." });
2582
2593
  }
2583
2594
  actions.push({ id: "close", label: "Close", description: "Exit the onboarding flow." });
2584
2595
  return actions;
@@ -2590,6 +2601,13 @@ var init_mcp_setup_panel = __esm({
2590
2601
  ];
2591
2602
  return [...new Set(paths)];
2592
2603
  }
2604
+ sharedTargetLabel() {
2605
+ return this.sharedConfigTarget === "project" ? "project .mcp.json" : "global ~/.config/mcp/mcp.json";
2606
+ }
2607
+ selectedSharedConfigExists() {
2608
+ const sourceId = this.sharedConfigTarget === "project" ? "shared-project" : "shared-global";
2609
+ return this.discovery.sources.some((source) => source.id === sourceId && source.exists);
2610
+ }
2593
2611
  getSelectedAction() {
2594
2612
  const actions = this.getActions();
2595
2613
  return actions[this.actionCursor];
@@ -2704,9 +2722,15 @@ var init_mcp_setup_panel = __esm({
2704
2722
  this.tui.requestRender();
2705
2723
  return;
2706
2724
  }
2707
- if (action.id === "scaffold-project") {
2725
+ if (action.id === "select-shared-target" && action.target) {
2726
+ this.sharedConfigTarget = action.target;
2727
+ this.notice = { text: `New shared servers will be written to ${this.sharedTargetLabel()}.`, tone: "muted" };
2728
+ this.tui.requestRender();
2729
+ return;
2730
+ }
2731
+ if (action.id === "scaffold-shared-config") {
2708
2732
  await this.runBusy(async () => {
2709
- const result = await this.callbacks.scaffoldProjectConfig();
2733
+ const result = await this.callbacks.scaffoldConfig(this.sharedConfigTarget);
2710
2734
  this.callbacks.markSetupCompleted();
2711
2735
  this.notice = { text: `Wrote starter config to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
2712
2736
  });
@@ -2714,7 +2738,7 @@ var init_mcp_setup_panel = __esm({
2714
2738
  }
2715
2739
  if (action.id === "add-repoprompt") {
2716
2740
  await this.runBusy(async () => {
2717
- const result = await this.callbacks.addRepoPrompt();
2741
+ const result = await this.callbacks.addRepoPrompt(this.sharedConfigTarget);
2718
2742
  this.callbacks.markSetupCompleted();
2719
2743
  this.notice = { text: `Added ${result.serverName} to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
2720
2744
  });
@@ -2723,7 +2747,7 @@ var init_mcp_setup_panel = __esm({
2723
2747
  if (action.id === "add-known-server" && action.preset) {
2724
2748
  const preset = action.preset;
2725
2749
  await this.runBusy(async () => {
2726
- const result = await this.callbacks.addKnownServer(preset);
2750
+ const result = await this.callbacks.addKnownServer(preset, this.sharedConfigTarget);
2727
2751
  this.callbacks.markSetupCompleted();
2728
2752
  this.notice = { text: `Added ${result.serverName} to ${result.path}. Pi will reload after this panel closes.`, tone: "success" };
2729
2753
  });
@@ -2812,8 +2836,11 @@ var init_mcp_setup_panel = __esm({
2812
2836
  for (let index = start; index < end; index++) {
2813
2837
  const action = actions[index];
2814
2838
  if (!action) continue;
2839
+ if (action.id === "select-shared-target" && (index === start || actions[index - 1]?.id !== "select-shared-target")) {
2840
+ lines.push(this.padLine(fg(this.t.title, "Choose where new shared servers go"), innerW));
2841
+ }
2815
2842
  if (action.id === "add-known-server" && (index === start || actions[index - 1]?.id !== "add-known-server")) {
2816
- lines.push(this.padLine(fg(this.t.title, "Add a known server"), innerW));
2843
+ lines.push(this.padLine(fg(this.t.title, `Add a known server to ${this.sharedTargetLabel()}`), innerW));
2817
2844
  }
2818
2845
  const selected = index === this.actionCursor;
2819
2846
  const cursor = selected ? fg(this.t.selected, "\u203A") : " ";
@@ -2878,12 +2905,12 @@ var init_mcp_setup_panel = __esm({
2878
2905
  const hostNote = this.discovery.hostConfigs.length > 0 ? ` Host discovery is ${this.discovery.hostConfigDiscovery}; ${this.discovery.hostConfigs.length} host source${this.discovery.hostConfigs.length === 1 ? "" : "s"} detected.` : "";
2879
2906
  const conflictNote = this.discovery.conflicts.length > 0 ? ` ${this.discovery.conflicts.length} same-name conflict${this.discovery.conflicts.length === 1 ? "" : "s"} reported.` : "";
2880
2907
  if (!this.discovery.hasAnyConfig) {
2881
- return `Create a shared .mcp.json, adopt host imports, or quick-add RepoPrompt from this screen.${hostNote}${conflictNote}`;
2908
+ return `Add shared servers to .mcp.json for this project/team or ~/.config/mcp/mcp.json for all projects. Adopt host imports or quick-add RepoPrompt from this screen.${hostNote}${conflictNote}`;
2882
2909
  }
2883
2910
  if (this.discovery.totalServerCount === 0 && this.discovery.imports.length > 0) {
2884
2911
  return `Detected ${this.discovery.imports.length} compatibility import source${this.discovery.imports.length === 1 ? "" : "s"}. Adopt them into Pi or inspect the underlying files.${hostNote}${conflictNote}`;
2885
2912
  }
2886
- return `Shared MCP files are preferred. Pi-owned files are only for compatibility imports and adapter-specific overrides.${hostNote}${conflictNote}`;
2913
+ return `Use .mcp.json for project/team servers or ~/.config/mcp/mcp.json for all projects. Pi-owned files are for compatibility imports and adapter-specific overrides, not another normal setup path.${hostNote}${conflictNote}`;
2887
2914
  }
2888
2915
  visibleActionRange(total) {
2889
2916
  if (total <= COMPACT_ACTION_ROWS) return { start: 0, end: total };
@@ -2913,6 +2940,12 @@ var init_mcp_setup_panel = __esm({
2913
2940
  ],
2914
2941
  previewW
2915
2942
  );
2943
+ case "select-shared-target":
2944
+ return this.formatPreview([
2945
+ action.target === "project" ? "Project target: .mcp.json" : "Global target: ~/.config/mcp/mcp.json",
2946
+ "Known server presets and starter configs will be written to the selected normal MCP setup path.",
2947
+ "Pi-owned mcp.json files remain compatibility and adapter-only override state."
2948
+ ], previewW);
2916
2949
  case "view-example":
2917
2950
  return this.formatPreview([
2918
2951
  "Example shared `.mcp.json`:",
@@ -2925,10 +2958,17 @@ var init_mcp_setup_panel = __esm({
2925
2958
  " }",
2926
2959
  "}",
2927
2960
  "",
2928
- "Use Scaffold project `.mcp.json` when you want a safe empty shell instead of a live example server."
2961
+ "Use Scaffold selected config when you want a safe empty shell instead of a live example server."
2929
2962
  ], previewW);
2930
2963
  case "show-precedence":
2931
2964
  return this.formatPreview([
2965
+ "Recommended shared config:",
2966
+ " project/team: .mcp.json",
2967
+ " all projects: ~/.config/mcp/mcp.json",
2968
+ "",
2969
+ "Advanced compatibility and Pi-owned layers:",
2970
+ " host imports, .agents files, package MCP manifests, and Pi overrides",
2971
+ "",
2932
2972
  "Read order (later entries win):",
2933
2973
  "0. detected host configs (opt-in lowest-precedence fallback)",
2934
2974
  "1. ~/.config/mcp/mcp.json",
@@ -2947,7 +2987,7 @@ var init_mcp_setup_panel = __esm({
2947
2987
  return this.formatPreview(this.getDetectedPaths().length > 0 ? ["Detected paths:", ...this.getDetectedPaths()] : ["No config paths were detected."], previewW);
2948
2988
  case "add-repoprompt": {
2949
2989
  const repoPrompt = this.discovery.repoPrompt;
2950
- const preview = this.callbacks.previewRepoPrompt();
2990
+ const preview = this.callbacks.previewRepoPrompt(this.sharedConfigTarget);
2951
2991
  if (!preview) {
2952
2992
  return this.formatPreview(["RepoPrompt is not available to add from this setup screen."], previewW);
2953
2993
  }
@@ -2956,7 +2996,7 @@ var init_mcp_setup_panel = __esm({
2956
2996
  preview,
2957
2997
  [
2958
2998
  `Executable: ${repoPrompt.executablePath ?? "not found"}`,
2959
- `Target: ${repoPrompt.targetPath ?? "n/a"}`,
2999
+ `Target: ${this.sharedTargetLabel()}`,
2960
3000
  `Server name: ${repoPrompt.serverName ?? "repoprompt"}`
2961
3001
  ],
2962
3002
  previewW
@@ -2967,17 +3007,17 @@ var init_mcp_setup_panel = __esm({
2967
3007
  if (!preset) return this.formatPreview(["Known server preset is unavailable."], previewW);
2968
3008
  return this.formatWritePreview(
2969
3009
  `${preset.name} write preview`,
2970
- this.callbacks.previewKnownServer(preset),
2971
- [preset.summary],
3010
+ this.callbacks.previewKnownServer(preset, this.sharedConfigTarget),
3011
+ [preset.summary, `Target: ${this.sharedTargetLabel()}`],
2972
3012
  previewW
2973
3013
  );
2974
3014
  }
2975
- case "scaffold-project":
3015
+ case "scaffold-shared-config":
2976
3016
  return this.formatWritePreview(
2977
- "Starter project `.mcp.json` write preview",
2978
- this.callbacks.previewStarterProject(),
3017
+ `${this.sharedTargetLabel()} starter write preview`,
3018
+ this.callbacks.previewStarterConfig(this.sharedConfigTarget),
2979
3019
  [
2980
- "This writes a minimal `.mcp.json` in the current project using the shared MCP layout.",
3020
+ "This writes a minimal config at the selected normal MCP setup path.",
2981
3021
  "It intentionally avoids adding a fake placeholder server that would fail on first reload."
2982
3022
  ],
2983
3023
  previewW
@@ -3373,6 +3413,7 @@ var init_mcp_panel = __esm({
3373
3413
  }
3374
3414
  const status = callbacks.getConnectionStatus(serverName);
3375
3415
  const failureMessage = callbacks.getFailureMessage?.(serverName) ?? null;
3416
+ const serverDisabled = isServerDisabled(definition);
3376
3417
  let directCount = 0;
3377
3418
  let directTokens = 0;
3378
3419
  for (const tool of tools) {
@@ -3388,6 +3429,8 @@ var init_mcp_panel = __esm({
3388
3429
  ...definition.includeTools !== void 0 ? { includeTools: definition.includeTools } : {},
3389
3430
  ...definition.excludeTools !== void 0 ? { excludeTools: definition.excludeTools } : {},
3390
3431
  exposeResources: definition.exposeResources !== false,
3432
+ disabled: serverDisabled,
3433
+ wasDisabled: serverDisabled,
3391
3434
  connectionStatus: status,
3392
3435
  failureMessage,
3393
3436
  tools,
@@ -3409,7 +3452,7 @@ var init_mcp_panel = __esm({
3409
3452
  if (this.inactivityTimeout) clearTimeout(this.inactivityTimeout);
3410
3453
  this.inactivityTimeout = setTimeout(() => {
3411
3454
  this.cleanup();
3412
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3455
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3413
3456
  }, _McpPanel.INACTIVITY_MS);
3414
3457
  }
3415
3458
  cleanup() {
@@ -3460,11 +3503,15 @@ var init_mcp_panel = __esm({
3460
3503
  }
3461
3504
  }
3462
3505
  updateDirty() {
3463
- this.dirty = this.servers.some((s) => s.tools.some((t) => t.isDirect !== t.wasDirect));
3506
+ this.dirty = this.servers.some((s) => s.disabled !== s.wasDisabled || s.tools.some((t) => t.isDirect !== t.wasDirect));
3464
3507
  }
3465
3508
  buildResult() {
3466
3509
  const changes = /* @__PURE__ */ new Map();
3510
+ const disabledChanges = /* @__PURE__ */ new Map();
3467
3511
  for (const server2 of this.servers) {
3512
+ if (server2.disabled !== server2.wasDisabled) {
3513
+ disabledChanges.set(server2.name, server2.disabled);
3514
+ }
3468
3515
  const changed = server2.tools.some((t) => t.isDirect !== t.wasDirect);
3469
3516
  if (!changed) continue;
3470
3517
  const directTools = server2.tools.filter((t) => t.isDirect);
@@ -3476,7 +3523,7 @@ var init_mcp_panel = __esm({
3476
3523
  changes.set(server2.name, directTools.map((t) => t.name));
3477
3524
  }
3478
3525
  }
3479
- return { changes, cancelled: false };
3526
+ return { changes, disabledChanges, cancelled: false };
3480
3527
  }
3481
3528
  handleInput(data) {
3482
3529
  this.resetInactivityTimeout();
@@ -3488,7 +3535,7 @@ var init_mcp_panel = __esm({
3488
3535
  }
3489
3536
  if (matchesKey3(data, "ctrl+c")) {
3490
3537
  this.cleanup();
3491
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3538
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3492
3539
  return;
3493
3540
  }
3494
3541
  if (this.keys.save(data)) {
@@ -3546,7 +3593,7 @@ var init_mcp_panel = __esm({
3546
3593
  return;
3547
3594
  }
3548
3595
  this.cleanup();
3549
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3596
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3550
3597
  return;
3551
3598
  }
3552
3599
  if (this.keys.selectUp(data)) {
@@ -3599,6 +3646,16 @@ var init_mcp_panel = __esm({
3599
3646
  if (server2) this.reconnectServer(server2);
3600
3647
  return;
3601
3648
  }
3649
+ if (matchesKey3(data, "ctrl+d")) {
3650
+ const item = this.visibleItems[this.cursorIndex];
3651
+ if (!item || item.type !== "server" || this.authOnly) return;
3652
+ const server2 = this.servers[item.serverIndex];
3653
+ if (!server2) return;
3654
+ server2.disabled = !server2.disabled;
3655
+ this.updateDirty();
3656
+ this.tui.requestRender();
3657
+ return;
3658
+ }
3602
3659
  if (matchesKey3(data, "ctrl+y")) {
3603
3660
  const item = this.visibleItems[this.cursorIndex];
3604
3661
  if (!item) return;
@@ -3737,7 +3794,7 @@ var init_mcp_panel = __esm({
3737
3794
  handleDiscardInput(data) {
3738
3795
  if (matchesKey3(data, "ctrl+c")) {
3739
3796
  this.cleanup();
3740
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3797
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3741
3798
  return;
3742
3799
  }
3743
3800
  if (matchesKey3(data, "escape") || data === "n" || data === "N") {
@@ -3747,7 +3804,7 @@ var init_mcp_panel = __esm({
3747
3804
  if (this.keys.selectConfirm(data)) {
3748
3805
  this.cleanup();
3749
3806
  if (this.discardSelected === 0) {
3750
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3807
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3751
3808
  } else {
3752
3809
  this.done(this.buildResult());
3753
3810
  }
@@ -3755,7 +3812,7 @@ var init_mcp_panel = __esm({
3755
3812
  }
3756
3813
  if (data === "y" || data === "Y") {
3757
3814
  this.cleanup();
3758
- this.done({ cancelled: true, changes: /* @__PURE__ */ new Map() });
3815
+ this.done({ cancelled: true, changes: /* @__PURE__ */ new Map(), disabledChanges: /* @__PURE__ */ new Map() });
3759
3816
  return;
3760
3817
  }
3761
3818
  if (matchesKey3(data, "left") || matchesKey3(data, "right") || matchesKey3(data, "tab")) {
@@ -3949,6 +4006,7 @@ var init_mcp_panel = __esm({
3949
4006
  italic("\u23CE") + " expand/auth",
3950
4007
  italic("ctrl+a") + " auth",
3951
4008
  italic("ctrl+r") + " reconnect",
4009
+ italic("ctrl+d") + " disable/enable",
3952
4010
  ...this.selectedServerHasFailureMessage() ? [italic("ctrl+y") + " copy error"] : [],
3953
4011
  italic("?") + " desc search",
3954
4012
  ...saveLabel ? [italic(saveLabel) + " save"] : [],
@@ -4048,7 +4106,7 @@ var init_mcp_panel = __esm({
4048
4106
  renderConnectionStatus(server2) {
4049
4107
  const t = this.t;
4050
4108
  if (this.authInFlight === server2.name) return ` ${fg2(t.needsAuth, "authenticating")}`;
4051
- if (server2.connectionStatus === "disabled") return ` ${fg2(t.description, "disabled")}`;
4109
+ if (server2.disabled) return ` ${fg2(t.description, "disabled")}`;
4052
4110
  if (server2.connectionStatus === "needs-auth") return ` ${fg2(t.needsAuth, "needs auth")}`;
4053
4111
  if (server2.connectionStatus === "connecting") return ` ${fg2(t.needsAuth, "connecting")}`;
4054
4112
  if (server2.connectionStatus === "failed") return ` ${fg2(t.cancel, "failed")}`;
@@ -4179,6 +4237,89 @@ function wrapError(error, context) {
4179
4237
  ...cause !== void 0 ? { cause } : {}
4180
4238
  });
4181
4239
  }
4240
+ var INPUT_REQUIRED_NEEDS_UI = "input_required_needs_ui";
4241
+ var INPUT_REQUIRED_METHODS = /* @__PURE__ */ new Set([
4242
+ "elicitation/create",
4243
+ "sampling/createMessage"
4244
+ ]);
4245
+ var MAX_INPUT_REQUIRED_SERVER_LENGTH = 96;
4246
+ var MAX_INPUT_REQUIRED_TARGET_LENGTH = 160;
4247
+ var MAX_INPUT_REQUIRED_DETAIL_LENGTH = 128;
4248
+ function asRecord(value) {
4249
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
4250
+ }
4251
+ function boundedText(value, maxLength) {
4252
+ if (typeof value !== "string" || value.length === 0) return void 0;
4253
+ return value.length <= maxLength ? value : `${value.slice(0, maxLength - 1)}\u2026`;
4254
+ }
4255
+ function findEmbeddedInputRequest(error) {
4256
+ let current = error;
4257
+ const seen = /* @__PURE__ */ new Set();
4258
+ for (let depth = 0; depth < 8 && current !== void 0 && current !== null; depth++) {
4259
+ if (seen.has(current)) return void 0;
4260
+ seen.add(current);
4261
+ const record = asRecord(current);
4262
+ if (!record) return void 0;
4263
+ if (record.code === INPUT_REQUIRED_NEEDS_UI) {
4264
+ const details = asRecord(record.details);
4265
+ const inputKey = boundedText(details?.inputKey, MAX_INPUT_REQUIRED_DETAIL_LENGTH);
4266
+ const inputMethod = boundedText(details?.inputMethod, MAX_INPUT_REQUIRED_DETAIL_LENGTH);
4267
+ if (inputKey && inputMethod && INPUT_REQUIRED_METHODS.has(inputMethod)) {
4268
+ const server2 = boundedText(details?.server, MAX_INPUT_REQUIRED_SERVER_LENGTH);
4269
+ const tool = boundedText(details?.tool, MAX_INPUT_REQUIRED_TARGET_LENGTH);
4270
+ const resourceUri = boundedText(details?.resourceUri, MAX_INPUT_REQUIRED_TARGET_LENGTH);
4271
+ return {
4272
+ inputKey,
4273
+ inputMethod,
4274
+ ...server2 ? { server: server2 } : {},
4275
+ ...tool ? { tool } : {},
4276
+ ...resourceUri ? { resourceUri } : {}
4277
+ };
4278
+ }
4279
+ }
4280
+ if (record.code === "CAPABILITY_NOT_SUPPORTED") {
4281
+ const data = asRecord(record.data);
4282
+ const inputKey = boundedText(data?.key, MAX_INPUT_REQUIRED_DETAIL_LENGTH);
4283
+ const inputMethod = boundedText(data?.method, MAX_INPUT_REQUIRED_DETAIL_LENGTH);
4284
+ if (inputKey && inputMethod && INPUT_REQUIRED_METHODS.has(inputMethod)) {
4285
+ return { inputKey, inputMethod };
4286
+ }
4287
+ }
4288
+ current = record.cause;
4289
+ }
4290
+ return void 0;
4291
+ }
4292
+ function getInputRequiredNeedsUiDetails(error, identity) {
4293
+ const request = findEmbeddedInputRequest(error);
4294
+ if (!request) return void 0;
4295
+ const server2 = boundedText(request.server ?? identity.server, MAX_INPUT_REQUIRED_SERVER_LENGTH) ?? "unknown";
4296
+ const tool = boundedText(request.tool ?? identity.tool, MAX_INPUT_REQUIRED_TARGET_LENGTH);
4297
+ const resourceUri = boundedText(request.resourceUri ?? identity.resourceUri, MAX_INPUT_REQUIRED_TARGET_LENGTH);
4298
+ const target = resourceUri ? `read resource "${resourceUri}"` : tool ? `call tool "${tool}"` : "complete the MCP request";
4299
+ const guidance = request.inputMethod === "elicitation/create" ? "Run this call in an interactive Pi session with elicitation enabled, then retry." : "Run this call in an interactive Pi session with the required MCP capability enabled, then retry.";
4300
+ const message = `MCP server "${server2}" requested input to ${target}, but this session has no handler for "${request.inputMethod}" (input "${request.inputKey}"). ${guidance}`;
4301
+ return {
4302
+ error: INPUT_REQUIRED_NEEDS_UI,
4303
+ server: server2,
4304
+ ...tool ? { tool } : {},
4305
+ ...resourceUri ? { resourceUri } : {},
4306
+ inputKey: request.inputKey,
4307
+ inputMethod: request.inputMethod,
4308
+ message
4309
+ };
4310
+ }
4311
+ var InputRequiredNeedsUiError = class extends McpUiError {
4312
+ constructor(details, cause) {
4313
+ super(details.message, {
4314
+ code: INPUT_REQUIRED_NEEDS_UI,
4315
+ context: { ...details },
4316
+ recoveryHint: "Run this call in an interactive Pi session with the required MCP input handler enabled, then retry.",
4317
+ ...cause ? { cause } : {}
4318
+ });
4319
+ this.details = details;
4320
+ this.name = "InputRequiredNeedsUiError";
4321
+ }
4322
+ };
4182
4323
 
4183
4324
  // packages/coding-agent/src/humain/mcp-adapter/vendor/logger.ts
4184
4325
  var LEVEL_PRIORITY = {
@@ -4968,14 +5109,14 @@ function readNpxCachePayload(cachePath) {
4968
5109
  return null;
4969
5110
  }
4970
5111
  }
4971
- function asRecord(value) {
5112
+ function asRecord2(value) {
4972
5113
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
4973
5114
  }
4974
5115
  function createCacheEntries() {
4975
5116
  return /* @__PURE__ */ Object.create(null);
4976
5117
  }
4977
5118
  function toNpxCacheEntry(value) {
4978
- const raw = asRecord(value);
5119
+ const raw = asRecord2(value);
4979
5120
  if (!raw) return null;
4980
5121
  if (typeof raw.resolvedBin !== "string") return null;
4981
5122
  if (typeof raw.resolvedAt !== "number" || !Number.isFinite(raw.resolvedAt)) return null;
@@ -4989,9 +5130,9 @@ function toNpxCacheEntry(value) {
4989
5130
  };
4990
5131
  }
4991
5132
  function toNpxCache(value) {
4992
- const raw = asRecord(value);
5133
+ const raw = asRecord2(value);
4993
5134
  if (!raw || raw.version !== CACHE_VERSION) return null;
4994
- const rawEntries = asRecord(raw.entries);
5135
+ const rawEntries = asRecord2(raw.entries);
4995
5136
  if (!rawEntries) return null;
4996
5137
  const entries = createCacheEntries();
4997
5138
  for (const [key, rawEntry] of Object.entries(rawEntries)) {
@@ -5002,7 +5143,7 @@ function toNpxCache(value) {
5002
5143
  }
5003
5144
  function clearLegacyCache() {
5004
5145
  const cachePath = getNpxCachePath();
5005
- const raw = asRecord(readNpxCachePayload(cachePath));
5146
+ const raw = asRecord2(readNpxCachePayload(cachePath));
5006
5147
  if (raw?.version !== 1) return false;
5007
5148
  try {
5008
5149
  unlinkSync(cachePath);
@@ -8713,6 +8854,9 @@ function appendStderrTail(tail, chunk) {
8713
8854
  return combined.length > MAX_CAPTURED_STDERR_BYTES ? Buffer.from(combined.subarray(combined.length - MAX_CAPTURED_STDERR_BYTES)) : combined;
8714
8855
  }
8715
8856
  var KEEP_ALIVE_REFRESH_TIMEOUT_MS = 5e3;
8857
+ var LISTEN_RETRY_DELAY_MS = 5e3;
8858
+ var RECENT_RESOURCE_TTL_MS = 10 * 6e4;
8859
+ var MAX_RESOURCE_SUBSCRIPTIONS = 32;
8716
8860
  function isTransientHttpConnectError(error) {
8717
8861
  let current = error;
8718
8862
  while (current instanceof Error) {
@@ -8729,6 +8873,7 @@ var McpServerManager = class {
8729
8873
  this.connectPromises = /* @__PURE__ */ new Map();
8730
8874
  this.reconnectPromises = /* @__PURE__ */ new Map();
8731
8875
  this.uiStreamListeners = /* @__PURE__ */ new Map();
8876
+ this.resourceUpdatedListeners = /* @__PURE__ */ new Map();
8732
8877
  this.pendingMetadataPublications = /* @__PURE__ */ new Map();
8733
8878
  this.authStorageOptions = {};
8734
8879
  this.acceptedUrlElicitations = /* @__PURE__ */ new Map();
@@ -8743,6 +8888,9 @@ var McpServerManager = class {
8743
8888
  setMetadataListChangedListener(listener) {
8744
8889
  this.metadataListChangedListener = listener;
8745
8890
  }
8891
+ setListenStateChangedListener(listener) {
8892
+ this.listenStateChangedListener = listener;
8893
+ }
8746
8894
  publishMetadataChanged(name, expectedConnection, reason) {
8747
8895
  const current = this.connections.get(name);
8748
8896
  if (current !== expectedConnection || current.status !== "connected") return false;
@@ -8830,6 +8978,10 @@ var McpServerManager = class {
8830
8978
  throw new Error(`MCP connection for ${name} was closed while connecting`);
8831
8979
  }
8832
8980
  this.connections.set(name, connection);
8981
+ this.watchListenSubscription(name, connection, connection.listenSubscription);
8982
+ if ([...this.resourceUpdatedListeners.values()].some((registration) => registration.serverName === name)) {
8983
+ void this.ensureListen(name, connection);
8984
+ }
8833
8985
  return connection;
8834
8986
  } finally {
8835
8987
  if (this.connectPromises.get(name) === promise) this.connectPromises.delete(name);
@@ -8876,6 +9028,7 @@ var McpServerManager = class {
8876
9028
  if (current !== expectedConnection || current.status !== "connected") {
8877
9029
  return "superseded";
8878
9030
  }
9031
+ await this.ensureListen(name, expectedConnection);
8879
9032
  const requestOptions = this.buildRequestOptions(expectedConnection.definition, signal);
8880
9033
  const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
8881
9034
  const healthOptions = {
@@ -8947,6 +9100,198 @@ var McpServerManager = class {
8947
9100
  this.pendingMetadataPublications.delete(name);
8948
9101
  }
8949
9102
  }
9103
+ setListenState(name, connection, state) {
9104
+ if (connection.listenState === state) return;
9105
+ connection.listenState = state;
9106
+ if (this.connections.get(name) === connection) {
9107
+ try {
9108
+ this.listenStateChangedListener?.(name, state);
9109
+ } catch {
9110
+ }
9111
+ }
9112
+ }
9113
+ catalogListenFilter(connection) {
9114
+ const capabilities = connection.client.getServerCapabilities?.();
9115
+ return {
9116
+ ...capabilities?.tools?.listChanged ? { toolsListChanged: true } : {},
9117
+ ...capabilities?.prompts?.listChanged ? { promptsListChanged: true } : {},
9118
+ ...capabilities?.resources?.listChanged ? { resourcesListChanged: true } : {}
9119
+ };
9120
+ }
9121
+ currentListenFilter(name, connection) {
9122
+ const now = Date.now();
9123
+ const recent = connection.recentResourceUris;
9124
+ if (recent) {
9125
+ for (const [uri, touchedAt] of recent) {
9126
+ if (now - touchedAt > RECENT_RESOURCE_TTL_MS) recent.delete(uri);
9127
+ }
9128
+ }
9129
+ const openResourceUris = [...new Set(
9130
+ [...this.resourceUpdatedListeners.values()].filter((registration) => registration.serverName === name).map((registration) => registration.uri)
9131
+ )].slice(-MAX_RESOURCE_SUBSCRIPTIONS);
9132
+ const openSet = new Set(openResourceUris);
9133
+ const recentSlots = MAX_RESOURCE_SUBSCRIPTIONS - openResourceUris.length;
9134
+ const recentResourceUris = recentSlots > 0 ? [...recent?.keys() ?? []].filter((uri) => !openSet.has(uri)).slice(-recentSlots) : [];
9135
+ const resourceSubscriptions = [...openResourceUris, ...recentResourceUris];
9136
+ return {
9137
+ ...this.catalogListenFilter(connection),
9138
+ ...resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {}
9139
+ };
9140
+ }
9141
+ watchListenSubscription(name, connection, subscription) {
9142
+ if (!subscription) return;
9143
+ void subscription.closed.then((cause) => {
9144
+ if (this.stopped || this.connections.get(name) !== connection || connection.status !== "connected" || connection.listenSubscription !== subscription) return;
9145
+ if (cause === "remote") {
9146
+ const staleUris = connection.listenFilter?.resourceSubscriptions ?? [];
9147
+ if (staleUris.length > 0) connection.resourceReadRefreshUris = new Set(staleUris);
9148
+ connection.listenCatalogStale = true;
9149
+ connection.listenRetryAfter = Date.now();
9150
+ this.setListenState(name, connection, "dropped");
9151
+ } else if (cause === "graceful" || connection.listenState !== "re-establishing") {
9152
+ connection.listenStopped = true;
9153
+ this.setListenState(name, connection, "not-listening");
9154
+ }
9155
+ });
9156
+ }
9157
+ /** Quietly repairs a modern catalog listen at an existing activity boundary. */
9158
+ async ensureListen(name, expectedConnection) {
9159
+ if (this.stopped || this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected" || expectedConnection.listenStopped || expectedConnection.client.getProtocolEra?.() !== "modern") return;
9160
+ const filter = this.currentListenFilter(name, expectedConnection);
9161
+ if (Object.keys(filter).length === 0) {
9162
+ this.setListenState(name, expectedConnection, "not-listening");
9163
+ return;
9164
+ }
9165
+ if (expectedConnection.listenPromise) {
9166
+ await expectedConnection.listenPromise;
9167
+ return this.ensureListen(name, expectedConnection);
9168
+ }
9169
+ if ((expectedConnection.listenRetryAfter ?? 0) > Date.now()) return;
9170
+ const sameFilter = isDeepStrictEqual(expectedConnection.listenFilter, filter);
9171
+ if (expectedConnection.listenState === "active" && sameFilter) {
9172
+ if (!expectedConnection.listenCatalogStale) return;
9173
+ const attempt2 = (async () => {
9174
+ this.setListenState(name, expectedConnection, "re-establishing");
9175
+ const confirmed = await this.reconcileCatalogAfterListen(name, expectedConnection, this.buildRequestOptions(expectedConnection.definition));
9176
+ if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
9177
+ this.setListenState(name, expectedConnection, "active");
9178
+ })().finally(() => {
9179
+ if (expectedConnection.listenPromise === attempt2) delete expectedConnection.listenPromise;
9180
+ });
9181
+ expectedConnection.listenPromise = attempt2;
9182
+ return attempt2;
9183
+ }
9184
+ const attempt = (async () => {
9185
+ const recoverDroppedListen = expectedConnection.listenState === "dropped";
9186
+ this.setListenState(name, expectedConnection, "re-establishing");
9187
+ const previous = expectedConnection.listenSubscription;
9188
+ if (previous) await previous.close().catch(() => {
9189
+ });
9190
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
9191
+ try {
9192
+ const requestOptions = this.buildRequestOptions(expectedConnection.definition);
9193
+ const subscription = await expectedConnection.client.listen(
9194
+ filter,
9195
+ {
9196
+ ...requestOptions,
9197
+ timeout: Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS)
9198
+ }
9199
+ );
9200
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") {
9201
+ await subscription.close().catch(() => {
9202
+ });
9203
+ return;
9204
+ }
9205
+ expectedConnection.listenSubscription = subscription;
9206
+ expectedConnection.listenFilter = filter;
9207
+ delete expectedConnection.listenStopped;
9208
+ delete expectedConnection.listenRetryAfter;
9209
+ this.watchListenSubscription(name, expectedConnection, subscription);
9210
+ const confirmed = recoverDroppedListen ? await this.reconcileCatalogAfterListen(name, expectedConnection, requestOptions) : true;
9211
+ if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
9212
+ this.setListenState(name, expectedConnection, "active");
9213
+ } catch (error) {
9214
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
9215
+ if (expectedConnection.listenSubscription) await expectedConnection.listenSubscription.close().catch(() => {
9216
+ });
9217
+ expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
9218
+ this.setListenState(name, expectedConnection, "dropped");
9219
+ logger.debug(`MCP: catalog listen repair failed for ${name}: ${error instanceof Error ? error.message : String(error)}`);
9220
+ }
9221
+ })().finally(() => {
9222
+ if (expectedConnection.listenPromise === attempt) delete expectedConnection.listenPromise;
9223
+ });
9224
+ expectedConnection.listenPromise = attempt;
9225
+ return attempt;
9226
+ }
9227
+ async reconcileCatalogAfterListen(name, expectedConnection, requestOptions) {
9228
+ const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
9229
+ const refreshSignal = combineAbortSignals(requestOptions?.signal, AbortSignal.timeout(timeout));
9230
+ const refreshOptions = {
9231
+ ...requestOptions,
9232
+ timeout,
9233
+ cacheMode: "refresh",
9234
+ ...refreshSignal ? { signal: refreshSignal } : {}
9235
+ };
9236
+ const [toolResult, resources, promptResult] = await Promise.allSettled([
9237
+ this.fetchAllTools(expectedConnection.client, refreshOptions),
9238
+ this.fetchAllResources(expectedConnection.client, refreshOptions, true),
9239
+ this.fetchAllPrompts(expectedConnection.client, refreshOptions)
9240
+ ]);
9241
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
9242
+ const nextTools = toolResult.status === "fulfilled" ? toolResult.value : void 0;
9243
+ const nextResources = resources.status === "fulfilled" ? resources.value : void 0;
9244
+ const nextPrompts = promptResult.status === "fulfilled" && !promptResult.value.failed ? promptResult.value : void 0;
9245
+ const confirmed = nextTools !== void 0 && nextResources !== void 0 && nextPrompts !== void 0;
9246
+ const changed = nextTools !== void 0 && (!isDeepStrictEqual(expectedConnection.tools, nextTools.tools) || !isDeepStrictEqual(expectedConnection.toolListHints, nextTools.hints)) || nextResources !== void 0 && !isDeepStrictEqual(expectedConnection.resources, nextResources) || nextPrompts !== void 0 && (!isDeepStrictEqual(expectedConnection.prompts, nextPrompts.prompts) || expectedConnection.promptDiscoveryFailed !== false);
9247
+ if (!changed) {
9248
+ this.retryPendingMetadataPublication(name, expectedConnection);
9249
+ if (confirmed) delete expectedConnection.listenCatalogStale;
9250
+ else expectedConnection.listenCatalogStale = true;
9251
+ return confirmed;
9252
+ }
9253
+ if (nextTools !== void 0) {
9254
+ expectedConnection.tools = nextTools.tools;
9255
+ expectedConnection.toolListHints = nextTools.hints;
9256
+ expectedConnection.toolsRevision = (expectedConnection.toolsRevision ?? 0) + 1;
9257
+ }
9258
+ if (nextResources !== void 0) expectedConnection.resources = nextResources;
9259
+ if (nextPrompts !== void 0) {
9260
+ expectedConnection.prompts = nextPrompts.prompts;
9261
+ expectedConnection.promptDiscoveryFailed = false;
9262
+ }
9263
+ if (confirmed) delete expectedConnection.listenCatalogStale;
9264
+ else expectedConnection.listenCatalogStale = true;
9265
+ this.metadataListChangedListener?.(name, "listen-recovered");
9266
+ this.pendingMetadataPublications.delete(name);
9267
+ return confirmed;
9268
+ }
9269
+ async prepareResourceUse(name, uri, expectedConnection) {
9270
+ if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
9271
+ const refreshRead = expectedConnection.listenState === "dropped" || expectedConnection.listenState === "re-establishing" || expectedConnection.resourceReadRefreshUris?.has(uri) === true;
9272
+ const recent = expectedConnection.recentResourceUris ?? /* @__PURE__ */ new Map();
9273
+ recent.delete(uri);
9274
+ recent.set(uri, Date.now());
9275
+ while (recent.size > MAX_RESOURCE_SUBSCRIPTIONS) {
9276
+ const oldest = recent.keys().next().value;
9277
+ if (oldest === void 0) break;
9278
+ recent.delete(oldest);
9279
+ }
9280
+ expectedConnection.recentResourceUris = recent;
9281
+ await this.ensureListen(name, expectedConnection);
9282
+ expectedConnection.resourceReadRefreshUris?.delete(uri);
9283
+ return refreshRead;
9284
+ }
9285
+ registerResourceUpdatedListener(token, serverName, uri, listener) {
9286
+ this.removeResourceUpdatedListener(token);
9287
+ this.resourceUpdatedListeners.set(token, { serverName, uri, listener });
9288
+ const connection = this.connections.get(serverName);
9289
+ if (!connection || connection.status !== "connected") return;
9290
+ void this.prepareResourceUse(serverName, uri, connection);
9291
+ }
9292
+ removeResourceUpdatedListener(token) {
9293
+ this.resourceUpdatedListeners.delete(token);
9294
+ }
8950
9295
  async doReconnect(name, definition, staleConnection, signal) {
8951
9296
  throwIfAborted(signal);
8952
9297
  const current = this.connections.get(name);
@@ -9031,6 +9376,7 @@ var McpServerManager = class {
9031
9376
  lastUsedAt: Date.now(),
9032
9377
  inFlight: 0,
9033
9378
  status: "needs-auth",
9379
+ listenState: "disconnected",
9034
9380
  credentialsInvalidated: invalidated
9035
9381
  };
9036
9382
  }
@@ -9051,6 +9397,8 @@ var McpServerManager = class {
9051
9397
  }
9052
9398
  this.attachAdapterNotificationHandlers(name, client);
9053
9399
  const instructions = client.getInstructions?.();
9400
+ const protocolEra = client.getProtocolEra?.();
9401
+ const autoOpenedSubscription = client.autoOpenedSubscription;
9054
9402
  const connection = {
9055
9403
  client,
9056
9404
  transport,
@@ -9062,8 +9410,13 @@ var McpServerManager = class {
9062
9410
  ...instructions !== void 0 ? { instructions } : {},
9063
9411
  lastUsedAt: Date.now(),
9064
9412
  inFlight: 0,
9065
- status: "connected"
9413
+ status: "connected",
9414
+ listenState: protocolEra === "modern" ? autoOpenedSubscription ? "active" : "not-listening" : "legacy",
9415
+ ...autoOpenedSubscription ? {
9416
+ listenSubscription: autoOpenedSubscription
9417
+ } : {}
9066
9418
  };
9419
+ if (autoOpenedSubscription) connection.listenFilter = this.catalogListenFilter(connection);
9067
9420
  client.onclose = () => {
9068
9421
  if (this.connections.get(name) === connection) {
9069
9422
  connection.status = "closed";
@@ -9106,6 +9459,7 @@ var McpServerManager = class {
9106
9459
  lastUsedAt: Date.now(),
9107
9460
  inFlight: 0,
9108
9461
  status: "needs-auth",
9462
+ listenState: "disconnected",
9109
9463
  credentialsInvalidated: invalidated
9110
9464
  };
9111
9465
  }
@@ -9316,7 +9670,16 @@ var McpServerManager = class {
9316
9670
  this.authStorageOptions,
9317
9671
  this.oauthRuntime?.signal
9318
9672
  );
9319
- let authState = supportsOAuth(definition) ? definition.auth === void 0 ? { status: "implicit-deferred" } : { status: "explicit", provider: createAuthProvider() } : { status: "disabled" };
9673
+ let implicitStoredAuth;
9674
+ if (definition.auth === void 0 && supportsOAuth(definition)) {
9675
+ try {
9676
+ implicitStoredAuth = inspectAuthForUrl(serverName, serverUrl, this.authStorageOptions);
9677
+ } catch {
9678
+ }
9679
+ }
9680
+ const hasImplicitStoredTokens = implicitStoredAuth?.status === "present" && implicitStoredAuth.entry.tokens !== void 0;
9681
+ if (hasImplicitStoredTokens) invalidateAuthEntryCache(serverName);
9682
+ let authState = supportsOAuth(definition) ? definition.auth === void 0 ? hasImplicitStoredTokens ? { status: "implicit-stored", provider: createAuthProvider() } : { status: "implicit-deferred" } : { status: "explicit", provider: createAuthProvider() } : { status: "disabled" };
9320
9683
  const attempt = async (kind2) => {
9321
9684
  const authProvider = "provider" in authState ? authState.provider : void 0;
9322
9685
  const transportOptions = {
@@ -9421,7 +9784,7 @@ var McpServerManager = class {
9421
9784
  return { prompts: [], failed: true };
9422
9785
  }
9423
9786
  }
9424
- async fetchAllResources(client, requestOptions) {
9787
+ async fetchAllResources(client, requestOptions, strict = false) {
9425
9788
  const capabilities = client.getServerCapabilities?.();
9426
9789
  if (!capabilities?.resources) return [];
9427
9790
  try {
@@ -9438,10 +9801,24 @@ var McpServerManager = class {
9438
9801
  throwIfAborted(requestOptions.signal);
9439
9802
  }
9440
9803
  if (isUnauthorizedHttpError(error)) throw error;
9804
+ if (strict) throw error;
9441
9805
  return [];
9442
9806
  }
9443
9807
  }
9444
9808
  attachAdapterNotificationHandlers(serverName, client) {
9809
+ client.setNotificationHandler("notifications/resources/updated", (notification) => {
9810
+ const uri = notification.params.uri;
9811
+ const connection = this.connections.get(serverName);
9812
+ if (!connection || connection.client !== client || connection.status !== "connected") return;
9813
+ for (const registration of this.resourceUpdatedListeners.values()) {
9814
+ if (registration.serverName === serverName && registration.uri === uri) {
9815
+ try {
9816
+ registration.listener(serverName, uri);
9817
+ } catch {
9818
+ }
9819
+ }
9820
+ }
9821
+ });
9445
9822
  client.setNotificationHandler(
9446
9823
  SERVER_STREAM_RESULT_PATCH_METHOD,
9447
9824
  { params: serverStreamResultPatchNotificationSchema.shape.params },
@@ -9466,6 +9843,7 @@ var McpServerManager = class {
9466
9843
  try {
9467
9844
  this.touch(name);
9468
9845
  this.incrementInFlight(name);
9846
+ await this.ensureListen(name, connection);
9469
9847
  return await connection.client.getPrompt(
9470
9848
  { name: promptName, ...args ? { arguments: args } : {} },
9471
9849
  this.getRequestOptions(name, signal)
@@ -9486,7 +9864,12 @@ var McpServerManager = class {
9486
9864
  try {
9487
9865
  this.touch(name);
9488
9866
  this.incrementInFlight(name);
9489
- return await connection.client.readResource({ uri }, this.getRequestOptions(name, signal));
9867
+ const refreshRead = await this.prepareResourceUse(name, uri, connection);
9868
+ const requestOptions = this.getRequestOptions(name, signal);
9869
+ return await connection.client.readResource(
9870
+ { uri },
9871
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions
9872
+ );
9490
9873
  } finally {
9491
9874
  this.decrementInFlight(name);
9492
9875
  this.touch(name);
@@ -9546,6 +9929,7 @@ var McpServerManager = class {
9546
9929
  const lateResults = await Promise.allSettled(lateNames.map((name) => this.close(name)));
9547
9930
  const failures = [...pendingResults, ...results, ...lateResults].flatMap((result) => result.status === "rejected" ? [result.reason] : []).filter((error) => this.containsCleanupFailure(error));
9548
9931
  this.uiStreamListeners.clear();
9932
+ this.resourceUpdatedListeners.clear();
9549
9933
  this.acceptedUrlElicitations.clear();
9550
9934
  this.pendingMetadataPublications.clear();
9551
9935
  this.samplingConfig = void 0;
@@ -10320,7 +10704,14 @@ var UiResourceHandler = class {
10320
10704
  ...options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}
10321
10705
  },
10322
10706
  serverName,
10323
- (connection) => connection.client.readResource({ uri }, this.manager.getRequestOptions(serverName, options.signal))
10707
+ async (connection) => {
10708
+ const refreshRead = await this.manager.prepareResourceUse?.(serverName, uri, connection);
10709
+ const requestOptions = this.manager.getRequestOptions(serverName, options.signal);
10710
+ return connection.client.readResource(
10711
+ { uri },
10712
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions
10713
+ );
10714
+ }
10324
10715
  );
10325
10716
  } finally {
10326
10717
  this.manager.decrementInFlight(serverName);
@@ -10331,6 +10722,10 @@ var UiResourceHandler = class {
10331
10722
  }
10332
10723
  } catch (error) {
10333
10724
  if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError) throw error;
10725
+ const inputRequired = getInputRequiredNeedsUiDetails(error, { server: serverName, resourceUri: uri });
10726
+ if (inputRequired) {
10727
+ throw new InputRequiredNeedsUiError(inputRequired, error instanceof Error ? error : void 0);
10728
+ }
10334
10729
  const message = error instanceof Error ? error.message : String(error);
10335
10730
  log.error("Failed to read resource", error instanceof Error ? error : void 0);
10336
10731
  throw new ResourceFetchError(uri, message, {
@@ -10538,7 +10933,9 @@ function createMcpStatusSnapshot(state) {
10538
10933
  servers.push({
10539
10934
  name,
10540
10935
  status,
10936
+ listenState: connection?.status === "connected" ? connection.listenState : "disconnected",
10541
10937
  toolCount,
10938
+ ...connection?.status === "connected" && connection.listenCatalogStale ? { catalogStale: true } : {},
10542
10939
  ...resourceCount !== void 0 ? { resourceCount } : {},
10543
10940
  ...status === "failed" && failedAgoSeconds !== null ? { failedAgoSeconds } : {},
10544
10941
  disabled
@@ -10725,6 +11122,10 @@ async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), options =
10725
11122
  notifyToolMetadataUpdated(state, serverName, reason);
10726
11123
  updateStatusBar(state);
10727
11124
  });
11125
+ manager.setListenStateChangedListener?.(() => {
11126
+ if (!owner.isActive()) return;
11127
+ updateStatusBar(state);
11128
+ });
10728
11129
  owner.addCleanup(() => lifecycle.gracefulShutdown());
10729
11130
  owner.addCleanup(() => {
10730
11131
  if (state.uiServer) {
@@ -11087,6 +11488,7 @@ async function lazyConnect(state, serverName, signal) {
11087
11488
  return false;
11088
11489
  }
11089
11490
  if (connection?.status === "connected") {
11491
+ await state.manager.ensureListen?.(serverName, connection);
11090
11492
  updateServerMetadata(state, serverName);
11091
11493
  markKeepAliveAfterConnect(state, serverName);
11092
11494
  return true;
@@ -11209,6 +11611,13 @@ function canRenderPanel(ctx) {
11209
11611
  async function showStatus(state, ctx) {
11210
11612
  if (!ctx.hasUI) return;
11211
11613
  const lines = ["MCP Server Status:", ""];
11614
+ if (!state.programmaticConfig) {
11615
+ lines.push(
11616
+ "Shared MCP config: .mcp.json for this project/team or ~/.config/mcp/mcp.json for all projects.",
11617
+ "Pi-owned files hold compatibility imports and adapter-specific overrides.",
11618
+ ""
11619
+ );
11620
+ }
11212
11621
  for (const name of Object.keys(state.config.mcpServers)) {
11213
11622
  const definition = state.config.mcpServers[name];
11214
11623
  if (isServerDisabled(definition)) {
@@ -11219,11 +11628,11 @@ async function showStatus(state, ctx) {
11219
11628
  const metadata = state.toolMetadata.get(name);
11220
11629
  const toolCount = metadata?.length ?? 0;
11221
11630
  const failedAgo = getFailureAgeSeconds(state, name);
11222
- let status = "not connected";
11631
+ let status = "not listening (disconnected)";
11223
11632
  let statusIcon = "\u25CB";
11224
11633
  let failed = false;
11225
11634
  if (connection?.status === "connected") {
11226
- status = "connected";
11635
+ status = connection.listenState === "active" ? connection.listenCatalogStale ? "connected; listen active; catalog may be stale" : "connected; listen active" : connection.listenState === "dropped" ? "connected; catalog may be stale \u2014 will reconcile on next keep-alive or tool use" : connection.listenState === "re-establishing" ? "connected; re-establishing listen" : connection.listenState === "legacy" ? "connected; legacy notification path" : "connected; not listening for catalog updates";
11227
11636
  statusIcon = "\u2713";
11228
11637
  } else if (connection?.status === "needs-auth") {
11229
11638
  status = "needs auth";
@@ -11234,14 +11643,17 @@ async function showStatus(state, ctx) {
11234
11643
  statusIcon = "\u2717";
11235
11644
  failed = true;
11236
11645
  } else if (metadata !== void 0) {
11237
- status = "cached";
11646
+ status = "cached; not listening (disconnected)";
11238
11647
  }
11239
- const toolSuffix = failed ? "" : ` (${toolCount} tools${status === "cached" ? ", cached" : ""})`;
11648
+ const toolSuffix = failed ? "" : ` (${toolCount} tools${status.startsWith("cached") ? ", cached" : ""})`;
11240
11649
  lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
11241
11650
  }
11651
+ if (state.config.settings?.freezeDirectTools === true) {
11652
+ lines.push("", "Direct tools frozen; active registrations may differ from current metadata.");
11653
+ }
11242
11654
  if (Object.keys(state.config.mcpServers).length === 0) {
11243
11655
  lines.push("No MCP servers configured");
11244
- lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
11656
+ lines.push("Run /mcp setup to add a server to .mcp.json or ~/.config/mcp/mcp.json");
11245
11657
  }
11246
11658
  ctx.ui.notify(lines.join("\n"), "info");
11247
11659
  }
@@ -11530,15 +11942,17 @@ async function manageBearerToken(action, serverName, state, ctx) {
11530
11942
  function buildSharedConfigNoticeLines(configOverridePath, cwd) {
11531
11943
  const discovery = getMcpStandardConfigSummary(configOverridePath, cwd);
11532
11944
  const onboardingState = loadOnboardingState();
11533
- if (!discovery.hasSharedServers || onboardingState.sharedConfigHintShown) {
11945
+ const sharedSources = discovery.sources.filter(
11946
+ (source) => (source.id === "shared-project" || source.id === "shared-global") && source.serverCount > 0
11947
+ );
11948
+ if (sharedSources.length === 0 || onboardingState.sharedConfigHintShown) {
11534
11949
  return { lines: [], fingerprint: null };
11535
11950
  }
11536
- const sharedSources = discovery.sources.filter((source) => source.kind === "shared" && source.serverCount > 0);
11537
11951
  const sourceList = sharedSources.map((source) => source.path).join(", ");
11538
11952
  return {
11539
11953
  lines: [
11540
11954
  `Using standard MCP config from ${sourceList}.`,
11541
- "Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed."
11955
+ "Use .mcp.json for project/team config or ~/.config/mcp/mcp.json for all projects. Pi only writes compatibility imports and adapter-specific overrides into Pi-owned files when needed."
11542
11956
  ],
11543
11957
  fingerprint: discovery.fingerprint
11544
11958
  };
@@ -11559,34 +11973,34 @@ async function openMcpSetup(state, pi, ctx, configOverridePath, mode = "setup",
11559
11973
  let configChanged = false;
11560
11974
  const callbacks = {
11561
11975
  previewImports: (imports) => previewCompatibilityImports(imports, configOverridePath),
11562
- previewStarterProject: () => previewStarterProjectConfig(ctx.cwd),
11563
- previewRepoPrompt: () => {
11976
+ previewStarterConfig: (target) => previewStarterSharedConfig(target, ctx.cwd),
11977
+ previewRepoPrompt: (target) => {
11564
11978
  const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;
11565
11979
  if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) return null;
11566
- return previewSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
11980
+ return previewSharedServerEntry(getSharedConfigPath(target, ctx.cwd), repoPrompt.serverName, repoPrompt.entry);
11567
11981
  },
11568
- previewKnownServer: (preset) => previewSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry),
11982
+ previewKnownServer: (preset, target) => previewSharedServerEntry(getSharedConfigPath(target, ctx.cwd), preset.id, preset.entry),
11569
11983
  adoptImports: async (imports) => {
11570
11984
  const result = ensureCompatibilityImports(imports, configOverridePath);
11571
11985
  if (result.added.length > 0) configChanged = true;
11572
11986
  return result;
11573
11987
  },
11574
- scaffoldProjectConfig: async () => {
11575
- const path2 = writeStarterProjectConfig(ctx.cwd);
11988
+ scaffoldConfig: async (target) => {
11989
+ const path2 = writeStarterSharedConfig(target, ctx.cwd);
11576
11990
  configChanged = true;
11577
11991
  return { path: path2 };
11578
11992
  },
11579
- addRepoPrompt: async () => {
11993
+ addRepoPrompt: async (target) => {
11580
11994
  const repoPrompt = getMcpDiscoverySummary(configOverridePath, ctx.cwd, options).repoPrompt;
11581
11995
  if (!repoPrompt.entry || !repoPrompt.targetPath || !repoPrompt.serverName) {
11582
11996
  throw new Error("RepoPrompt is not available to add from this setup screen.");
11583
11997
  }
11584
- const path2 = writeSharedServerEntry(repoPrompt.targetPath, repoPrompt.serverName, repoPrompt.entry);
11998
+ const path2 = writeSharedServerEntry(getSharedConfigPath(target, ctx.cwd), repoPrompt.serverName, repoPrompt.entry);
11585
11999
  configChanged = true;
11586
12000
  return { path: path2, serverName: repoPrompt.serverName };
11587
12001
  },
11588
- addKnownServer: async (preset) => {
11589
- const path2 = writeSharedServerEntry(getProjectConfigPath(ctx.cwd), preset.id, preset.entry);
12002
+ addKnownServer: async (preset, target) => {
12003
+ const path2 = writeSharedServerEntry(getSharedConfigPath(target, ctx.cwd), preset.id, preset.entry);
11590
12004
  configChanged = true;
11591
12005
  return { path: path2, serverName: preset.name };
11592
12006
  },
@@ -11689,6 +12103,18 @@ async function openMcpPanel(state, pi, ctx, configOverridePath, onDirectToolsCon
11689
12103
  (tui, _theme, keybindings, done) => {
11690
12104
  return createMcpPanel2(config, cache, provenanceMap, callbacks, tui, (result) => {
11691
12105
  void (async () => {
12106
+ if (!result.cancelled && result.disabledChanges.size > 0) {
12107
+ for (const [serverName, disabled] of result.disabledChanges) {
12108
+ try {
12109
+ const override = writeProjectServerDisabledOverride(configPath, ctx.cwd, serverName, disabled);
12110
+ if (override.changed) {
12111
+ configChanged = true;
12112
+ }
12113
+ } catch (error) {
12114
+ ctx.ui.notify(`Failed to ${disabled ? "disable" : "enable"} server "${serverName}": ${error instanceof Error ? error.message : String(error)}`, "error");
12115
+ }
12116
+ }
12117
+ }
11692
12118
  if (!result.cancelled && result.changes.size > 0) {
11693
12119
  writeDirectToolsConfig(result.changes, provenanceMap, config);
11694
12120
  await onDirectToolsConfigChanged?.(result.changes);
@@ -12171,7 +12597,7 @@ async function compactMcpResultOmission(summary, marker, maxBytes) {
12171
12597
  }
12172
12598
  async function summarizeMcpResult(result, raw, rawBytes) {
12173
12599
  const { path: fullResultPath, error: resultWriteError } = await saveArtifact("mcp-result", raw);
12174
- const record = asRecord2(result);
12600
+ const record = asRecord3(result);
12175
12601
  const content = Array.isArray(record?.content) ? record.content : [];
12176
12602
  const summary = {
12177
12603
  omitted: true,
@@ -12198,7 +12624,7 @@ async function summarizeMcpResult(result, raw, rawBytes) {
12198
12624
  }
12199
12625
  function summarizeContent(content) {
12200
12626
  const summaries = content.slice(0, CONTENT_SUMMARY_LIMIT).map((block) => {
12201
- const record = asRecord2(block);
12627
+ const record = asRecord3(block);
12202
12628
  if (!record) return { type: typeof block, omitted: true };
12203
12629
  if (record.type === "text") {
12204
12630
  const text = typeof record.text === "string" ? record.text : "";
@@ -12216,7 +12642,7 @@ function summarizeContent(content) {
12216
12642
  return summaries;
12217
12643
  }
12218
12644
  function summarizeValue(value) {
12219
- const record = asRecord2(value);
12645
+ const record = asRecord3(value);
12220
12646
  if (!record) {
12221
12647
  return { type: value === null ? "null" : typeof value, estimatedBytes: estimateValueBytes(value), omitted: true };
12222
12648
  }
@@ -12230,7 +12656,7 @@ function summarizeValue(value) {
12230
12656
  };
12231
12657
  }
12232
12658
  function summarizeStructuredContent(value) {
12233
- const record = asRecord2(value);
12659
+ const record = asRecord3(value);
12234
12660
  if (!record || Array.isArray(value)) return summarizeValue(value);
12235
12661
  const keys = Object.keys(record);
12236
12662
  const entries = Object.entries(record).slice(0, KEY_PREVIEW_LIMIT);
@@ -12260,7 +12686,7 @@ function estimateValueBytes(value, depth = 0) {
12260
12686
  if (value === null || value === void 0) return 0;
12261
12687
  if (typeof value === "string") return byteLength(value);
12262
12688
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return byteLength(String(value));
12263
- const record = asRecord2(value);
12689
+ const record = asRecord3(value);
12264
12690
  if (!record || depth >= 2) return 0;
12265
12691
  const values = Array.isArray(value) ? value.slice(0, KEY_PREVIEW_LIMIT) : Object.values(record).slice(0, KEY_PREVIEW_LIMIT);
12266
12692
  return values.reduce((total, item) => total + estimateValueBytes(item, depth + 1), 0);
@@ -12304,7 +12730,7 @@ async function discardArtifact(path2) {
12304
12730
  } catch {
12305
12731
  }
12306
12732
  }
12307
- function asRecord2(value) {
12733
+ function asRecord3(value) {
12308
12734
  return typeof value === "object" && value !== null ? value : void 0;
12309
12735
  }
12310
12736
  function safeStringify(value) {
@@ -12357,6 +12783,8 @@ init_utils();
12357
12783
  // packages/coding-agent/src/humain/mcp-adapter/vendor/host-html-template.ts
12358
12784
  var DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
12359
12785
  var APP_SANDBOX = "allow-scripts allow-forms allow-modals allow-popups allow-downloads";
12786
+ var APP_PROXY_SANDBOX = `${APP_SANDBOX} allow-same-origin`;
12787
+ var APP_INNER_SANDBOX = APP_PROXY_SANDBOX;
12360
12788
  function buildHostHtmlTemplate(input) {
12361
12789
  const hostContext = input.hostContext ?? {};
12362
12790
  const sessionToken = safeInlineJSON(input.sessionToken);
@@ -12369,6 +12797,9 @@ function buildHostHtmlTemplate(input) {
12369
12797
  const requireToolConsent = safeInlineJSON(input.requireToolConsent);
12370
12798
  const cacheToolConsent = safeInlineJSON(input.cacheToolConsent);
12371
12799
  const moduleUrl = safeInlineJSON(input.appBridgeModuleUrl ?? DEFAULT_APP_BRIDGE_MODULE_URL);
12800
+ const sandboxProxyUrl = safeInlineJSON(input.sandboxProxyUrl);
12801
+ const resourceCsp = safeInlineJSON(input.resource.meta.csp);
12802
+ const resourcePermissions = safeInlineJSON(input.resource.meta.permissions);
12372
12803
  return `<!doctype html>
12373
12804
  <html lang="en">
12374
12805
  <head>
@@ -12449,7 +12880,7 @@ function buildHostHtmlTemplate(input) {
12449
12880
  </div>
12450
12881
  </header>
12451
12882
  <main>
12452
- <iframe id="mcp-app" sandbox="${APP_SANDBOX}" referrerpolicy="no-referrer"></iframe>
12883
+ <iframe id="mcp-app" sandbox="${APP_PROXY_SANDBOX}" referrerpolicy="no-referrer"></iframe>
12453
12884
  </main>
12454
12885
  <div class="overlay" id="error-overlay">
12455
12886
  <div class="panel">
@@ -12475,6 +12906,10 @@ function buildHostHtmlTemplate(input) {
12475
12906
  const ALLOW_ATTRIBUTE = ${allowAttribute};
12476
12907
  const REQUIRE_TOOL_CONSENT = ${requireToolConsent};
12477
12908
  const CACHE_TOOL_CONSENT = ${cacheToolConsent};
12909
+ const SANDBOX_PROXY_URL = ${sandboxProxyUrl};
12910
+ const RESOURCE_CSP = ${resourceCsp};
12911
+ const RESOURCE_PERMISSIONS = ${resourcePermissions};
12912
+ const INNER_SANDBOX = ${safeInlineJSON(APP_INNER_SANDBOX)};
12478
12913
  const STREAM_CONTEXT_KEY = "pi-mcp-adapter/stream";
12479
12914
  const STREAM_PATCH_METHOD = "notifications/pi-mcp-adapter/ui-result-patch";
12480
12915
 
@@ -12485,6 +12920,7 @@ function buildHostHtmlTemplate(input) {
12485
12920
  const errorOverlay = document.getElementById("error-overlay");
12486
12921
  const completionOverlay = document.getElementById("completion-overlay");
12487
12922
  const errorMessage = document.getElementById("error-message");
12923
+ const sandboxProxyOrigin = new URL(SANDBOX_PROXY_URL).origin;
12488
12924
 
12489
12925
  document.getElementById("server-name").textContent = SERVER_NAME;
12490
12926
  document.getElementById("tool-name").textContent = TOOL_NAME;
@@ -12542,10 +12978,41 @@ function buildHostHtmlTemplate(input) {
12542
12978
  const bridge = new AppBridge(
12543
12979
  null,
12544
12980
  { name: "pi", version: "1.0.0" },
12545
- { serverTools: {}, openLinks: {}, logging: {}, updateModelContext: {}, message: {} },
12981
+ {
12982
+ serverTools: {},
12983
+ openLinks: {},
12984
+ logging: {},
12985
+ updateModelContext: {},
12986
+ message: {},
12987
+ sandbox: {
12988
+ ...(RESOURCE_CSP ? { csp: RESOURCE_CSP } : {}),
12989
+ ...(RESOURCE_PERMISSIONS ? { permissions: RESOURCE_PERMISSIONS } : {}),
12990
+ },
12991
+ },
12546
12992
  { hostContext: HOST_CONTEXT }
12547
12993
  );
12548
12994
 
12995
+ let sandboxResourceSent = false;
12996
+ bridge.onsandboxready = async () => {
12997
+ if (sandboxResourceSent) return;
12998
+ sandboxResourceSent = true;
12999
+ try {
13000
+ const response = await fetch("/ui-app?resource=" + encodeURIComponent(UI_RESOURCE_TOKEN), {
13001
+ headers: { Accept: "text/html" },
13002
+ });
13003
+ if (!response.ok) throw new Error("UI resource request failed: HTTP " + response.status);
13004
+ const html = await response.text();
13005
+ await bridge.sendSandboxResourceReady({
13006
+ html,
13007
+ sandbox: INNER_SANDBOX,
13008
+ ...(RESOURCE_CSP ? { csp: RESOURCE_CSP } : {}),
13009
+ ...(RESOURCE_PERMISSIONS ? { permissions: RESOURCE_PERMISSIONS } : {}),
13010
+ });
13011
+ } catch (error) {
13012
+ showError("Failed to load MCP App resource: " + String(error));
13013
+ }
13014
+ };
13015
+
12549
13016
  bridge.oncalltool = async (params) => {
12550
13017
  if (!consentGranted) {
12551
13018
  const accepted = window.confirm("Allow this UI to call server tools for this session?");
@@ -12577,7 +13044,7 @@ function buildHostHtmlTemplate(input) {
12577
13044
  // Also listen for raw postMessage events with custom types (notify, prompt, intent, etc.)
12578
13045
  // These bypass the AppBridge protocol but are used by some MCP UI implementations
12579
13046
  window.addEventListener("message", async (event) => {
12580
- if (event.source !== iframe.contentWindow) return;
13047
+ if (event.source !== iframe.contentWindow || event.origin !== sandboxProxyOrigin) return;
12581
13048
  const data = event.data;
12582
13049
  if (!data || typeof data !== "object") return;
12583
13050
 
@@ -12641,6 +13108,12 @@ function buildHostHtmlTemplate(input) {
12641
13108
  }
12642
13109
 
12643
13110
  // Connect bridge BEFORE loading iframe to ensure we're listening when the app sends ui/initialize
13111
+ const sandboxMessageGuard = (event) => {
13112
+ if (event.source === iframe.contentWindow && event.origin !== sandboxProxyOrigin) {
13113
+ event.stopImmediatePropagation();
13114
+ }
13115
+ };
13116
+ window.addEventListener("message", sandboxMessageGuard, true);
12644
13117
  try {
12645
13118
  const transport = new PostMessageTransport(iframe.contentWindow, iframe.contentWindow);
12646
13119
  await bridge.connect(transport);
@@ -12652,7 +13125,7 @@ function buildHostHtmlTemplate(input) {
12652
13125
  const iframeLoaded = new Promise((resolve) => {
12653
13126
  iframe.onload = resolve;
12654
13127
  });
12655
- iframe.src = "/ui-app?resource=" + encodeURIComponent(UI_RESOURCE_TOKEN);
13128
+ iframe.src = SANDBOX_PROXY_URL;
12656
13129
  await iframeLoaded;
12657
13130
 
12658
13131
  const eventSource = new EventSource("/events?session=" + encodeURIComponent(SESSION_TOKEN));
@@ -12677,6 +13150,9 @@ function buildHostHtmlTemplate(input) {
12677
13150
  showError("Failed to forward cancellation: " + String(error));
12678
13151
  }
12679
13152
  });
13153
+ eventSource.addEventListener("resource-updated", () => {
13154
+ setStatus("Resource updated on the server. Reopen this UI to load the latest version.");
13155
+ });
12680
13156
  eventSource.addEventListener("result-patch", async (event) => {
12681
13157
  try {
12682
13158
  await bridge.notification({
@@ -12764,12 +13240,205 @@ function sanitizeCspDomains(domains) {
12764
13240
  ))];
12765
13241
  }
12766
13242
  function safeInlineJSON(value) {
12767
- return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
13243
+ const json = JSON.stringify(value);
13244
+ if (json === void 0) return "undefined";
13245
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
12768
13246
  }
12769
13247
  function escapeHtml2(value) {
12770
13248
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
12771
13249
  }
12772
13250
 
13251
+ // packages/coding-agent/src/humain/mcp-adapter/vendor/sandbox-proxy-template.ts
13252
+ var SANDBOX_PROXY_SANDBOX = "allow-scripts allow-forms allow-modals allow-popups allow-downloads allow-same-origin";
13253
+ var SANDBOX_INNER_SANDBOX = SANDBOX_PROXY_SANDBOX;
13254
+ var SANDBOX_PROXY_PATH = "/sandbox";
13255
+ function buildSandboxProxyHtml(input) {
13256
+ const parentOrigin = safeInlineJSON2(input.parentOrigin);
13257
+ return `<!doctype html>
13258
+ <html lang="en">
13259
+ <head>
13260
+ <meta charset="utf-8" />
13261
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13262
+ <title>MCP App Sandbox</title>
13263
+ <style>
13264
+ html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: transparent; }
13265
+ iframe { display: block; width: 100%; height: 100%; border: 0; }
13266
+ </style>
13267
+ </head>
13268
+ <body>
13269
+ <iframe id="mcp-app" title="MCP App" sandbox="${SANDBOX_INNER_SANDBOX}" referrerpolicy="no-referrer"></iframe>
13270
+ <script>
13271
+ const EXPECTED_PARENT_ORIGIN = ${parentOrigin};
13272
+ const SANDBOX_PROXY_READY_METHOD = "ui/notifications/sandbox-proxy-ready";
13273
+ const SANDBOX_RESOURCE_READY_METHOD = "ui/notifications/sandbox-resource-ready";
13274
+ const INNER_SANDBOX = ${safeInlineJSON2(SANDBOX_INNER_SANDBOX)};
13275
+ const MAX_PENDING_MESSAGES = 64;
13276
+ const innerFrame = document.getElementById("mcp-app");
13277
+ const pendingToInner = [];
13278
+ let innerReady = false;
13279
+
13280
+ const isObject = (value) => value !== null && typeof value === "object";
13281
+ const isMessageFromParent = (event) =>
13282
+ event.source === window.parent && event.origin === EXPECTED_PARENT_ORIGIN;
13283
+ const isMessageFromInner = (event) =>
13284
+ event.source === innerFrame.contentWindow && event.origin === window.location.origin;
13285
+
13286
+ const postToParent = (data) => {
13287
+ window.parent.postMessage(data, EXPECTED_PARENT_ORIGIN);
13288
+ };
13289
+
13290
+ const postToInner = (data) => {
13291
+ if (!innerReady || !innerFrame.contentWindow) {
13292
+ if (pendingToInner.length < MAX_PENDING_MESSAGES) pendingToInner.push(data);
13293
+ return;
13294
+ }
13295
+ innerFrame.contentWindow.postMessage(data, window.location.origin);
13296
+ };
13297
+
13298
+ const flushPendingMessages = () => {
13299
+ if (!innerFrame.contentWindow) return;
13300
+ innerReady = true;
13301
+ for (const data of pendingToInner.splice(0)) {
13302
+ innerFrame.contentWindow.postMessage(data, window.location.origin);
13303
+ }
13304
+ };
13305
+
13306
+ const sanitizeDomains = (domains) => {
13307
+ if (!Array.isArray(domains)) return [];
13308
+ return [...new Set(domains.filter((domain) =>
13309
+ typeof domain === "string" &&
13310
+ domain.length > 0 &&
13311
+ /^[\\x21-\\x7E]+$/.test(domain) &&
13312
+ !/[;'"]/.test(domain),
13313
+ ))];
13314
+ };
13315
+
13316
+ const toDirective = (name, trustedSources, domains) =>
13317
+ name + " " + [...new Set([...trustedSources, ...domains])].join(" ");
13318
+
13319
+ const buildResourceCsp = (csp) => {
13320
+ const resourceDomains = sanitizeDomains(csp?.resourceDomains);
13321
+ const connectDomains = sanitizeDomains(csp?.connectDomains);
13322
+ const frameDomains = sanitizeDomains(csp?.frameDomains);
13323
+ const baseUriDomains = sanitizeDomains(csp?.baseUriDomains);
13324
+ return [
13325
+ "default-src 'none'",
13326
+ "sandbox " + INNER_SANDBOX,
13327
+ toDirective("script-src", ["'self'", "'unsafe-inline'"], resourceDomains),
13328
+ toDirective("style-src", ["'self'", "'unsafe-inline'"], resourceDomains),
13329
+ toDirective("font-src", ["'self'"], resourceDomains),
13330
+ toDirective("img-src", ["'self'", "data:"], resourceDomains),
13331
+ toDirective("media-src", ["'self'", "data:"], resourceDomains),
13332
+ connectDomains.length > 0 ? "connect-src " + connectDomains.join(" ") : "connect-src 'none'",
13333
+ frameDomains.length > 0 ? "frame-src " + frameDomains.join(" ") : "frame-src 'none'",
13334
+ "worker-src 'none'",
13335
+ "object-src 'none'",
13336
+ baseUriDomains.length > 0 ? "base-uri " + baseUriDomains.join(" ") : "base-uri 'self'",
13337
+ ].join("; ");
13338
+ };
13339
+
13340
+ const escapeAttribute = (value) => value
13341
+ .replace(/&/g, "&amp;")
13342
+ .replace(/"/g, "&quot;")
13343
+ .replace(/</g, "&lt;")
13344
+ .replace(/>/g, "&gt;");
13345
+
13346
+ const injectCsp = (html, csp) => {
13347
+ const meta = '<meta http-equiv="Content-Security-Policy" content="' + escapeAttribute(csp) + '">';
13348
+ const head = html.match(/<head(?:\\s[^>]*)?>/i);
13349
+ if (head && head.index !== undefined) {
13350
+ const end = head.index + head[0].length;
13351
+ return html.slice(0, end) + meta + html.slice(end);
13352
+ }
13353
+ return meta + html;
13354
+ };
13355
+
13356
+ const safeSandbox = (requested) => {
13357
+ const requestedTokens = typeof requested === "string" ? requested.split(/\\s+/) : [];
13358
+ const allowedTokens = new Set(INNER_SANDBOX.split(" "));
13359
+ const tokens = requestedTokens.filter((token) => allowedTokens.has(token));
13360
+ // Provider HTML needs both script execution and a real proxy-origin
13361
+ // storage context. Never accept popup escape or top-navigation tokens.
13362
+ return [...new Set([
13363
+ "allow-scripts",
13364
+ "allow-same-origin",
13365
+ ...tokens,
13366
+ ])].filter((token) => allowedTokens.has(token)).join(" ");
13367
+ };
13368
+
13369
+ const buildAllowAttribute = (permissions) => {
13370
+ if (!isObject(permissions) || Array.isArray(permissions)) return "";
13371
+ const allowed = [];
13372
+ if (permissions.camera) allowed.push("camera");
13373
+ if (permissions.microphone) allowed.push("microphone");
13374
+ if (permissions.geolocation) allowed.push("geolocation");
13375
+ if (permissions.clipboardWrite) allowed.push("clipboard-write");
13376
+ return allowed.join("; ");
13377
+ };
13378
+
13379
+ const loadResource = (params) => {
13380
+ if (!isObject(params) || typeof params.html !== "string" || !innerFrame.contentDocument) return;
13381
+ innerFrame.setAttribute("sandbox", safeSandbox(params.sandbox));
13382
+ const allow = buildAllowAttribute(params.permissions);
13383
+ if (allow) innerFrame.setAttribute("allow", allow);
13384
+ else innerFrame.removeAttribute("allow");
13385
+ innerReady = false;
13386
+ const document = innerFrame.contentDocument;
13387
+ document.open();
13388
+ document.write(injectCsp(params.html, buildResourceCsp(params.csp)));
13389
+ document.close();
13390
+ flushPendingMessages();
13391
+ };
13392
+
13393
+ window.addEventListener("message", (event) => {
13394
+ if (isMessageFromParent(event)) {
13395
+ const data = event.data;
13396
+ if (!isObject(data)) return;
13397
+ if (data.method === SANDBOX_RESOURCE_READY_METHOD) {
13398
+ loadResource(data.params);
13399
+ return;
13400
+ }
13401
+ if (data.method === SANDBOX_PROXY_READY_METHOD) return;
13402
+ if (typeof data.method === "string" && data.method.startsWith("ui/notifications/sandbox-")) return;
13403
+ postToInner(data);
13404
+ return;
13405
+ }
13406
+
13407
+ if (!isMessageFromInner(event)) return;
13408
+ const data = event.data;
13409
+ if (!isObject(data)) return;
13410
+ if (typeof data.method === "string" && data.method.startsWith("ui/notifications/sandbox-")) return;
13411
+ postToParent(data);
13412
+ });
13413
+
13414
+ postToParent({
13415
+ jsonrpc: "2.0",
13416
+ method: SANDBOX_PROXY_READY_METHOD,
13417
+ params: {},
13418
+ });
13419
+ </script>
13420
+ </body>
13421
+ </html>`;
13422
+ }
13423
+ function buildSandboxProxyCsp() {
13424
+ return [
13425
+ "default-src 'none'",
13426
+ "script-src 'unsafe-inline'",
13427
+ "style-src 'unsafe-inline'",
13428
+ "frame-src 'self'",
13429
+ "connect-src 'none'",
13430
+ "worker-src 'none'",
13431
+ "object-src 'none'",
13432
+ "base-uri 'none'",
13433
+ `sandbox ${SANDBOX_PROXY_SANDBOX}`
13434
+ ].join("; ");
13435
+ }
13436
+ function safeInlineJSON2(value) {
13437
+ const json = JSON.stringify(value);
13438
+ if (json === void 0) return "undefined";
13439
+ return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
13440
+ }
13441
+
12773
13442
  // packages/coding-agent/src/humain/mcp-adapter/vendor/tool-approval.ts
12774
13443
  import { createHash as createHash4, randomUUID as randomUUID2 } from "node:crypto";
12775
13444
  init_types();
@@ -12939,6 +13608,11 @@ async function startUiServer(options) {
12939
13608
  const eventLog = [];
12940
13609
  let latestCheckpointEventId;
12941
13610
  let streamSummary;
13611
+ let server2 = null;
13612
+ let sandboxProxyServer = null;
13613
+ let sandboxProxyUrl = null;
13614
+ let closeTimer = null;
13615
+ let listenersClosed = false;
12942
13616
  const sessionMessages = {
12943
13617
  prompts: [],
12944
13618
  notifications: [],
@@ -13086,6 +13760,32 @@ data: ${JSON.stringify(payload)}
13086
13760
  clearInterval(watchdog);
13087
13761
  watchdog = null;
13088
13762
  };
13763
+ const closeListeners = () => {
13764
+ if (listenersClosed) return;
13765
+ listenersClosed = true;
13766
+ stopWatchdog();
13767
+ if (closeTimer) {
13768
+ clearTimeout(closeTimer);
13769
+ closeTimer = null;
13770
+ }
13771
+ try {
13772
+ server2?.close();
13773
+ } catch {
13774
+ }
13775
+ try {
13776
+ sandboxProxyServer?.close();
13777
+ } catch {
13778
+ }
13779
+ closeSse();
13780
+ };
13781
+ const scheduleListenerClose = () => {
13782
+ if (closeTimer || listenersClosed) return;
13783
+ closeTimer = setTimeout(() => {
13784
+ closeTimer = null;
13785
+ closeListeners();
13786
+ }, 20);
13787
+ closeTimer.unref();
13788
+ };
13089
13789
  const markCompleted = (reason) => {
13090
13790
  if (completed) return;
13091
13791
  log.debug("Session completed", { reason });
@@ -13093,8 +13793,9 @@ data: ${JSON.stringify(payload)}
13093
13793
  completed = true;
13094
13794
  stopWatchdog();
13095
13795
  options.onComplete?.(reason);
13796
+ scheduleListenerClose();
13096
13797
  };
13097
- const server2 = http.createServer(async (req, res) => {
13798
+ const hostServer = http.createServer(async (req, res) => {
13098
13799
  try {
13099
13800
  const method = req.method || "GET";
13100
13801
  const hostHeader = req.headers.host;
@@ -13118,6 +13819,10 @@ data: ${JSON.stringify(payload)}
13118
13819
  }
13119
13820
  if (!validateTokenQuery(url, sessionToken, res)) return;
13120
13821
  touchHeartbeat();
13822
+ if (!sandboxProxyUrl) {
13823
+ sendText(res, 503, "Sandbox proxy is not ready");
13824
+ return;
13825
+ }
13121
13826
  const html = buildHostHtmlTemplate({
13122
13827
  sessionToken,
13123
13828
  uiResourceToken,
@@ -13128,7 +13833,8 @@ data: ${JSON.stringify(payload)}
13128
13833
  allowAttribute: buildAllowAttribute(options.resource.meta.permissions),
13129
13834
  requireToolConsent: options.consentManager.requiresPrompt(options.serverName),
13130
13835
  cacheToolConsent: options.consentManager.shouldCacheConsent(),
13131
- hostContext
13836
+ hostContext,
13837
+ sandboxProxyUrl
13132
13838
  });
13133
13839
  res.writeHead(200, {
13134
13840
  "Content-Type": "text/html; charset=utf-8",
@@ -13292,8 +13998,14 @@ data: ${JSON.stringify(payload)}
13292
13998
  ...options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}
13293
13999
  },
13294
14000
  options.serverName,
13295
- (conn) => conn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName))
13296
- ) : await connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
14001
+ async (conn) => {
14002
+ await options.manager.ensureListen?.(options.serverName, conn);
14003
+ return conn.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
14004
+ }
14005
+ ) : await (async () => {
14006
+ await options.manager.ensureListen?.(options.serverName, connection);
14007
+ return connection.client.callTool(callArgs, options.manager.getRequestOptions?.(options.serverName));
14008
+ })();
13297
14009
  sendJson(res, 200, { ok: true, result });
13298
14010
  } finally {
13299
14011
  options.manager.decrementInFlight(options.serverName);
@@ -13393,13 +14105,6 @@ data: ${JSON.stringify(payload)}
13393
14105
  const reason = typeof params.reason === "string" ? params.reason : "done";
13394
14106
  markCompleted(reason);
13395
14107
  sendJson(res, 200, { ok: true, result: {} });
13396
- setTimeout(() => {
13397
- try {
13398
- server2.close();
13399
- } catch {
13400
- }
13401
- closeSse();
13402
- }, 20).unref();
13403
14108
  return;
13404
14109
  }
13405
14110
  sendJson(res, 404, { ok: false, error: "Not found" });
@@ -13418,6 +14123,7 @@ data: ${JSON.stringify(payload)}
13418
14123
  sendJson(res, status, { ok: false, error: wrapped.message });
13419
14124
  }
13420
14125
  });
14126
+ server2 = hostServer;
13421
14127
  if (options.initialResultPromise) {
13422
14128
  options.initialResultPromise.then(
13423
14129
  (result) => pushEvent("tool-result", result),
@@ -13431,22 +14137,78 @@ data: ${JSON.stringify(payload)}
13431
14137
  if (completed) return;
13432
14138
  if (Date.now() - lastHeartbeatAt <= ABANDONED_GRACE_MS) return;
13433
14139
  markCompleted("stale");
13434
- try {
13435
- server2.close();
13436
- } catch {
13437
- }
13438
- closeSse();
13439
14140
  }, WATCHDOG_INTERVAL_MS);
13440
14141
  watchdog.unref();
13441
14142
  return new Promise((resolve7, reject) => {
13442
14143
  const candidates = resolvePortCandidates(options.port);
13443
14144
  let candidateIndex = 0;
14145
+ const startSandboxProxy = (parentOrigin) => {
14146
+ const proxy = http.createServer((req, res) => {
14147
+ try {
14148
+ const method = req.method || "GET";
14149
+ const hostHeader = req.headers.host;
14150
+ const url = new URL(req.url || "/", `http://${hostHeader || "127.0.0.1"}`);
14151
+ if (hostHeader !== void 0 && !isAllowedHost(url.hostname)) {
14152
+ sendText(res, 403, "Invalid host");
14153
+ return;
14154
+ }
14155
+ if (method === "HEAD" && url.pathname === SANDBOX_PROXY_PATH) {
14156
+ res.writeHead(200, {
14157
+ "Content-Type": "text/html; charset=utf-8",
14158
+ "Cache-Control": "no-store",
14159
+ "Content-Security-Policy": buildSandboxProxyCsp()
14160
+ });
14161
+ res.end();
14162
+ return;
14163
+ }
14164
+ if (method === "GET" && url.pathname === SANDBOX_PROXY_PATH) {
14165
+ res.writeHead(200, {
14166
+ "Content-Type": "text/html; charset=utf-8",
14167
+ "Cache-Control": "no-store",
14168
+ "Content-Security-Policy": buildSandboxProxyCsp(),
14169
+ "Referrer-Policy": "no-referrer",
14170
+ "X-Content-Type-Options": "nosniff"
14171
+ });
14172
+ res.end(buildSandboxProxyHtml({ parentOrigin }));
14173
+ return;
14174
+ }
14175
+ sendJson(res, 404, { ok: false, error: "Not found" });
14176
+ } catch (error) {
14177
+ sendJson(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) });
14178
+ }
14179
+ });
14180
+ sandboxProxyServer = proxy;
14181
+ return new Promise((resolveProxy, rejectProxy) => {
14182
+ const onError2 = (error) => {
14183
+ proxy.off("listening", onListening2);
14184
+ rejectProxy(error);
14185
+ };
14186
+ const onListening2 = () => {
14187
+ proxy.off("error", onError2);
14188
+ const address = proxy.address();
14189
+ if (!address || typeof address === "string") {
14190
+ rejectProxy(new ServerError("invalid sandbox proxy address"));
14191
+ return;
14192
+ }
14193
+ resolveProxy(address.port);
14194
+ };
14195
+ proxy.once("error", onError2);
14196
+ proxy.listen(0, "127.0.0.1", onListening2);
14197
+ });
14198
+ };
13444
14199
  const listen = () => {
13445
- server2.once("error", onError);
13446
- server2.listen(candidates[candidateIndex], "127.0.0.1", onListening);
14200
+ const candidate = candidates[candidateIndex];
14201
+ if (candidate === void 0) {
14202
+ const error = new ServerError("no UI server port candidates available");
14203
+ closeListeners();
14204
+ reject(error);
14205
+ return;
14206
+ }
14207
+ hostServer.once("error", onError);
14208
+ hostServer.listen(candidate, "127.0.0.1", onListening);
13447
14209
  };
13448
14210
  const onError = (error) => {
13449
- server2.off("listening", onListening);
14211
+ hostServer.off("listening", onListening);
13450
14212
  if (error.code === "EADDRINUSE" && candidateIndex < candidates.length - 1) {
13451
14213
  candidateIndex += 1;
13452
14214
  listen();
@@ -13454,56 +14216,73 @@ data: ${JSON.stringify(payload)}
13454
14216
  }
13455
14217
  log.error("Failed to start server", error);
13456
14218
  const port = candidates[candidateIndex];
14219
+ closeListeners();
13457
14220
  reject(new ServerError(error.message, {
13458
14221
  ...port !== void 0 ? { port } : {},
13459
14222
  cause: error
13460
14223
  }));
13461
14224
  };
13462
14225
  const onListening = () => {
13463
- server2.off("error", onError);
13464
- const address = server2.address();
14226
+ hostServer.off("error", onError);
14227
+ const address = hostServer.address();
13465
14228
  if (!address || typeof address === "string") {
13466
14229
  const err = new ServerError("invalid address");
13467
14230
  log.error("Invalid server address", err);
14231
+ closeListeners();
13468
14232
  reject(err);
13469
14233
  return;
13470
14234
  }
13471
- log.debug("Server started", { port: address.port });
13472
- rememberMoshiDiscoveryPort(address.port);
13473
- const handle = {
13474
- url: `http://localhost:${address.port}/?session=${sessionToken}`,
13475
- port: address.port,
13476
- sessionToken,
13477
- serverName: options.serverName,
13478
- toolName: options.toolName,
13479
- close: (reason) => {
13480
- markCompleted(reason ?? "closed");
13481
- try {
13482
- server2.close();
13483
- } catch {
13484
- }
13485
- closeSse();
13486
- },
13487
- sendToolInput: (args) => {
13488
- pushEvent("tool-input", { arguments: args });
13489
- },
13490
- sendToolResult: (result) => {
13491
- pushEvent("tool-result", result);
13492
- },
13493
- sendResultPatch: (result) => {
13494
- pushEvent("result-patch", result);
13495
- },
13496
- sendToolCancelled: (reason) => {
13497
- pushEvent("tool-cancelled", { reason });
13498
- },
13499
- sendHostContext: (context) => {
13500
- Object.assign(hostContext, context);
13501
- pushEvent("host-context", context);
13502
- },
13503
- getSessionMessages: () => ({ ...sessionMessages }),
13504
- getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : void 0
13505
- };
13506
- resolve7(handle);
14235
+ const parentOrigin = `http://localhost:${address.port}`;
14236
+ void startSandboxProxy(parentOrigin).then((proxyPort) => {
14237
+ if (completed || listenersClosed) {
14238
+ closeListeners();
14239
+ reject(new ServerError("UI session completed before sandbox proxy was ready"));
14240
+ return;
14241
+ }
14242
+ sandboxProxyUrl = `http://localhost:${proxyPort}${SANDBOX_PROXY_PATH}`;
14243
+ log.debug("Servers started", { port: address.port, proxyPort });
14244
+ rememberMoshiDiscoveryPort(address.port);
14245
+ const handle = {
14246
+ url: `http://localhost:${address.port}/?session=${sessionToken}`,
14247
+ port: address.port,
14248
+ proxyUrl: sandboxProxyUrl,
14249
+ proxyPort,
14250
+ sessionToken,
14251
+ serverName: options.serverName,
14252
+ toolName: options.toolName,
14253
+ close: (reason) => {
14254
+ markCompleted(reason ?? "closed");
14255
+ closeListeners();
14256
+ },
14257
+ sendToolInput: (args) => {
14258
+ pushEvent("tool-input", { arguments: args });
14259
+ },
14260
+ sendToolResult: (result) => {
14261
+ pushEvent("tool-result", result);
14262
+ },
14263
+ sendResultPatch: (result) => {
14264
+ pushEvent("result-patch", result);
14265
+ },
14266
+ sendToolCancelled: (reason) => {
14267
+ pushEvent("tool-cancelled", { reason });
14268
+ },
14269
+ sendResourceUpdated: (uri) => {
14270
+ pushEvent("resource-updated", { uri });
14271
+ },
14272
+ sendHostContext: (context) => {
14273
+ Object.assign(hostContext, context);
14274
+ pushEvent("host-context", context);
14275
+ },
14276
+ getSessionMessages: () => ({ ...sessionMessages }),
14277
+ getStreamSummary: () => streamSummary ? { ...streamSummary, phases: [...streamSummary.phases] } : void 0
14278
+ };
14279
+ resolve7(handle);
14280
+ }).catch((error) => {
14281
+ log.error("Failed to start sandbox proxy", error instanceof Error ? error : void 0);
14282
+ closeListeners();
14283
+ const wrapped = error instanceof ServerError ? error : new ServerError(error instanceof Error ? error.message : String(error), { cause: error });
14284
+ reject(wrapped);
14285
+ });
13507
14286
  };
13508
14287
  listen();
13509
14288
  });
@@ -13749,10 +14528,12 @@ function remoteAccessHint(opts) {
13749
14528
  lines.push(`Browser launch failed: ${opts.openError}`);
13750
14529
  }
13751
14530
  if (opts.moshi) {
13752
- lines.push("Moshi: tap the preview button in the terminal title bar and pick this MCP UI server.");
14531
+ lines.push(
14532
+ `Moshi: tap the preview button in the terminal title bar and pick this MCP UI server (it must reach ports ${opts.port} and ${opts.proxyPort}).`
14533
+ );
13753
14534
  }
13754
14535
  lines.push(
13755
- `SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} <this-host>\` on your local machine, then open the URL above.`,
14536
+ `SSH: run \`ssh -L ${opts.port}:127.0.0.1:${opts.port} -L ${opts.proxyPort}:127.0.0.1:${opts.proxyPort} <this-host>\` on your local machine, then open the URL above.`,
13756
14537
  "mosh can't forward ports - run that ssh command in a separate terminal."
13757
14538
  );
13758
14539
  return lines.join("\n");
@@ -13773,7 +14554,7 @@ async function maybeStartUiSession(state, request) {
13773
14554
  const streamToken2 = streamMode2 ? randomUUID4() : void 0;
13774
14555
  let active2 = true;
13775
14556
  let nextStreamSequence2 = 0;
13776
- const cleanupStreamListener2 = () => {
14557
+ const cleanupStreamListener = () => {
13777
14558
  if (streamToken2) {
13778
14559
  state.manager.removeUiStreamListener(streamToken2);
13779
14560
  }
@@ -13827,7 +14608,7 @@ async function maybeStartUiSession(state, request) {
13827
14608
  },
13828
14609
  close: () => {
13829
14610
  active2 = false;
13830
- cleanupStreamListener2();
14611
+ cleanupStreamListener();
13831
14612
  }
13832
14613
  };
13833
14614
  }
@@ -13859,10 +14640,12 @@ async function maybeStartUiSession(state, request) {
13859
14640
  let active = true;
13860
14641
  let nextStreamSequence = 0;
13861
14642
  let handle;
13862
- const cleanupStreamListener = () => {
14643
+ const resourceListenerToken = randomUUID4();
14644
+ const cleanupListeners = () => {
13863
14645
  if (streamToken) {
13864
14646
  state.manager.removeUiStreamListener(streamToken);
13865
14647
  }
14648
+ state.manager.removeResourceUpdatedListener?.(resourceListenerToken);
13866
14649
  };
13867
14650
  handle = await startUiServer({
13868
14651
  serverName: request.serverName,
@@ -13935,7 +14718,7 @@ ${update.summary}` }],
13935
14718
  },
13936
14719
  onComplete: (reason) => {
13937
14720
  active = false;
13938
- cleanupStreamListener();
14721
+ cleanupListeners();
13939
14722
  if (state.uiServer === handle) {
13940
14723
  const messages = handle.getSessionMessages();
13941
14724
  const stream = handle.getStreamSummary();
@@ -13982,6 +14765,15 @@ ${update.summary}` }],
13982
14765
  handle.sendResultPatch(withStreamEnvelope(notification.result, streamId, nextStreamSequence));
13983
14766
  });
13984
14767
  }
14768
+ state.manager.registerResourceUpdatedListener?.(
14769
+ resourceListenerToken,
14770
+ request.serverName,
14771
+ request.uiResourceUri,
14772
+ (_serverName, uri) => {
14773
+ if (!active || state.uiServer !== handle) return;
14774
+ handle.sendResourceUpdated(uri);
14775
+ }
14776
+ );
13985
14777
  state.uiServer = handle;
13986
14778
  const viewerPref = process.env.MCP_UI_VIEWER?.toLowerCase();
13987
14779
  const uiSuppressed = viewerPref === "none" || viewerPref === "off" || viewerPref === "disabled";
@@ -13991,7 +14783,11 @@ ${update.summary}` }],
13991
14783
  if (uiSuppressed) {
13992
14784
  viewer = "suppressed";
13993
14785
  windowOpen = false;
13994
- state.ui?.notify(`MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}`, "info");
14786
+ state.ui?.notify(
14787
+ `MCP UI window suppressed (MCP_UI_VIEWER=${viewerPref}). Open manually: ${handle.url}
14788
+ If this session is remote, run ssh -L ${handle.port}:127.0.0.1:${handle.port} -L ${handle.proxyPort}:127.0.0.1:${handle.proxyPort} <this-host> first.`,
14789
+ "info"
14790
+ );
13995
14791
  log.info("Suppressing MCP UI window (MCP_UI_VIEWER=" + viewerPref + ")", { url: handle.url });
13996
14792
  } else {
13997
14793
  const remoteLikely = remoteByEnv || await hasActiveRemoteLogin();
@@ -13999,6 +14795,7 @@ ${update.summary}` }],
13999
14795
  state.ui?.notify(remoteAccessHint({
14000
14796
  url: handle.url,
14001
14797
  port: handle.port,
14798
+ proxyPort: handle.proxyPort,
14002
14799
  moshi: await probeMoshiGateway(),
14003
14800
  openError,
14004
14801
  openedOnHost
@@ -14075,12 +14872,12 @@ ${update.summary}` }],
14075
14872
  },
14076
14873
  close: (reason) => {
14077
14874
  active = false;
14078
- cleanupStreamListener();
14875
+ cleanupListeners();
14079
14876
  handle.close(reason);
14080
14877
  }
14081
14878
  };
14082
14879
  } catch (error) {
14083
- if (error instanceof UrlElicitationRequiredError2 || isAbortError(error, runtimeSignal)) throw error;
14880
+ if (error instanceof UrlElicitationRequiredError2 || error instanceof InputRequiredNeedsUiError || isAbortError(error, runtimeSignal)) throw error;
14084
14881
  const message = error instanceof Error ? error.message : String(error);
14085
14882
  log.error("Failed to start UI session", error instanceof Error ? error : void 0);
14086
14883
  state.ui?.notify(
@@ -14475,7 +15272,13 @@ function createDirectToolExecutor(getState, getInitPromise, spec) {
14475
15272
  onNeedsAuth: recoverAuthConnection
14476
15273
  },
14477
15274
  spec.serverName,
14478
- (conn) => conn.client.readResource({ uri: spec.resourceUri }, requestOptions)
15275
+ async (conn) => {
15276
+ const refreshRead = await state.manager.prepareResourceUse?.(spec.serverName, spec.resourceUri, conn);
15277
+ return conn.client.readResource(
15278
+ { uri: spec.resourceUri },
15279
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions
15280
+ );
15281
+ }
14479
15282
  );
14480
15283
  const content2 = transformMcpResourceContents(result2.contents ?? [], state.owner?.signal);
14481
15284
  const guarded2 = await guardMcpOutput(content2.length > 0 ? content2 : [{ type: "text", text: "(empty resource)" }], {
@@ -14505,11 +15308,14 @@ function createDirectToolExecutor(getState, getInitPromise, spec) {
14505
15308
  onNeedsAuth: recoverAuthConnection
14506
15309
  },
14507
15310
  spec.serverName,
14508
- (conn) => abortable(conn.client.callTool({
14509
- name: spec.originalName,
14510
- arguments: normalizedParams,
14511
- _meta: uiSession?.requestMeta
14512
- }, requestOptions), ownedSignal)
15311
+ async (conn) => {
15312
+ await state.manager.ensureListen?.(spec.serverName, conn);
15313
+ return abortable(conn.client.callTool({
15314
+ name: spec.originalName,
15315
+ arguments: normalizedParams,
15316
+ _meta: uiSession?.requestMeta
15317
+ }, requestOptions), ownedSignal);
15318
+ }
14513
15319
  );
14514
15320
  uiSession?.sendToolResult(result);
14515
15321
  if (result.isError) {
@@ -14581,6 +15387,14 @@ ${uiSummary.message}`,
14581
15387
  details: { error: "url_elicitation_required", server: spec.serverName, action }
14582
15388
  };
14583
15389
  }
15390
+ const inputRequired = getInputRequiredNeedsUiDetails(error, spec.resourceUri ? { server: spec.serverName, resourceUri: spec.resourceUri } : { server: spec.serverName, tool: spec.originalName });
15391
+ if (inputRequired) {
15392
+ uiSession?.sendToolCancelled(inputRequired.message);
15393
+ return {
15394
+ content: [{ type: "text", text: inputRequired.message }],
15395
+ details: { ...inputRequired }
15396
+ };
15397
+ }
14584
15398
  const message = error instanceof Error ? error.message : String(error);
14585
15399
  const aborted = isAbortError(error, ownedSignal);
14586
15400
  if (!aborted) {
@@ -15451,7 +16265,16 @@ function executeStatus(state) {
15451
16265
  status = "cached";
15452
16266
  }
15453
16267
  const toolCount = status === "failed" ? 0 : metadata?.length ?? 0;
15454
- servers.push({ name, status, toolCount, failedAgo, ...disabled ? { disabled: true } : {} });
16268
+ const listenState = connection?.status === "connected" ? connection.listenState : "disconnected";
16269
+ servers.push({
16270
+ name,
16271
+ status,
16272
+ listenState,
16273
+ ...connection?.status === "connected" && connection.listenCatalogStale ? { catalogStale: true } : {},
16274
+ toolCount,
16275
+ failedAgo,
16276
+ ...disabled ? { disabled: true } : {}
16277
+ });
15455
16278
  }
15456
16279
  const disabledCount = servers.filter((s) => s.disabled).length;
15457
16280
  const enabledServers = servers.filter((s) => !s.disabled);
@@ -15467,7 +16290,8 @@ function executeStatus(state) {
15467
16290
  continue;
15468
16291
  }
15469
16292
  if (server2.status === "connected") {
15470
- text += `\u2713 ${server2.name} (${server2.toolCount} tools)
16293
+ const listen = server2.listenState === "active" ? server2.catalogStale ? ", listen active, catalog may be stale" : ", listen active" : server2.listenState === "dropped" ? ", catalog may be stale; will reconcile on next keep-alive or tool use" : server2.listenState === "re-establishing" ? ", re-establishing listen" : server2.listenState === "legacy" ? ", legacy notification path" : ", not listening for catalog updates";
16294
+ text += `\u2713 ${server2.name} (${server2.toolCount} tools${listen})
15471
16295
  `;
15472
16296
  continue;
15473
16297
  }
@@ -15477,7 +16301,7 @@ function executeStatus(state) {
15477
16301
  continue;
15478
16302
  }
15479
16303
  if (server2.status === "cached") {
15480
- text += `\u25CB ${server2.name} (${server2.toolCount} tools, cached)
16304
+ text += `\u25CB ${server2.name} (${server2.toolCount} tools, cached; not listening)
15481
16305
  `;
15482
16306
  continue;
15483
16307
  }
@@ -15486,16 +16310,20 @@ function executeStatus(state) {
15486
16310
  `;
15487
16311
  continue;
15488
16312
  }
15489
- text += `\u25CB ${server2.name} (not connected)
16313
+ text += `\u25CB ${server2.name} (not listening; disconnected)
15490
16314
  `;
15491
16315
  }
16316
+ const directToolsFrozen = state.config.settings?.freezeDirectTools === true;
16317
+ if (directToolsFrozen) {
16318
+ text += "\nDirect tools frozen; active registrations may differ from current metadata.\n";
16319
+ }
15492
16320
  if (servers.length > 0) {
15493
16321
  text += `
15494
16322
  mcp({ server: "name" }) to list tools, mcp({ search: "..." }) to search`;
15495
16323
  }
15496
16324
  return {
15497
16325
  content: [{ type: "text", text: text.trim() }],
15498
- details: { mode: "status", servers, totalTools, connectedCount, disabledCount }
16326
+ details: { mode: "status", servers, totalTools, connectedCount, disabledCount, directToolsFrozen }
15499
16327
  };
15500
16328
  }
15501
16329
  async function executeAuthStart(state, serverName, signal) {
@@ -15810,7 +16638,14 @@ Use mcp({ instructions: "${server2}" }) for the full text.`;
15810
16638
  details: { mode: "list", server: server2, tools: [], count: 0, error: "not_connected", hasInstructions: Boolean(instructions) }
15811
16639
  };
15812
16640
  }
15813
- const cachedNote = connection?.status === "connected" ? "" : " (not connected, cached)";
16641
+ let cachedNote = "";
16642
+ if (connection?.status !== "connected") {
16643
+ if (connection?.status === "needs-auth") {
16644
+ cachedNote = ` (needs auth \u2014 run mcp({ action: "auth-start", server: "${server2}" }))`;
16645
+ } else {
16646
+ cachedNote = ` (lazy: tools from cache, not connected yet \u2014 mcp({ connect: "${server2}" }) to connect)`;
16647
+ }
16648
+ }
15814
16649
  let text = `${server2} (${toolNames.length} tools${cachedNote}):
15815
16650
 
15816
16651
  `;
@@ -16301,7 +17136,13 @@ async function executeCall(state, toolName, args, serverOverride, getPiTools, si
16301
17136
  onNeedsAuth: recoverAuthConnection
16302
17137
  },
16303
17138
  serverName,
16304
- (conn) => conn.client.readResource({ uri: toolMeta.resourceUri }, requestOptions)
17139
+ async (conn) => {
17140
+ const refreshRead = await state.manager.prepareResourceUse?.(serverName, toolMeta.resourceUri, conn);
17141
+ return conn.client.readResource(
17142
+ { uri: toolMeta.resourceUri },
17143
+ refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions
17144
+ );
17145
+ }
16305
17146
  );
16306
17147
  const content2 = transformMcpResourceContents(result2.contents ?? [], state.owner?.signal);
16307
17148
  const guarded2 = await guardMcpOutput(content2.length > 0 ? content2 : [{ type: "text", text: "(empty resource)" }], outputGuardOptions);
@@ -16327,11 +17168,14 @@ async function executeCall(state, toolName, args, serverOverride, getPiTools, si
16327
17168
  onNeedsAuth: recoverAuthConnection
16328
17169
  },
16329
17170
  serverName,
16330
- (conn) => abortable(conn.client.callTool({
16331
- name: toolMeta.originalName,
16332
- arguments: normalizedArgs,
16333
- _meta: uiSession?.requestMeta
16334
- }, requestOptions), ownedSignal)
17171
+ async (conn) => {
17172
+ await state.manager.ensureListen?.(serverName, conn);
17173
+ return abortable(conn.client.callTool({
17174
+ name: toolMeta.originalName,
17175
+ arguments: normalizedArgs,
17176
+ _meta: uiSession?.requestMeta
17177
+ }, requestOptions), ownedSignal);
17178
+ }
16335
17179
  );
16336
17180
  if (toolMeta.uiResourceUri) {
16337
17181
  uiSession?.sendToolResult(result);
@@ -16406,6 +17250,14 @@ ${formatSchema(toolMeta.inputSchema)}` : "";
16406
17250
  details: { mode: "call", error: "url_elicitation_required", ...callIdentity, action }
16407
17251
  };
16408
17252
  }
17253
+ const inputRequired = getInputRequiredNeedsUiDetails(error, callIdentity);
17254
+ if (inputRequired) {
17255
+ uiSession?.sendToolCancelled(inputRequired.message);
17256
+ return {
17257
+ content: [{ type: "text", text: inputRequired.message }],
17258
+ details: { mode: "call", ...inputRequired }
17259
+ };
17260
+ }
16409
17261
  const message = error instanceof Error ? error.message : String(error);
16410
17262
  uiSession?.sendToolCancelled(message);
16411
17263
  const schemaText = toolMeta.inputSchema ? `
@@ -16748,7 +17600,13 @@ function createMcpToolResultRenderer(renderOptions) {
16748
17600
  function toolErrorOverride(details) {
16749
17601
  if (details && typeof details === "object" && "error" in details) {
16750
17602
  const code = details.error;
16751
- if (code === "tool_error" || code === "call_failed") {
17603
+ if (code === "tool_error" || code === "call_failed" || code === INPUT_REQUIRED_NEEDS_UI) {
17604
+ return { isError: true };
17605
+ }
17606
+ }
17607
+ if (details && typeof details === "object" && details.mode === "script") {
17608
+ const calls = details.calls;
17609
+ if (Array.isArray(calls) && calls.some((call) => !!call && typeof call === "object" && call.error === INPUT_REQUIRED_NEEDS_UI)) {
16752
17610
  return { isError: true };
16753
17611
  }
16754
17612
  }
@@ -17860,7 +18718,7 @@ function installMcpAdapter(pi, options) {
17860
18718
  initPromise = null;
17861
18719
  if (earlyConfig.settings?.freezeDirectTools === true) {
17862
18720
  directToolsFrozen = true;
17863
- logger.info(`MCP: direct tools frozen after initial sync \u2014 reconnects won't rebuild the system prompt; use mcp({ connect: "server" }) to rediscover`);
18721
+ logger.info("MCP: direct tools frozen after initial sync \u2014 metadata can refresh without rebuilding the active tool surface");
17864
18722
  }
17865
18723
  }).catch(async (err) => {
17866
18724
  if (!owner.isActive() || generation !== lifecycleGeneration) {
@@ -18053,7 +18911,6 @@ function installMcpAdapter(pi, options) {
18053
18911
  case "reconnect":
18054
18912
  commandOwner?.throwIfInactive();
18055
18913
  await reconnectServers(state, commandCtx, targetServer);
18056
- if (directToolsFrozen) syncToolSurface(commandCtx);
18057
18914
  break;
18058
18915
  case "tools":
18059
18916
  await showTools(state, commandCtx);
@@ -18364,7 +19221,7 @@ function installMcpAdapter(pi, options) {
18364
19221
  }
18365
19222
  if (params.connect) {
18366
19223
  const result = await executeConnect(state, params.connect, signal);
18367
- syncToolSurface(_ctx);
19224
+ if (!directToolsFrozen) syncToolSurface(_ctx);
18368
19225
  return result;
18369
19226
  }
18370
19227
  if (params.describe) {