@herbertgao/pi-extensions 2026.8.14 → 2026.9.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 +19 -13
- package/THIRD_PARTY_NOTICES.md +26 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +8 -1
- package/node_modules/@czottmann/pi-automode/README.md +4 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +71 -9
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/pi-bark/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +129 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +283 -59
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +264 -24
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +18 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
- package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
- package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
- package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
- package/node_modules/@tifan/pi-handoff/README.md +39 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +166 -5
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +42 -0
- package/node_modules/pi-mcp-adapter/README.md +15 -15
- package/node_modules/pi-mcp-adapter/commands.ts +59 -22
- package/node_modules/pi-mcp-adapter/config.ts +18 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +12 -4
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
- package/node_modules/pi-mcp-adapter/errors.ts +141 -0
- package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
- package/node_modules/pi-mcp-adapter/index.ts +2 -3
- package/node_modules/pi-mcp-adapter/init.ts +5 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
- package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +88 -25
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +343 -6
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
- package/node_modules/pi-mcp-adapter/types.ts +19 -4
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
- package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
- package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
- package/node_modules/pi-web-access/CHANGELOG.md +20 -0
- package/node_modules/pi-web-access/README.md +15 -2
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +1 -1
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/package.json +21 -17
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
type GetPromptResult,
|
|
12
12
|
type ListToolsResult,
|
|
13
13
|
type ReadResourceResult,
|
|
14
|
+
type McpSubscription,
|
|
15
|
+
type SubscriptionFilter,
|
|
14
16
|
type CacheableRequestOptions,
|
|
15
17
|
type RequestOptions,
|
|
16
18
|
type UrlElicitationRequiredError,
|
|
@@ -28,15 +30,17 @@ import {
|
|
|
28
30
|
type ServerStreamResultPatchNotification,
|
|
29
31
|
type Transport,
|
|
30
32
|
type McpTraceSettings,
|
|
33
|
+
type McpListenState,
|
|
31
34
|
SERVER_STREAM_RESULT_PATCH_METHOD,
|
|
32
35
|
serverStreamResultPatchNotificationSchema,
|
|
33
36
|
} from "./types.ts";
|
|
34
37
|
import { resolveNpxBinary } from "./npx-resolver.ts";
|
|
35
38
|
import { createJsonSchemaValidator } from "./json-schema-validator.ts";
|
|
36
39
|
import { logger } from "./logger.ts";
|
|
40
|
+
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
37
41
|
import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
38
42
|
import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
39
|
-
import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
|
|
43
|
+
import { inspectAuthForUrl, invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
|
|
40
44
|
import { getBearerTokenForUrl } from "./mcp-bearer-store.ts";
|
|
41
45
|
import { registerSamplingHandler, type ServerSamplingConfig } from "./sampling-handler.ts";
|
|
42
46
|
import {
|
|
@@ -71,6 +75,7 @@ const abortCleanupPromises = new WeakMap<object, Promise<void>>();
|
|
|
71
75
|
type HttpAuthProviderState =
|
|
72
76
|
| { status: "disabled" }
|
|
73
77
|
| { status: "implicit-deferred" }
|
|
78
|
+
| { status: "implicit-stored"; provider: McpOAuthProvider }
|
|
74
79
|
| { status: "explicit"; provider: McpOAuthProvider }
|
|
75
80
|
| { status: "implicit-challenged"; provider: McpOAuthProvider };
|
|
76
81
|
|
|
@@ -141,6 +146,18 @@ export interface ServerConnection {
|
|
|
141
146
|
lastUsedAt: number;
|
|
142
147
|
inFlight: number;
|
|
143
148
|
status: "connected" | "closed" | "needs-auth";
|
|
149
|
+
/** Catalog subscription health, tracked independently from transport health. */
|
|
150
|
+
listenState: McpListenState;
|
|
151
|
+
listenSubscription?: McpSubscription;
|
|
152
|
+
/** Last requested filter; the server's honored filter may be a subset. */
|
|
153
|
+
listenFilter?: SubscriptionFilter;
|
|
154
|
+
/** True when recovery has an active listen but could not confirm every catalog list. */
|
|
155
|
+
listenCatalogStale?: boolean;
|
|
156
|
+
listenPromise?: Promise<void>;
|
|
157
|
+
listenRetryAfter?: number;
|
|
158
|
+
listenStopped?: boolean;
|
|
159
|
+
recentResourceUris?: Map<string, number>;
|
|
160
|
+
resourceReadRefreshUris?: Set<string>;
|
|
144
161
|
/** True once this needs-auth episode discarded the cached credential. */
|
|
145
162
|
credentialsInvalidated?: boolean;
|
|
146
163
|
}
|
|
@@ -148,6 +165,8 @@ export interface ServerConnection {
|
|
|
148
165
|
|
|
149
166
|
type UiStreamListener = (serverName: string, notification: ServerStreamResultPatchNotification["params"]) => void;
|
|
150
167
|
type MetadataListChangedListener = (serverName: string, reason: string) => void;
|
|
168
|
+
type ListenStateChangedListener = (serverName: string, state: McpListenState) => void;
|
|
169
|
+
type ResourceUpdatedListener = (serverName: string, uri: string) => void;
|
|
151
170
|
|
|
152
171
|
export type ToolRefreshResult = "updated" | "unchanged" | "superseded" | "refresh-timeout";
|
|
153
172
|
|
|
@@ -155,6 +174,9 @@ type ToolListCacheHints = Partial<Pick<ListToolsResult, "ttlMs" | "cacheScope">>
|
|
|
155
174
|
type ToolListResult = { tools: McpTool[]; hints?: ToolListCacheHints };
|
|
156
175
|
|
|
157
176
|
const KEEP_ALIVE_REFRESH_TIMEOUT_MS = 5_000;
|
|
177
|
+
const LISTEN_RETRY_DELAY_MS = 5_000;
|
|
178
|
+
const RECENT_RESOURCE_TTL_MS = 10 * 60_000;
|
|
179
|
+
const MAX_RESOURCE_SUBSCRIPTIONS = 32;
|
|
158
180
|
|
|
159
181
|
export function isTransientHttpConnectError(error: unknown): boolean {
|
|
160
182
|
let current: unknown = error;
|
|
@@ -172,6 +194,12 @@ export class McpServerManager {
|
|
|
172
194
|
private uiStreamListeners = new Map<string, UiStreamListener>();
|
|
173
195
|
private samplingConfig: ServerSamplingConfig | undefined;
|
|
174
196
|
private metadataListChangedListener: MetadataListChangedListener | undefined;
|
|
197
|
+
private listenStateChangedListener: ListenStateChangedListener | undefined;
|
|
198
|
+
private resourceUpdatedListeners = new Map<string, {
|
|
199
|
+
serverName: string;
|
|
200
|
+
uri: string;
|
|
201
|
+
listener: ResourceUpdatedListener;
|
|
202
|
+
}>();
|
|
175
203
|
private pendingMetadataPublications = new Map<
|
|
176
204
|
string,
|
|
177
205
|
{ connection: ServerConnection; reason: string }
|
|
@@ -200,6 +228,10 @@ export class McpServerManager {
|
|
|
200
228
|
this.metadataListChangedListener = listener;
|
|
201
229
|
}
|
|
202
230
|
|
|
231
|
+
setListenStateChangedListener(listener: ListenStateChangedListener | undefined): void {
|
|
232
|
+
this.listenStateChangedListener = listener;
|
|
233
|
+
}
|
|
234
|
+
|
|
203
235
|
publishMetadataChanged(
|
|
204
236
|
name: string,
|
|
205
237
|
expectedConnection: ServerConnection,
|
|
@@ -312,6 +344,10 @@ export class McpServerManager {
|
|
|
312
344
|
throw new Error(`MCP connection for ${name} was closed while connecting`);
|
|
313
345
|
}
|
|
314
346
|
this.connections.set(name, connection);
|
|
347
|
+
this.watchListenSubscription(name, connection, connection.listenSubscription);
|
|
348
|
+
if ([...this.resourceUpdatedListeners.values()].some(registration => registration.serverName === name)) {
|
|
349
|
+
void this.ensureListen(name, connection);
|
|
350
|
+
}
|
|
315
351
|
return connection;
|
|
316
352
|
} finally {
|
|
317
353
|
if (this.connectPromises.get(name) === promise) this.connectPromises.delete(name);
|
|
@@ -372,6 +408,8 @@ export class McpServerManager {
|
|
|
372
408
|
return "superseded";
|
|
373
409
|
}
|
|
374
410
|
|
|
411
|
+
await this.ensureListen(name, expectedConnection);
|
|
412
|
+
|
|
375
413
|
const requestOptions = this.buildRequestOptions(expectedConnection.definition, signal);
|
|
376
414
|
const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
|
|
377
415
|
const healthOptions = {
|
|
@@ -456,6 +494,250 @@ export class McpServerManager {
|
|
|
456
494
|
}
|
|
457
495
|
}
|
|
458
496
|
|
|
497
|
+
private setListenState(name: string, connection: ServerConnection, state: McpListenState): void {
|
|
498
|
+
if (connection.listenState === state) return;
|
|
499
|
+
connection.listenState = state;
|
|
500
|
+
if (this.connections.get(name) === connection) {
|
|
501
|
+
try {
|
|
502
|
+
this.listenStateChangedListener?.(name, state);
|
|
503
|
+
} catch {
|
|
504
|
+
// Status consumers must not interrupt listen lifecycle recovery.
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private catalogListenFilter(connection: ServerConnection): SubscriptionFilter {
|
|
510
|
+
const capabilities = connection.client.getServerCapabilities?.();
|
|
511
|
+
return {
|
|
512
|
+
...(capabilities?.tools?.listChanged ? { toolsListChanged: true } : {}),
|
|
513
|
+
...(capabilities?.prompts?.listChanged ? { promptsListChanged: true } : {}),
|
|
514
|
+
...(capabilities?.resources?.listChanged ? { resourcesListChanged: true } : {}),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
private currentListenFilter(name: string, connection: ServerConnection): SubscriptionFilter {
|
|
519
|
+
const now = Date.now();
|
|
520
|
+
const recent = connection.recentResourceUris;
|
|
521
|
+
if (recent) {
|
|
522
|
+
for (const [uri, touchedAt] of recent) {
|
|
523
|
+
if (now - touchedAt > RECENT_RESOURCE_TTL_MS) recent.delete(uri);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
const openResourceUris = [...new Set(
|
|
527
|
+
[...this.resourceUpdatedListeners.values()]
|
|
528
|
+
.filter(registration => registration.serverName === name)
|
|
529
|
+
.map(registration => registration.uri),
|
|
530
|
+
)].slice(-MAX_RESOURCE_SUBSCRIPTIONS);
|
|
531
|
+
const openSet = new Set(openResourceUris);
|
|
532
|
+
const recentSlots = MAX_RESOURCE_SUBSCRIPTIONS - openResourceUris.length;
|
|
533
|
+
const recentResourceUris = recentSlots > 0
|
|
534
|
+
? [...(recent?.keys() ?? [])].filter(uri => !openSet.has(uri)).slice(-recentSlots)
|
|
535
|
+
: [];
|
|
536
|
+
const resourceSubscriptions = [...openResourceUris, ...recentResourceUris];
|
|
537
|
+
return {
|
|
538
|
+
...this.catalogListenFilter(connection),
|
|
539
|
+
...(resourceSubscriptions.length > 0 ? { resourceSubscriptions } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private watchListenSubscription(
|
|
544
|
+
name: string,
|
|
545
|
+
connection: ServerConnection,
|
|
546
|
+
subscription: McpSubscription | undefined,
|
|
547
|
+
): void {
|
|
548
|
+
if (!subscription) return;
|
|
549
|
+
void subscription.closed.then(cause => {
|
|
550
|
+
if (
|
|
551
|
+
this.stopped ||
|
|
552
|
+
this.connections.get(name) !== connection ||
|
|
553
|
+
connection.status !== "connected" ||
|
|
554
|
+
connection.listenSubscription !== subscription
|
|
555
|
+
) return;
|
|
556
|
+
if (cause === "remote") {
|
|
557
|
+
const staleUris = connection.listenFilter?.resourceSubscriptions ?? [];
|
|
558
|
+
if (staleUris.length > 0) connection.resourceReadRefreshUris = new Set(staleUris);
|
|
559
|
+
connection.listenCatalogStale = true;
|
|
560
|
+
connection.listenRetryAfter = Date.now();
|
|
561
|
+
this.setListenState(name, connection, "dropped");
|
|
562
|
+
} else if (cause === "graceful" || connection.listenState !== "re-establishing") {
|
|
563
|
+
connection.listenStopped = true;
|
|
564
|
+
this.setListenState(name, connection, "not-listening");
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/** Quietly repairs a modern catalog listen at an existing activity boundary. */
|
|
570
|
+
async ensureListen(name: string, expectedConnection: ServerConnection): Promise<void> {
|
|
571
|
+
if (
|
|
572
|
+
this.stopped ||
|
|
573
|
+
this.connections.get(name) !== expectedConnection ||
|
|
574
|
+
expectedConnection.status !== "connected" ||
|
|
575
|
+
expectedConnection.listenStopped ||
|
|
576
|
+
expectedConnection.client.getProtocolEra?.() !== "modern"
|
|
577
|
+
) return;
|
|
578
|
+
|
|
579
|
+
const filter = this.currentListenFilter(name, expectedConnection);
|
|
580
|
+
if (Object.keys(filter).length === 0) {
|
|
581
|
+
this.setListenState(name, expectedConnection, "not-listening");
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (expectedConnection.listenPromise) {
|
|
585
|
+
await expectedConnection.listenPromise;
|
|
586
|
+
return this.ensureListen(name, expectedConnection);
|
|
587
|
+
}
|
|
588
|
+
if ((expectedConnection.listenRetryAfter ?? 0) > Date.now()) return;
|
|
589
|
+
|
|
590
|
+
const sameFilter = isDeepStrictEqual(expectedConnection.listenFilter, filter);
|
|
591
|
+
if (expectedConnection.listenState === "active" && sameFilter) {
|
|
592
|
+
if (!expectedConnection.listenCatalogStale) return;
|
|
593
|
+
const attempt = (async () => {
|
|
594
|
+
this.setListenState(name, expectedConnection, "re-establishing");
|
|
595
|
+
const confirmed = await this.reconcileCatalogAfterListen(name, expectedConnection, this.buildRequestOptions(expectedConnection.definition));
|
|
596
|
+
if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
|
|
597
|
+
this.setListenState(name, expectedConnection, "active");
|
|
598
|
+
})().finally(() => {
|
|
599
|
+
if (expectedConnection.listenPromise === attempt) delete expectedConnection.listenPromise;
|
|
600
|
+
});
|
|
601
|
+
expectedConnection.listenPromise = attempt;
|
|
602
|
+
return attempt;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const attempt = (async () => {
|
|
606
|
+
const recoverDroppedListen = expectedConnection.listenState === "dropped";
|
|
607
|
+
this.setListenState(name, expectedConnection, "re-establishing");
|
|
608
|
+
const previous = expectedConnection.listenSubscription;
|
|
609
|
+
if (previous) await previous.close().catch(() => {});
|
|
610
|
+
if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
|
|
611
|
+
try {
|
|
612
|
+
const requestOptions = this.buildRequestOptions(expectedConnection.definition);
|
|
613
|
+
const subscription = await expectedConnection.client.listen(
|
|
614
|
+
filter,
|
|
615
|
+
{
|
|
616
|
+
...requestOptions,
|
|
617
|
+
timeout: Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS),
|
|
618
|
+
},
|
|
619
|
+
);
|
|
620
|
+
if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") {
|
|
621
|
+
await subscription.close().catch(() => {});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
expectedConnection.listenSubscription = subscription;
|
|
625
|
+
expectedConnection.listenFilter = filter;
|
|
626
|
+
delete expectedConnection.listenStopped;
|
|
627
|
+
delete expectedConnection.listenRetryAfter;
|
|
628
|
+
this.watchListenSubscription(name, expectedConnection, subscription);
|
|
629
|
+
const confirmed = recoverDroppedListen
|
|
630
|
+
? await this.reconcileCatalogAfterListen(name, expectedConnection, requestOptions)
|
|
631
|
+
: true;
|
|
632
|
+
if (!confirmed) expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
|
|
633
|
+
this.setListenState(name, expectedConnection, "active");
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return;
|
|
636
|
+
if (expectedConnection.listenSubscription) await expectedConnection.listenSubscription.close().catch(() => {});
|
|
637
|
+
expectedConnection.listenRetryAfter = Date.now() + LISTEN_RETRY_DELAY_MS;
|
|
638
|
+
this.setListenState(name, expectedConnection, "dropped");
|
|
639
|
+
logger.debug(`MCP: catalog listen repair failed for ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
640
|
+
}
|
|
641
|
+
})().finally(() => {
|
|
642
|
+
if (expectedConnection.listenPromise === attempt) delete expectedConnection.listenPromise;
|
|
643
|
+
});
|
|
644
|
+
expectedConnection.listenPromise = attempt;
|
|
645
|
+
return attempt;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
private async reconcileCatalogAfterListen(
|
|
649
|
+
name: string,
|
|
650
|
+
expectedConnection: ServerConnection,
|
|
651
|
+
requestOptions?: CacheableRequestOptions,
|
|
652
|
+
): Promise<boolean> {
|
|
653
|
+
const timeout = Math.min(requestOptions?.timeout ?? KEEP_ALIVE_REFRESH_TIMEOUT_MS, KEEP_ALIVE_REFRESH_TIMEOUT_MS);
|
|
654
|
+
const refreshSignal = combineAbortSignals(requestOptions?.signal, AbortSignal.timeout(timeout));
|
|
655
|
+
const refreshOptions: CacheableRequestOptions = {
|
|
656
|
+
...requestOptions,
|
|
657
|
+
timeout,
|
|
658
|
+
cacheMode: "refresh",
|
|
659
|
+
...(refreshSignal ? { signal: refreshSignal } : {}),
|
|
660
|
+
};
|
|
661
|
+
const [toolResult, resources, promptResult] = await Promise.allSettled([
|
|
662
|
+
this.fetchAllTools(expectedConnection.client, refreshOptions),
|
|
663
|
+
this.fetchAllResources(expectedConnection.client, refreshOptions, true),
|
|
664
|
+
this.fetchAllPrompts(expectedConnection.client, refreshOptions),
|
|
665
|
+
]);
|
|
666
|
+
if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
|
|
667
|
+
|
|
668
|
+
const nextTools = toolResult.status === "fulfilled" ? toolResult.value : undefined;
|
|
669
|
+
const nextResources = resources.status === "fulfilled" ? resources.value : undefined;
|
|
670
|
+
const nextPrompts = promptResult.status === "fulfilled" && !promptResult.value.failed ? promptResult.value : undefined;
|
|
671
|
+
const confirmed = nextTools !== undefined && nextResources !== undefined && nextPrompts !== undefined;
|
|
672
|
+
const changed = (nextTools !== undefined && (
|
|
673
|
+
!isDeepStrictEqual(expectedConnection.tools, nextTools.tools) ||
|
|
674
|
+
!isDeepStrictEqual(expectedConnection.toolListHints, nextTools.hints)
|
|
675
|
+
)) ||
|
|
676
|
+
(nextResources !== undefined && !isDeepStrictEqual(expectedConnection.resources, nextResources)) ||
|
|
677
|
+
(nextPrompts !== undefined && (
|
|
678
|
+
!isDeepStrictEqual(expectedConnection.prompts, nextPrompts.prompts) ||
|
|
679
|
+
expectedConnection.promptDiscoveryFailed !== false
|
|
680
|
+
));
|
|
681
|
+
if (!changed) {
|
|
682
|
+
this.retryPendingMetadataPublication(name, expectedConnection);
|
|
683
|
+
if (confirmed) delete expectedConnection.listenCatalogStale;
|
|
684
|
+
else expectedConnection.listenCatalogStale = true;
|
|
685
|
+
return confirmed;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
if (nextTools !== undefined) {
|
|
689
|
+
expectedConnection.tools = nextTools.tools;
|
|
690
|
+
expectedConnection.toolListHints = nextTools.hints;
|
|
691
|
+
expectedConnection.toolsRevision = (expectedConnection.toolsRevision ?? 0) + 1;
|
|
692
|
+
}
|
|
693
|
+
if (nextResources !== undefined) expectedConnection.resources = nextResources;
|
|
694
|
+
if (nextPrompts !== undefined) {
|
|
695
|
+
expectedConnection.prompts = nextPrompts.prompts;
|
|
696
|
+
expectedConnection.promptDiscoveryFailed = false;
|
|
697
|
+
}
|
|
698
|
+
if (confirmed) delete expectedConnection.listenCatalogStale;
|
|
699
|
+
else expectedConnection.listenCatalogStale = true;
|
|
700
|
+
this.metadataListChangedListener?.(name, "listen-recovered");
|
|
701
|
+
this.pendingMetadataPublications.delete(name);
|
|
702
|
+
return confirmed;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
async prepareResourceUse(name: string, uri: string, expectedConnection: ServerConnection): Promise<boolean> {
|
|
706
|
+
if (this.connections.get(name) !== expectedConnection || expectedConnection.status !== "connected") return false;
|
|
707
|
+
const refreshRead = expectedConnection.listenState === "dropped" ||
|
|
708
|
+
expectedConnection.listenState === "re-establishing" ||
|
|
709
|
+
expectedConnection.resourceReadRefreshUris?.has(uri) === true;
|
|
710
|
+
const recent = expectedConnection.recentResourceUris ?? new Map<string, number>();
|
|
711
|
+
recent.delete(uri);
|
|
712
|
+
recent.set(uri, Date.now());
|
|
713
|
+
while (recent.size > MAX_RESOURCE_SUBSCRIPTIONS) {
|
|
714
|
+
const oldest = recent.keys().next().value as string | undefined;
|
|
715
|
+
if (oldest === undefined) break;
|
|
716
|
+
recent.delete(oldest);
|
|
717
|
+
}
|
|
718
|
+
expectedConnection.recentResourceUris = recent;
|
|
719
|
+
await this.ensureListen(name, expectedConnection);
|
|
720
|
+
expectedConnection.resourceReadRefreshUris?.delete(uri);
|
|
721
|
+
return refreshRead;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
registerResourceUpdatedListener(
|
|
725
|
+
token: string,
|
|
726
|
+
serverName: string,
|
|
727
|
+
uri: string,
|
|
728
|
+
listener: ResourceUpdatedListener,
|
|
729
|
+
): void {
|
|
730
|
+
this.removeResourceUpdatedListener(token);
|
|
731
|
+
this.resourceUpdatedListeners.set(token, { serverName, uri, listener });
|
|
732
|
+
const connection = this.connections.get(serverName);
|
|
733
|
+
if (!connection || connection.status !== "connected") return;
|
|
734
|
+
void this.prepareResourceUse(serverName, uri, connection);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
removeResourceUpdatedListener(token: string): void {
|
|
738
|
+
this.resourceUpdatedListeners.delete(token);
|
|
739
|
+
}
|
|
740
|
+
|
|
459
741
|
private async doReconnect(
|
|
460
742
|
name: string,
|
|
461
743
|
definition: ServerDefinition,
|
|
@@ -570,6 +852,7 @@ export class McpServerManager {
|
|
|
570
852
|
lastUsedAt: Date.now(),
|
|
571
853
|
inFlight: 0,
|
|
572
854
|
status: "needs-auth",
|
|
855
|
+
listenState: "disconnected",
|
|
573
856
|
credentialsInvalidated: invalidated,
|
|
574
857
|
};
|
|
575
858
|
}
|
|
@@ -593,6 +876,8 @@ export class McpServerManager {
|
|
|
593
876
|
this.attachAdapterNotificationHandlers(name, client);
|
|
594
877
|
|
|
595
878
|
const instructions = client.getInstructions?.();
|
|
879
|
+
const protocolEra = client.getProtocolEra?.();
|
|
880
|
+
const autoOpenedSubscription = client.autoOpenedSubscription;
|
|
596
881
|
const connection: ServerConnection = {
|
|
597
882
|
client,
|
|
598
883
|
transport,
|
|
@@ -605,7 +890,16 @@ export class McpServerManager {
|
|
|
605
890
|
lastUsedAt: Date.now(),
|
|
606
891
|
inFlight: 0,
|
|
607
892
|
status: "connected",
|
|
893
|
+
listenState: protocolEra === "modern"
|
|
894
|
+
? autoOpenedSubscription
|
|
895
|
+
? "active"
|
|
896
|
+
: "not-listening"
|
|
897
|
+
: "legacy",
|
|
898
|
+
...(autoOpenedSubscription ? {
|
|
899
|
+
listenSubscription: autoOpenedSubscription,
|
|
900
|
+
} : {}),
|
|
608
901
|
};
|
|
902
|
+
if (autoOpenedSubscription) connection.listenFilter = this.catalogListenFilter(connection);
|
|
609
903
|
|
|
610
904
|
// Reflect the SDK's own close signal in connection status, guarded by
|
|
611
905
|
// identity so a stale connection's late close can never clobber a fresh
|
|
@@ -663,6 +957,7 @@ export class McpServerManager {
|
|
|
663
957
|
lastUsedAt: Date.now(),
|
|
664
958
|
inFlight: 0,
|
|
665
959
|
status: "needs-auth",
|
|
960
|
+
listenState: "disconnected",
|
|
666
961
|
credentialsInvalidated: invalidated,
|
|
667
962
|
};
|
|
668
963
|
}
|
|
@@ -725,6 +1020,11 @@ export class McpServerManager {
|
|
|
725
1020
|
|
|
726
1021
|
private buildClientCapabilities() {
|
|
727
1022
|
return {
|
|
1023
|
+
extensions: {
|
|
1024
|
+
"io.modelcontextprotocol/ui": {
|
|
1025
|
+
mimeTypes: [RESOURCE_MIME_TYPE],
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
728
1028
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
729
1029
|
...(this.elicitationConfig
|
|
730
1030
|
? {
|
|
@@ -929,11 +1229,26 @@ export class McpServerManager {
|
|
|
929
1229
|
this.oauthRuntime?.signal,
|
|
930
1230
|
);
|
|
931
1231
|
|
|
932
|
-
// Explicit OAuth checks secure storage immediately. Implicit OAuth
|
|
933
|
-
//
|
|
1232
|
+
// Explicit OAuth checks secure storage immediately. Implicit OAuth keeps
|
|
1233
|
+
// anonymous servers provider-free unless URL-bound credentials are already
|
|
1234
|
+
// stored, so an unavailable credential store does not break anonymous use.
|
|
1235
|
+
let implicitStoredAuth: ReturnType<typeof inspectAuthForUrl> | undefined;
|
|
1236
|
+
if (definition.auth === undefined && supportsOAuth(definition)) {
|
|
1237
|
+
try {
|
|
1238
|
+
implicitStoredAuth = inspectAuthForUrl(serverName, serverUrl, this.authStorageOptions);
|
|
1239
|
+
} catch {
|
|
1240
|
+
// Implicit preflight is opportunistic; malformed records must not block
|
|
1241
|
+
// an otherwise anonymous-capable server from connecting.
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
const hasImplicitStoredTokens = implicitStoredAuth?.status === "present"
|
|
1245
|
+
&& implicitStoredAuth.entry.tokens !== undefined;
|
|
1246
|
+
if (hasImplicitStoredTokens) invalidateAuthEntryCache(serverName);
|
|
934
1247
|
let authState: HttpAuthProviderState = supportsOAuth(definition)
|
|
935
1248
|
? definition.auth === undefined
|
|
936
|
-
?
|
|
1249
|
+
? hasImplicitStoredTokens
|
|
1250
|
+
? { status: "implicit-stored", provider: createAuthProvider() }
|
|
1251
|
+
: { status: "implicit-deferred" }
|
|
937
1252
|
: { status: "explicit", provider: createAuthProvider() }
|
|
938
1253
|
: { status: "disabled" };
|
|
939
1254
|
|
|
@@ -1076,7 +1391,7 @@ export class McpServerManager {
|
|
|
1076
1391
|
}
|
|
1077
1392
|
}
|
|
1078
1393
|
|
|
1079
|
-
private async fetchAllResources(client: Client, requestOptions?: RequestOptions): Promise<McpResource[]> {
|
|
1394
|
+
private async fetchAllResources(client: Client, requestOptions?: RequestOptions, strict = false): Promise<McpResource[]> {
|
|
1080
1395
|
const capabilities = client.getServerCapabilities?.();
|
|
1081
1396
|
if (!capabilities?.resources) return [];
|
|
1082
1397
|
|
|
@@ -1096,12 +1411,27 @@ export class McpServerManager {
|
|
|
1096
1411
|
throwIfAborted(requestOptions.signal);
|
|
1097
1412
|
}
|
|
1098
1413
|
if (isUnauthorizedHttpError(error)) throw error;
|
|
1414
|
+
if (strict) throw error;
|
|
1099
1415
|
// The server advertises resources but the listing failed
|
|
1100
1416
|
return [];
|
|
1101
1417
|
}
|
|
1102
1418
|
}
|
|
1103
1419
|
|
|
1104
1420
|
private attachAdapterNotificationHandlers(serverName: string, client: Client): void {
|
|
1421
|
+
client.setNotificationHandler("notifications/resources/updated", notification => {
|
|
1422
|
+
const uri = notification.params.uri;
|
|
1423
|
+
const connection = this.connections.get(serverName);
|
|
1424
|
+
if (!connection || connection.client !== client || connection.status !== "connected") return;
|
|
1425
|
+
for (const registration of this.resourceUpdatedListeners.values()) {
|
|
1426
|
+
if (registration.serverName === serverName && registration.uri === uri) {
|
|
1427
|
+
try {
|
|
1428
|
+
registration.listener(serverName, uri);
|
|
1429
|
+
} catch {
|
|
1430
|
+
// One UI listener must not block resource invalidation for others.
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
});
|
|
1105
1435
|
client.setNotificationHandler(
|
|
1106
1436
|
SERVER_STREAM_RESULT_PATCH_METHOD,
|
|
1107
1437
|
{ params: serverStreamResultPatchNotificationSchema.shape.params },
|
|
@@ -1134,6 +1464,7 @@ export class McpServerManager {
|
|
|
1134
1464
|
try {
|
|
1135
1465
|
this.touch(name);
|
|
1136
1466
|
this.incrementInFlight(name);
|
|
1467
|
+
await this.ensureListen(name, connection);
|
|
1137
1468
|
return await connection.client.getPrompt(
|
|
1138
1469
|
{ name: promptName, ...(args ? { arguments: args } : {}) },
|
|
1139
1470
|
this.getRequestOptions(name, signal),
|
|
@@ -1156,7 +1487,12 @@ export class McpServerManager {
|
|
|
1156
1487
|
try {
|
|
1157
1488
|
this.touch(name);
|
|
1158
1489
|
this.incrementInFlight(name);
|
|
1159
|
-
|
|
1490
|
+
const refreshRead = await this.prepareResourceUse(name, uri, connection);
|
|
1491
|
+
const requestOptions = this.getRequestOptions(name, signal);
|
|
1492
|
+
return await connection.client.readResource(
|
|
1493
|
+
{ uri },
|
|
1494
|
+
refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions,
|
|
1495
|
+
);
|
|
1160
1496
|
} finally {
|
|
1161
1497
|
this.decrementInFlight(name);
|
|
1162
1498
|
this.touch(name);
|
|
@@ -1229,6 +1565,7 @@ export class McpServerManager {
|
|
|
1229
1565
|
.flatMap(result => result.status === "rejected" ? [result.reason] : [])
|
|
1230
1566
|
.filter(error => this.containsCleanupFailure(error));
|
|
1231
1567
|
this.uiStreamListeners.clear();
|
|
1568
|
+
this.resourceUpdatedListeners.clear();
|
|
1232
1569
|
this.acceptedUrlElicitations.clear();
|
|
1233
1570
|
this.pendingMetadataPublications.clear();
|
|
1234
1571
|
this.samplingConfig = undefined;
|
|
@@ -26,6 +26,14 @@ export type McpServerRuntimeStatus =
|
|
|
26
26
|
| "not-connected"
|
|
27
27
|
| "disabled";
|
|
28
28
|
|
|
29
|
+
export type McpListenState =
|
|
30
|
+
| "active"
|
|
31
|
+
| "dropped"
|
|
32
|
+
| "re-establishing"
|
|
33
|
+
| "legacy"
|
|
34
|
+
| "not-listening"
|
|
35
|
+
| "disconnected";
|
|
36
|
+
|
|
29
37
|
export interface McpServerStatusSnapshot {
|
|
30
38
|
readonly name: string;
|
|
31
39
|
readonly status: McpServerRuntimeStatus;
|
|
@@ -33,6 +41,8 @@ export interface McpServerStatusSnapshot {
|
|
|
33
41
|
readonly resourceCount?: number;
|
|
34
42
|
readonly failedAgoSeconds?: number;
|
|
35
43
|
readonly disabled: boolean;
|
|
44
|
+
readonly listenState: McpListenState;
|
|
45
|
+
readonly catalogStale?: boolean;
|
|
36
46
|
}
|
|
37
47
|
|
|
38
48
|
export interface McpStatusSnapshot {
|
|
@@ -173,6 +183,9 @@ export type UiDisplayMode = "inline" | "fullscreen" | "pip";
|
|
|
173
183
|
export interface UiServerHandle {
|
|
174
184
|
url: string;
|
|
175
185
|
port: number;
|
|
186
|
+
/** URL of the second-origin MCP Apps sandbox proxy. */
|
|
187
|
+
proxyUrl: string;
|
|
188
|
+
proxyPort: number;
|
|
176
189
|
sessionToken: string;
|
|
177
190
|
serverName: string;
|
|
178
191
|
toolName: string;
|
|
@@ -183,6 +196,7 @@ export interface UiServerHandle {
|
|
|
183
196
|
sendToolResult: (result: CallToolResult) => void;
|
|
184
197
|
sendResultPatch: (result: CallToolResult) => void;
|
|
185
198
|
sendToolCancelled: (reason: string) => void;
|
|
199
|
+
sendResourceUpdated: (uri: string) => void;
|
|
186
200
|
sendHostContext: (context: UiHostContext) => void;
|
|
187
201
|
/** Get accumulated messages from this session */
|
|
188
202
|
getSessionMessages: () => UiSessionMessages;
|
|
@@ -378,7 +392,7 @@ export interface OAuthConfig {
|
|
|
378
392
|
scope?: string;
|
|
379
393
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
380
394
|
authorizationParams?: Record<string, string>;
|
|
381
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
395
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
382
396
|
redirectUri?: string;
|
|
383
397
|
/** Client display name for dynamic registration */
|
|
384
398
|
clientName?: string;
|
|
@@ -564,9 +578,8 @@ export interface McpSettings {
|
|
|
564
578
|
approveTools?: boolean | string[];
|
|
565
579
|
disableProxyTool?: boolean;
|
|
566
580
|
/** Freeze direct-tool registration after the initial sync. Automatic metadata updates
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
* mcp({ connect: "server" }). Default: false. */
|
|
581
|
+
* and explicit reconnects won't rebuild the system prompt, preserving the
|
|
582
|
+
* prompt-cache prefix. Proxy/search/cache metadata still refreshes. Default: false. */
|
|
570
583
|
freezeDirectTools?: boolean;
|
|
571
584
|
autoAuth?: boolean;
|
|
572
585
|
sampling?: boolean;
|
|
@@ -710,6 +723,8 @@ export interface McpPanelCallbacks {
|
|
|
710
723
|
|
|
711
724
|
export interface McpPanelResult {
|
|
712
725
|
changes: Map<string, true | string[] | false>;
|
|
726
|
+
/** Servers whose disabled flag changed during the panel session (name → new disabled state). */
|
|
727
|
+
disabledChanges: Map<string, boolean>;
|
|
713
728
|
cancelled: boolean;
|
|
714
729
|
}
|
|
715
730
|
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
2
2
|
import { UrlElicitationRequiredError, type ReadResourceResult } from "@modelcontextprotocol/client";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
getInputRequiredNeedsUiDetails,
|
|
5
|
+
InputRequiredNeedsUiError,
|
|
6
|
+
ResourceFetchError,
|
|
7
|
+
ResourceParseError,
|
|
8
|
+
} from "./errors.ts";
|
|
4
9
|
import { logger } from "./logger.ts";
|
|
5
10
|
import { SessionRecoveryAuthRequiredError, withSessionRecovery, type SessionRecoveryDeps } from "./session-recovery.ts";
|
|
6
11
|
import type { McpServerManager } from "./server-manager.ts";
|
|
@@ -52,7 +57,14 @@ export class UiResourceHandler {
|
|
|
52
57
|
...(options.onNeedsAuth ? { onNeedsAuth: options.onNeedsAuth } : {}),
|
|
53
58
|
},
|
|
54
59
|
serverName,
|
|
55
|
-
(connection) =>
|
|
60
|
+
async (connection) => {
|
|
61
|
+
const refreshRead = await this.manager.prepareResourceUse?.(serverName, uri, connection);
|
|
62
|
+
const requestOptions = this.manager.getRequestOptions(serverName, options.signal);
|
|
63
|
+
return connection.client.readResource(
|
|
64
|
+
{ uri },
|
|
65
|
+
refreshRead ? { ...requestOptions, cacheMode: "refresh" } : requestOptions,
|
|
66
|
+
);
|
|
67
|
+
},
|
|
56
68
|
);
|
|
57
69
|
} finally {
|
|
58
70
|
this.manager.decrementInFlight(serverName);
|
|
@@ -63,6 +75,10 @@ export class UiResourceHandler {
|
|
|
63
75
|
}
|
|
64
76
|
} catch (error) {
|
|
65
77
|
if (error instanceof UrlElicitationRequiredError || error instanceof SessionRecoveryAuthRequiredError) throw error;
|
|
78
|
+
const inputRequired = getInputRequiredNeedsUiDetails(error, { server: serverName, resourceUri: uri });
|
|
79
|
+
if (inputRequired) {
|
|
80
|
+
throw new InputRequiredNeedsUiError(inputRequired, error instanceof Error ? error : undefined);
|
|
81
|
+
}
|
|
66
82
|
const message = error instanceof Error ? error.message : String(error);
|
|
67
83
|
log.error("Failed to read resource", error instanceof Error ? error : undefined);
|
|
68
84
|
throw new ResourceFetchError(uri, message, {
|