one-code-extension 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -219
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +66 -34
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +19 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
package/extensions/mcp/index.ts
CHANGED
|
@@ -16,11 +16,13 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import os from "node:os";
|
|
19
|
-
import {
|
|
19
|
+
import { isDeepStrictEqual } from "node:util";
|
|
20
|
+
import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI, type ExtensionContext, type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
20
21
|
import { Type } from "typebox";
|
|
21
22
|
import { DEFER_CHANNEL } from "../lib/deferred.ts";
|
|
22
23
|
import { MCP_TOOLS_CHANNEL } from "../lib/mcp-share.ts";
|
|
23
24
|
import { persistIfLarge, sessionResultsDir } from "../lib/persisted-output.ts";
|
|
25
|
+
import { emptyToolResultContent } from "../lib/empty-tool-result.ts";
|
|
24
26
|
import { ccToolRenderers } from "../lib/tui-render.ts";
|
|
25
27
|
import {
|
|
26
28
|
callTool,
|
|
@@ -47,14 +49,15 @@ import { readDisabledMcpServers, setMcpServerDisabled } from "../lib/mcp-overrid
|
|
|
47
49
|
import { boundedDockHeight, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
|
|
48
50
|
import { authenticate as runOAuthFlow, silentProvider } from "./oauth/flow.ts";
|
|
49
51
|
import { hasStoredTokens } from "./oauth/store.ts";
|
|
50
|
-
import { announcedFrom, applyMcpDelta, emptyAnnounced, mcpChangeNotices, mcpDelta, type McpSnapshot } from "./announce.ts";
|
|
52
|
+
import { announcedFrom, applyMcpDelta, emptyAnnounced, mcpAnnouncedBaseline, mcpAnnouncedFromReminders, mcpChangeNotices, mcpDelta, serializeMcpAnnounced, type McpAnnounced, type McpSnapshot, mcpStartupNotices } from "./announce.ts";
|
|
53
|
+
import { CONTEXT_BASELINE_CHANNEL, restoredContext } from "../lib/context-stack.ts";
|
|
51
54
|
import { decodeMcpKey } from "./panel/keys.ts";
|
|
52
55
|
import { type McpEntry, type McpEntryStatus } from "./panel/model.ts";
|
|
53
56
|
import { renderMcpPanel, type McpPaint } from "./panel/render.ts";
|
|
54
57
|
import { applyMcpKey, initialMcpState, type McpEffect } from "./panel/state.ts";
|
|
55
58
|
import { sessionAlive } from "../lib/session-lifecycle.ts";
|
|
56
|
-
import { loadServers, type McpServer } from "./config.ts";
|
|
57
|
-
import { approveMcpServers, persistApproval, projectRootOf } from "./trust.ts";
|
|
59
|
+
import { loadServers, type McpServer, noMcpServersMessage, piMcpConfigPaths, registeredMcpServers } from "./config.ts";
|
|
60
|
+
import { approveMcpServers, type McpTrustDeps, persistApproval, projectRootOf, reconnectRefusal } from "./trust.ts";
|
|
58
61
|
import {
|
|
59
62
|
capDescription,
|
|
60
63
|
describeContent,
|
|
@@ -65,6 +68,11 @@ import {
|
|
|
65
68
|
namespacedToolName,
|
|
66
69
|
} from "./schema.ts";
|
|
67
70
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
71
|
+
import { canShowCustomUi, notifyRpcReadOnly } from "../lib/headless-output.ts";
|
|
72
|
+
import { claudeSourcesOn } from "../lib/config-mode.ts";
|
|
73
|
+
import { claudeJsonPath } from "../lib/paths.ts";
|
|
74
|
+
import { consentDialog, startupConsentReady } from "../lib/consent-dialogs.ts";
|
|
75
|
+
import { panelUi } from "../lib/panel-focus.ts";
|
|
68
76
|
|
|
69
77
|
/** Anthropic's hard limit on a tool name; a longer one fails the whole request. */
|
|
70
78
|
const MAX_TOOL_NAME_LENGTH = 128;
|
|
@@ -78,10 +86,13 @@ function unknownServerError(name: string) {
|
|
|
78
86
|
};
|
|
79
87
|
}
|
|
80
88
|
|
|
89
|
+
/** pi's own `mcp.json` files, read because our `/mcp` replaces pi 0.99's built-in MCP (config.ts). */
|
|
90
|
+
const piMcpConfigs = (cwd: string) => piMcpConfigPaths(getAgentDir(), cwd, CONFIG_DIR_NAME);
|
|
91
|
+
|
|
81
92
|
export default function mcpExtension(pi: ExtensionAPI) {
|
|
82
93
|
const connections = new Map<string, Connection>();
|
|
83
94
|
let failures: FailedConnection[] = [];
|
|
84
|
-
const registered = new
|
|
95
|
+
const registered = new Map<string, { server: string; tool: string }>();
|
|
85
96
|
let servers: McpServer[] = [];
|
|
86
97
|
// http servers that returned a 401 with no stored tokens — authNeeded via OAuth,
|
|
87
98
|
// distinct from a missing-env authNeeded (which Authenticate cannot fix).
|
|
@@ -93,6 +104,13 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
93
104
|
const withheldNames = new Map<string, string>();
|
|
94
105
|
// Config file paths contributed by plugins, so the panel can group them.
|
|
95
106
|
let pluginConfigPaths = new Set<string>();
|
|
107
|
+
// pi 0.99's registry of servers other extensions register; absent on older pi.
|
|
108
|
+
// Registered after the session started: our servers are read once per session.
|
|
109
|
+
pi.on("mcp_servers_change", (_event, ctx) => {
|
|
110
|
+
const text = "An extension changed its MCP servers; run /reload to connect the change.";
|
|
111
|
+
if (ctx.hasUI) ctx.ui.notify(text, "info");
|
|
112
|
+
else process.stderr.write(`${text}\n`);
|
|
113
|
+
});
|
|
96
114
|
const home = os.homedir();
|
|
97
115
|
// Live tool definitions, shared with in-process subagents so they reach MCP
|
|
98
116
|
// through these same open connections instead of connecting their own.
|
|
@@ -102,6 +120,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
102
120
|
// those blocks are frozen and later changes ride one-shot notices (./announce.ts).
|
|
103
121
|
let requestSent = false;
|
|
104
122
|
let announced = emptyAnnounced();
|
|
123
|
+
/** A resumed snapshot from before structured baselines: its MCP blocks, until the server names are known. */
|
|
124
|
+
let legacyBlocks: { instructions?: string; failures?: string } | undefined;
|
|
105
125
|
pi.on("context", () => {
|
|
106
126
|
requestSent = true;
|
|
107
127
|
});
|
|
@@ -114,6 +134,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
114
134
|
const resultsDir = (ctx: ExtensionContext | undefined): string => sessionResultsDir(ctx);
|
|
115
135
|
|
|
116
136
|
const registerToolsFor = (connection: Connection) => {
|
|
137
|
+
// Names other extensions own, read once per batch (getAllTools builds every definition).
|
|
138
|
+
let taken: Set<string> | undefined;
|
|
117
139
|
for (const tool of connection.tools) {
|
|
118
140
|
const name = namespacedToolName(connection.server.name, tool.name);
|
|
119
141
|
// Anthropic caps a tool name at 128 characters and rejects the whole
|
|
@@ -128,8 +150,12 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
128
150
|
});
|
|
129
151
|
continue;
|
|
130
152
|
}
|
|
131
|
-
|
|
132
|
-
|
|
153
|
+
const owner = registered.get(name);
|
|
154
|
+
// An unchanged tool on reconnect keeps its exact definition and loaded
|
|
155
|
+
// state. Only a different raw server/tool pair is a name collision.
|
|
156
|
+
if (owner?.server === connection.server.name && owner.tool === tool.name) continue;
|
|
157
|
+
if (owner || (taken ??= new Set(pi.getAllTools().map((existing) => existing.name))).has(name)) {
|
|
158
|
+
// Two tools sanitise to one name (`a.b` and `a_b`, or a server
|
|
133
159
|
// reconnecting under a stale registration). Skipping silently would
|
|
134
160
|
// make the second tool unreachable with no trace; record it where
|
|
135
161
|
// the connection summary reports it.
|
|
@@ -139,7 +165,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
139
165
|
});
|
|
140
166
|
continue;
|
|
141
167
|
}
|
|
142
|
-
registered.
|
|
168
|
+
registered.set(name, { server: connection.server.name, tool: tool.name });
|
|
143
169
|
|
|
144
170
|
const def: ToolDefinition = {
|
|
145
171
|
name,
|
|
@@ -147,7 +173,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
147
173
|
...ccToolRenderers(`${connection.server.name}: ${tool.name}`),
|
|
148
174
|
description: capDescription(tool.description) ?? `MCP tool "${tool.name}" from server "${connection.server.name}".`,
|
|
149
175
|
parameters: jsonSchemaToTypeBox(tool.inputSchema),
|
|
150
|
-
async execute(toolCallId, params,
|
|
176
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
151
177
|
const live = connections.get(connection.server.name);
|
|
152
178
|
if (!live) {
|
|
153
179
|
return {
|
|
@@ -156,17 +182,27 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
156
182
|
isError: true,
|
|
157
183
|
};
|
|
158
184
|
}
|
|
185
|
+
const current = live.tools.find((candidate) => candidate.name === tool.name);
|
|
186
|
+
if (!current || !isDeepStrictEqual(current.inputSchema, tool.inputSchema)) {
|
|
187
|
+
return {
|
|
188
|
+
content: [{ type: "text", text: !current
|
|
189
|
+
? `MCP server "${connection.server.name}" no longer exposes tool "${tool.name}".`
|
|
190
|
+
: `MCP tool "${name}" changed its input schema. Run /reload to load the new definition; this call was not sent.` }],
|
|
191
|
+
details: {} as Record<string, unknown>,
|
|
192
|
+
isError: true,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
159
195
|
try {
|
|
160
|
-
const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown
|
|
161
|
-
const { text, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
|
|
196
|
+
const result = await callTool(live, tool.name, (params ?? {}) as Record<string, unknown>, signal);
|
|
197
|
+
const { text: contentText, images } = describeContent(result.content as McpContentBlock[] | undefined, connection.server.name);
|
|
198
|
+
const structuredText = result.structuredContent !== undefined ? JSON.stringify(result.structuredContent, null, 2) : "";
|
|
199
|
+
const text = [contentText, structuredText].filter(Boolean).join("\n");
|
|
200
|
+
const content = [
|
|
201
|
+
...(text ? [{ type: "text" as const, text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) }] : []),
|
|
202
|
+
...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
|
|
203
|
+
];
|
|
162
204
|
return {
|
|
163
|
-
content:
|
|
164
|
-
{
|
|
165
|
-
type: "text",
|
|
166
|
-
text: text ? persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }) : "(no output)",
|
|
167
|
-
},
|
|
168
|
-
...images.map((image) => ({ type: "image" as const, data: image.data, mimeType: image.mimeType })),
|
|
169
|
-
],
|
|
205
|
+
content: emptyToolResultContent(content, name) ?? content,
|
|
170
206
|
details: { server: connection.server.name, tool: tool.name } as Record<string, unknown>,
|
|
171
207
|
isError: result.isError === true,
|
|
172
208
|
};
|
|
@@ -232,15 +268,35 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
232
268
|
});
|
|
233
269
|
emitInstructions();
|
|
234
270
|
};
|
|
271
|
+
let previousTools = connection.tools;
|
|
272
|
+
connection.onToolsChanged = (error) => {
|
|
273
|
+
if (!alive() || connection.closing || connections.get(server.name) !== connection) return;
|
|
274
|
+
if (error) {
|
|
275
|
+
failures.push({ server, error: error.message });
|
|
276
|
+
pi.events.emit(REMINDER_CHANNEL, { text: `MCP server "${server.name}" could not refresh its tool list: ${error.message}` });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const unavailable = previousTools.filter((old) => {
|
|
280
|
+
const current = connection.tools.find((tool) => tool.name === old.name);
|
|
281
|
+
return !current || !isDeepStrictEqual(current.inputSchema, old.inputSchema);
|
|
282
|
+
});
|
|
283
|
+
previousTools = connection.tools;
|
|
284
|
+
registerToolsFor(connection);
|
|
285
|
+
if (unavailable.length > 0) {
|
|
286
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
287
|
+
text: `MCP server "${server.name}" removed or changed these tools: ${unavailable.map((tool) => namespacedToolName(server.name, tool.name)).join(", ")}. Their old definitions cannot be used. Run /reload to refresh changed schemas.`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
};
|
|
235
291
|
registerToolsFor(connection);
|
|
236
292
|
for (const warning of connection.warnings) failures.push({ server, error: warning });
|
|
237
|
-
if (connection.resources.length > 0) registerResourceTools();
|
|
293
|
+
if (connection.resources.length > 0 || connection.resourceTemplates?.length) registerResourceTools();
|
|
238
294
|
};
|
|
239
295
|
|
|
240
296
|
const connectOne = async (server: McpServer): Promise<void> => {
|
|
241
297
|
failures = failures.filter((f) => f.server.name !== server.name);
|
|
242
298
|
oauthNeeded.delete(server.name);
|
|
243
|
-
const provider = server.kind === "http" && hasStoredTokens(server
|
|
299
|
+
const provider = server.kind === "http" && hasStoredTokens(server) ? silentProvider(server) : undefined;
|
|
244
300
|
try {
|
|
245
301
|
const connection = await connect(server, provider);
|
|
246
302
|
if (!alive()) {
|
|
@@ -265,6 +321,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
265
321
|
* message 1 are (re)written; after it they are frozen and only the changes are
|
|
266
322
|
* announced (./announce.ts).
|
|
267
323
|
*/
|
|
324
|
+
const publishBaseline = (value: McpAnnounced) => {
|
|
325
|
+
pi.events.emit(CONTEXT_BASELINE_CHANNEL, { key: "mcp", value: serializeMcpAnnounced(value) });
|
|
326
|
+
};
|
|
268
327
|
const emitInstructions = () => {
|
|
269
328
|
const live = [...connections.values()];
|
|
270
329
|
const snapshot: McpSnapshot = {
|
|
@@ -274,6 +333,8 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
274
333
|
|
|
275
334
|
if (!requestSent) {
|
|
276
335
|
// Nothing is cached yet: (re)write the standing message-1 blocks.
|
|
336
|
+
const nextAnnounced = announcedFrom(snapshot);
|
|
337
|
+
publishBaseline(nextAnnounced);
|
|
277
338
|
if (snapshot.failed.length === 0) {
|
|
278
339
|
pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-failures", text: "", remove: true });
|
|
279
340
|
} else {
|
|
@@ -300,35 +361,75 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
300
361
|
// being told to use tools that are gone.
|
|
301
362
|
pi.events.emit(REMINDER_CHANNEL, { scope: "every-turn", key: "mcp-instructions", text: "", remove: true });
|
|
302
363
|
}
|
|
303
|
-
announced =
|
|
364
|
+
announced = nextAnnounced;
|
|
304
365
|
return;
|
|
305
366
|
}
|
|
306
367
|
|
|
307
368
|
// Message 1 is frozen (rewriting it would re-cache the whole conversation):
|
|
308
369
|
// tell the model only what changed, as one-shots where it reads next.
|
|
309
370
|
const delta = mcpDelta(announced, snapshot);
|
|
310
|
-
|
|
371
|
+
const notices = mcpChangeNotices(delta);
|
|
372
|
+
if (notices.length > 0) {
|
|
373
|
+
const nextAnnounced = { instructed: new Map(announced.instructed), failed: new Map(announced.failed) };
|
|
374
|
+
applyMcpDelta(nextAnnounced, delta);
|
|
375
|
+
publishBaseline(nextAnnounced);
|
|
376
|
+
}
|
|
377
|
+
for (const text of notices) pi.events.emit(REMINDER_CHANNEL, { text });
|
|
311
378
|
applyMcpDelta(announced, delta);
|
|
312
379
|
};
|
|
313
380
|
|
|
314
381
|
/** Config files that exist but failed to parse (review M11) — shown at startup and in /mcp. */
|
|
315
382
|
let configErrors: string[] = [];
|
|
316
383
|
|
|
384
|
+
/** How the consent check asks, tells and records a "No", for startup and for Reconnect alike. */
|
|
385
|
+
const consentDeps = (ctx: ExtensionContext): McpTrustDeps => ({
|
|
386
|
+
hasUI: ctx.hasUI,
|
|
387
|
+
select: (title, options) => consentDialog(pi.events, (signal) => ctx.ui.select(title, options, { signal })),
|
|
388
|
+
notify: (message) => {
|
|
389
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
390
|
+
else process.stderr.write(`${message}\n`);
|
|
391
|
+
},
|
|
392
|
+
disable: (server) => {
|
|
393
|
+
setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
|
|
394
|
+
disabledNames.add(server.name);
|
|
395
|
+
},
|
|
396
|
+
});
|
|
397
|
+
|
|
317
398
|
const connectAll = async (ctx: ExtensionContext) => {
|
|
318
399
|
const plugins = discoverPlugins(defaultDiscoverRoots(getAgentDir(), ctx.cwd));
|
|
319
400
|
pluginConfigPaths = new Set(plugins.mcpConfigs);
|
|
320
401
|
configErrors = [];
|
|
402
|
+
const configNotes: string[] = [];
|
|
321
403
|
servers = loadServers(ctx.cwd, home, process.env, [...pluginConfigPaths], {
|
|
322
404
|
pluginNames: plugins.mcpConfigPlugins,
|
|
323
405
|
onError: (path, message) => configErrors.push(`${path}: ${message}`),
|
|
406
|
+
piConfigs: piMcpConfigs(ctx.cwd),
|
|
407
|
+
onNote: (message) => configNotes.push(message),
|
|
408
|
+
registered: registeredMcpServers(pi),
|
|
324
409
|
});
|
|
325
|
-
|
|
326
|
-
|
|
410
|
+
// An older snapshot stored only the rendered blocks: read them again by the
|
|
411
|
+
// configured names, before any delta is taken against them.
|
|
412
|
+
if (legacyBlocks) {
|
|
413
|
+
announced = mcpAnnouncedFromReminders(legacyBlocks.instructions, legacyBlocks.failures, servers.map((server) => server.name));
|
|
414
|
+
legacyBlocks = undefined;
|
|
415
|
+
}
|
|
416
|
+
const configWarnings = [
|
|
417
|
+
...(configErrors.length > 0 ? [`MCP config could not be parsed (ignored): ${configErrors.join("; ")}`] : []),
|
|
418
|
+
...configNotes,
|
|
419
|
+
];
|
|
420
|
+
for (const text of configWarnings) {
|
|
327
421
|
if (ctx.hasUI) ctx.ui.notify(text, "warning");
|
|
328
422
|
else process.stderr.write(`${text}\n`);
|
|
329
423
|
}
|
|
330
424
|
disabledNames = readDisabledMcpServers(ctx.cwd, home);
|
|
331
|
-
|
|
425
|
+
// A resumed first-prepend may describe a server whose configuration was
|
|
426
|
+
// removed entirely. There is no connection attempt in that case, so make
|
|
427
|
+
// the frozen-prefix delta explicit rather than leaving the stale capability
|
|
428
|
+
// silently standing. Fresh empty sessions keep their byte-identical no-op.
|
|
429
|
+
if (servers.length === 0) {
|
|
430
|
+
if (requestSent) emitInstructions();
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
332
433
|
|
|
333
434
|
// Disabled servers are listed but never connected; a server with an unset
|
|
334
435
|
// credential env var stays "needs authentication" and is not attempted
|
|
@@ -336,19 +437,13 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
336
437
|
// server needs the user's consent first (trust.ts — a cloned repo must not
|
|
337
438
|
// run commands at startup). Everything else connects.
|
|
338
439
|
const candidates = servers.filter((server) => !disabledNames.has(server.name) && !server.missingEnv?.length);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
disable: (server) => {
|
|
347
|
-
setMcpServerDisabled(server.name, true, "project", ctx.cwd, home);
|
|
348
|
-
disabledNames.add(server.name);
|
|
349
|
-
},
|
|
350
|
-
});
|
|
351
|
-
if (!alive()) return;
|
|
440
|
+
// Reserve the modal position before the asynchronous policy read, so the
|
|
441
|
+
// external-includes dialog cannot jump ahead of MCP's startup consent.
|
|
442
|
+
const consent = await consentDialog(pi.events, (signal) => approveMcpServers(candidates, pluginConfigPaths, ctx.cwd, home, {
|
|
443
|
+
...consentDeps(ctx),
|
|
444
|
+
select: (title, options) => ctx.ui.select(title, options, { signal }),
|
|
445
|
+
}));
|
|
446
|
+
if (!consent || !alive()) return;
|
|
352
447
|
withheldNames.clear();
|
|
353
448
|
for (const { server, reason } of consent.withheld) {
|
|
354
449
|
withheldNames.set(
|
|
@@ -364,16 +459,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
364
459
|
|
|
365
460
|
if (!alive()) return;
|
|
366
461
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
);
|
|
462
|
+
// Warnings from servers that did connect are shown in /mcp, not as failures.
|
|
463
|
+
const failed = failures.filter((f) => !connections.has(f.server.name));
|
|
464
|
+
if (ctx.hasUI) {
|
|
465
|
+
// Needs auth: an OAuth sign-in, or a credential variable that is not set (never attempted).
|
|
466
|
+
const needsAuth = new Set([...oauthNeeded, ...servers.filter((server) => !disabledNames.has(server.name) && server.missingEnv?.length).map((server) => server.name)]).size;
|
|
467
|
+
for (const text of mcpStartupNotices(new Set(failed.map((f) => f.server.name)).size, needsAuth)) ctx.ui.notify(text, "warning");
|
|
468
|
+
} else if (failed.length > 0) {
|
|
469
|
+
// Headless runs have no /mcp, so they keep each server's error on stderr.
|
|
470
|
+
process.stderr.write(`${failed.map((f) => `MCP server "${f.server.name}" failed: ${f.error}`).join("\n")}\n`);
|
|
377
471
|
}
|
|
378
472
|
|
|
379
473
|
emitInstructions();
|
|
@@ -384,12 +478,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
384
478
|
if (resourceToolsRegistered) return;
|
|
385
479
|
resourceToolsRegistered = true;
|
|
386
480
|
|
|
387
|
-
|
|
481
|
+
const registerResourceTool: ExtensionAPI["registerTool"] = (definition) => {
|
|
482
|
+
pi.registerTool(definition);
|
|
483
|
+
sharedTools.push(definition as ToolDefinition);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
registerResourceTool({
|
|
388
487
|
name: "list_mcp_resources",
|
|
389
488
|
label: "MCP Resources",
|
|
390
489
|
...ccToolRenderers("MCP Resources"),
|
|
391
490
|
description:
|
|
392
|
-
"List resources exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri.",
|
|
491
|
+
"List resources and URI templates exposed by connected MCP servers. Resources are readable documents or data the server offers, addressed by uri. Substitute template variables to construct a resource uri.",
|
|
393
492
|
parameters: Type.Object({
|
|
394
493
|
server: Type.Optional(Type.String({ description: "Limit to one server by name" })),
|
|
395
494
|
}),
|
|
@@ -417,12 +516,15 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
417
516
|
`${connection.server.name} ${resource.uri}${resource.name ? ` — ${resource.name}` : ""}${resource.description ? `: ${resource.description}` : ""}`,
|
|
418
517
|
);
|
|
419
518
|
}
|
|
519
|
+
for (const template of connection.resourceTemplates ?? []) {
|
|
520
|
+
lines.push(`${connection.server.name} ${template.uriTemplate} (URI template)${template.name ? ` — ${template.name}` : ""}${template.description ? `: ${template.description}` : ""}`);
|
|
521
|
+
}
|
|
420
522
|
}
|
|
421
523
|
return {
|
|
422
524
|
content: [
|
|
423
525
|
{
|
|
424
526
|
type: "text",
|
|
425
|
-
text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No MCP resources
|
|
527
|
+
text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : "No resources found. MCP servers may still provide tools even if they have no resources.",
|
|
426
528
|
},
|
|
427
529
|
],
|
|
428
530
|
details: { count: lines.length },
|
|
@@ -430,7 +532,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
430
532
|
},
|
|
431
533
|
});
|
|
432
534
|
|
|
433
|
-
|
|
535
|
+
registerResourceTool({
|
|
434
536
|
name: "read_mcp_resource",
|
|
435
537
|
label: "Read MCP Resource",
|
|
436
538
|
...ccToolRenderers("Read MCP Resource"),
|
|
@@ -439,17 +541,17 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
439
541
|
server: Type.String({ description: "Server name that owns the resource" }),
|
|
440
542
|
uri: Type.String({ description: "Resource uri" }),
|
|
441
543
|
}),
|
|
442
|
-
async execute(toolCallId, params,
|
|
544
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
443
545
|
const connection = connections.get(params.server);
|
|
444
546
|
if (!connection) return unknownServerError(params.server);
|
|
445
547
|
try {
|
|
446
|
-
const result = await readResource(connection, params.uri);
|
|
447
|
-
const text = describeResourceContents(result.contents as McpResourceContents[] | undefined);
|
|
548
|
+
const result = await readResource(connection, params.uri, signal);
|
|
549
|
+
const text = describeResourceContents(result.contents as McpResourceContents[] | undefined) || JSON.stringify({ contents: result.contents ?? [] });
|
|
448
550
|
return {
|
|
449
551
|
content: [
|
|
450
552
|
{
|
|
451
553
|
type: "text",
|
|
452
|
-
text:
|
|
554
|
+
text: persistIfLarge(text, { dir: resultsDir(ctx), id: toolCallId }),
|
|
453
555
|
},
|
|
454
556
|
],
|
|
455
557
|
details: { server: params.server, uri: params.uri },
|
|
@@ -464,7 +566,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
464
566
|
},
|
|
465
567
|
});
|
|
466
568
|
|
|
467
|
-
|
|
569
|
+
registerResourceTool({
|
|
468
570
|
name: "read_mcp_resource_dir",
|
|
469
571
|
label: "List MCP Resource Directory",
|
|
470
572
|
...ccToolRenderers("List MCP Resource Directory"),
|
|
@@ -474,18 +576,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
474
576
|
server: Type.String({ description: "Server name that owns the directory" }),
|
|
475
577
|
uri: Type.String({ description: "The directory resource uri to list" }),
|
|
476
578
|
}),
|
|
477
|
-
async execute(toolCallId, params,
|
|
579
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
478
580
|
const connection = connections.get(params.server);
|
|
479
581
|
if (!connection) return unknownServerError(params.server);
|
|
480
582
|
try {
|
|
481
|
-
const result = await readResourceDir(connection, params.uri);
|
|
583
|
+
const result = await readResourceDir(connection, params.uri, signal);
|
|
482
584
|
const entries = (result.resources ?? result.entries ?? []) as Array<{ uri?: string; name?: string; mimeType?: string }>;
|
|
483
585
|
const lines = entries.map((e) => `${e.uri ?? "(no uri)"}${e.name ? ` — ${e.name}` : ""}${e.mimeType ? ` (${e.mimeType})` : ""}`);
|
|
484
586
|
return {
|
|
485
587
|
content: [
|
|
486
588
|
{
|
|
487
589
|
type: "text",
|
|
488
|
-
text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) :
|
|
590
|
+
text: lines.length ? persistIfLarge(lines.join("\n"), { dir: resultsDir(ctx), id: toolCallId }) : 'Directory is empty.\n{"resources":[]}',
|
|
489
591
|
},
|
|
490
592
|
],
|
|
491
593
|
details: { server: params.server, uri: params.uri },
|
|
@@ -503,13 +605,19 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
503
605
|
for (const name of ["list_mcp_resources", "read_mcp_resource", "read_mcp_resource_dir"]) {
|
|
504
606
|
pi.events.emit(DEFER_CHANNEL, { name, keywords: ["mcp", "resource", "document", "uri", "directory"] });
|
|
505
607
|
}
|
|
608
|
+
pi.events.emit(MCP_TOOLS_CHANNEL, { tools: [...sharedTools] });
|
|
506
609
|
};
|
|
507
610
|
|
|
508
611
|
pi.on("session_start", async (_event, ctx) => {
|
|
509
|
-
|
|
510
|
-
//
|
|
511
|
-
|
|
512
|
-
|
|
612
|
+
const restored = restoredContext(pi.events);
|
|
613
|
+
// The restored stack owns message 1. Its structured state records the exact
|
|
614
|
+
// arbitrary server text that was shown; a fresh session keeps prior behavior.
|
|
615
|
+
requestSent = restored !== undefined;
|
|
616
|
+
const stackText = (key: string) => restored?.stack.find((entry) => (entry as { key?: string }).key === key)?.text;
|
|
617
|
+
const structured = mcpAnnouncedBaseline(restored?.baselines.mcp);
|
|
618
|
+
legacyBlocks = structured ? undefined : { instructions: stackText("mcp-instructions"), failures: stackText("mcp-failures") };
|
|
619
|
+
announced = structured ?? mcpAnnouncedFromReminders(legacyBlocks?.instructions, legacyBlocks?.failures);
|
|
620
|
+
if (!restored) publishBaseline(announced);
|
|
513
621
|
// pi awaits session_start handlers serially before the prompt opens, and
|
|
514
622
|
// remote servers take seconds to answer — awaiting here was the entire
|
|
515
623
|
// "slow startup" (4.9s → 0.24s measured, findings §15). In the interactive
|
|
@@ -524,6 +632,9 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
524
632
|
// `failures`, so this only catches setup bugs — surface them loud.
|
|
525
633
|
if (ctx.hasUI) ctx.ui.notify(`MCP startup failed: ${(error as Error).message}`, "error");
|
|
526
634
|
});
|
|
635
|
+
// The external-includes question follows the startup hooks/MCP consent,
|
|
636
|
+
// but does not wait for slow server connections.
|
|
637
|
+
startupConsentReady(pi.events);
|
|
527
638
|
connecting.finally(() => {
|
|
528
639
|
connectSettled = true;
|
|
529
640
|
// The session may be gone by now (/clear while a remote server was still
|
|
@@ -600,12 +711,18 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
600
711
|
if (pluginConfigPaths.has(server.source)) {
|
|
601
712
|
return { rank: 3, group: "Plugin MCPs", configLocation: "Plugin configuration", scope: "project" };
|
|
602
713
|
}
|
|
603
|
-
if (server.source ===
|
|
714
|
+
if (server.source === claudeJsonPath(home) || server.piOrigin === "user") {
|
|
604
715
|
return { rank: 0, group: `User MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "user" };
|
|
605
716
|
}
|
|
606
717
|
if (server.source.endsWith(join(".claude", "settings.local.json"))) {
|
|
607
718
|
return { rank: 2, group: "Project MCPs (.claude/settings.local.json)", configLocation: shorten(server.source), scope: "project" };
|
|
608
719
|
}
|
|
720
|
+
if (server.piOrigin === "extension") {
|
|
721
|
+
return { rank: 3, group: "Extension MCPs", configLocation: shorten(server.source), scope: "user" };
|
|
722
|
+
}
|
|
723
|
+
if (server.piOrigin === "project") {
|
|
724
|
+
return { rank: 1, group: `Project MCPs (${join(CONFIG_DIR_NAME, "mcp.json")})`, configLocation: shorten(server.source), scope: "project" };
|
|
725
|
+
}
|
|
609
726
|
return { rank: 1, group: `Project MCPs (${shorten(server.source)})`, configLocation: shorten(server.source), scope: "project" };
|
|
610
727
|
};
|
|
611
728
|
|
|
@@ -626,7 +743,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
626
743
|
// honestly from whether tokens are stored — not just "failed http".
|
|
627
744
|
const authState =
|
|
628
745
|
status === "failed" && server.kind === "http"
|
|
629
|
-
? hasStoredTokens(server
|
|
746
|
+
? hasStoredTokens(server)
|
|
630
747
|
? ("authenticated" as const)
|
|
631
748
|
: ("notAuthenticated" as const)
|
|
632
749
|
: undefined;
|
|
@@ -656,7 +773,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
656
773
|
const MCP_PANEL_MAX_HEIGHT = 22;
|
|
657
774
|
|
|
658
775
|
const openMcpPanel = async (ctx: ExtensionContext): Promise<void> => {
|
|
659
|
-
await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
|
|
776
|
+
await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
|
|
660
777
|
const paint: McpPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
|
|
661
778
|
const state = initialMcpState();
|
|
662
779
|
let notices: string[] = [];
|
|
@@ -692,7 +809,24 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
692
809
|
const runReconnect = async (entry: McpEntry) => {
|
|
693
810
|
const server = servers.find((s) => s.name === entry.name);
|
|
694
811
|
if (!server || busy.has(entry.name)) return;
|
|
812
|
+
const refusal = reconnectRefusal(server);
|
|
813
|
+
if (refusal) {
|
|
814
|
+
notices = [refusal];
|
|
815
|
+
syncRepaint();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
695
818
|
busy.add(entry.name);
|
|
819
|
+
// Reconnect spawns the command again, so a project server needs the
|
|
820
|
+
// same consent startup asked for (a stored or session approval
|
|
821
|
+
// passes without a prompt).
|
|
822
|
+
const consent = await approveMcpServers([server], pluginConfigPaths, ctx.cwd, home, consentDeps(ctx));
|
|
823
|
+
if (consent.approved.length === 0) {
|
|
824
|
+
busy.delete(entry.name);
|
|
825
|
+
if (!alive()) return;
|
|
826
|
+
notices = [`"${entry.name}" was not approved, so it was not reconnected.`];
|
|
827
|
+
syncRepaint();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
696
830
|
notices = [`Reconnecting to "${entry.name}"…`];
|
|
697
831
|
syncRepaint();
|
|
698
832
|
const existing = connections.get(entry.name);
|
|
@@ -844,16 +978,19 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
844
978
|
};
|
|
845
979
|
|
|
846
980
|
registerLocalCommand(pi, "mcp", {
|
|
981
|
+
reportsResult: true,
|
|
847
982
|
description: "Manage MCP servers (status, reconnect, authenticate, enable/disable)",
|
|
848
983
|
handler: async (args, ctx) => {
|
|
849
984
|
if (servers.length === 0) {
|
|
850
|
-
|
|
851
|
-
|
|
985
|
+
const stdout = noMcpServersMessage(claudeSourcesOn());
|
|
986
|
+
ctx.ui.notify(stdout, "info");
|
|
987
|
+
return stdout;
|
|
852
988
|
}
|
|
853
|
-
if (ctx
|
|
989
|
+
if (canShowCustomUi(ctx)) {
|
|
854
990
|
await openMcpPanel(ctx);
|
|
855
|
-
return;
|
|
991
|
+
return "MCP dialog dismissed";
|
|
856
992
|
}
|
|
993
|
+
const readOnly = notifyRpcReadOnly(ctx, "/mcp", "reconnect, authenticate, enable or disable servers");
|
|
857
994
|
// Non-interactive fallback: a flat status listing, with tool/resource
|
|
858
995
|
// counts and any warning for connected servers.
|
|
859
996
|
const lines = servers.map((server) => {
|
|
@@ -869,6 +1006,7 @@ export default function mcpExtension(pi: ExtensionAPI) {
|
|
|
869
1006
|
return `${status.padEnd(12)} ${server.name}${suffix} (${server.source})`;
|
|
870
1007
|
});
|
|
871
1008
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
1009
|
+
return [readOnly, ...lines].filter(Boolean).join("\n");
|
|
872
1010
|
},
|
|
873
1011
|
});
|
|
874
1012
|
}
|
|
@@ -1,26 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Open a URL in the user's default browser (
|
|
2
|
+
* Open a sign-in URL in the user's default browser (`lib/open-browser.ts`),
|
|
3
|
+
* falling back to printing the URL for manual paste when no browser starts.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The authorization URL is the server's choice (its metadata names the
|
|
6
|
+
* endpoint, the SDK appends `&`-joined parameters), so only `https:`, or
|
|
7
|
+
* `http:` on a loopback host, is opened: `open` and `xdg-open` hand any other
|
|
8
|
+
* scheme (`file:`, a custom handler) to whatever the desktop registered for it.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
|
-
import {
|
|
11
|
+
import { launchOpener, type OpenerPlan, openerPlan } from "../../lib/open-browser.ts";
|
|
10
12
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
export type BrowserPlan = OpenerPlan | { refused: string };
|
|
14
|
+
|
|
15
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
16
|
+
|
|
17
|
+
/** Why a URL must not be opened, or undefined when it may. */
|
|
18
|
+
export function refuseAuthorizationUrl(url: string): string | undefined {
|
|
19
|
+
let parsed: URL;
|
|
18
20
|
try {
|
|
19
|
-
|
|
20
|
-
child.on("error", () => {});
|
|
21
|
-
child.unref();
|
|
22
|
-
return true;
|
|
21
|
+
parsed = new URL(url);
|
|
23
22
|
} catch {
|
|
24
|
-
return
|
|
23
|
+
return "it is not a valid URL";
|
|
25
24
|
}
|
|
25
|
+
if (parsed.protocol === "https:") return undefined;
|
|
26
|
+
if (parsed.protocol === "http:" && LOOPBACK_HOSTS.has(parsed.hostname)) return undefined;
|
|
27
|
+
return `its scheme is ${parsed.protocol} on ${parsed.hostname || "no host"}; a sign-in URL must be https (or http on a loopback host)`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The command and argv that open `url` on `platform`, or why it is refused. Pure. */
|
|
31
|
+
export function browserOpenPlan(url: string, platform: NodeJS.Platform = process.platform, env: Record<string, string | undefined> = process.env): BrowserPlan {
|
|
32
|
+
const refused = refuseAuthorizationUrl(url);
|
|
33
|
+
if (refused) return { refused };
|
|
34
|
+
return openerPlan(url, platform, env);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Open `url`, or throw (synchronously) when it is refused: a sign-in the
|
|
39
|
+
* server pointed at a non-web scheme is a hostile or broken server, and the
|
|
40
|
+
* error names why. Resolves false when the opener could not start (no browser
|
|
41
|
+
* on the box).
|
|
42
|
+
*/
|
|
43
|
+
export function openBrowser(url: string): Promise<boolean> {
|
|
44
|
+
const plan = browserOpenPlan(url);
|
|
45
|
+
if ("refused" in plan) throw new Error(`refusing to open the authorization URL: ${plan.refused}`);
|
|
46
|
+
return launchOpener(plan);
|
|
26
47
|
}
|