@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- 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/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- 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 +106 -17
- 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/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- 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 +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- 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
|
@@ -75,6 +75,8 @@ type PendingAuth = {
|
|
|
75
75
|
authProvider: McpOAuthProvider
|
|
76
76
|
serverUrl: string
|
|
77
77
|
authorizationUrl: string
|
|
78
|
+
manualRedirect: boolean
|
|
79
|
+
manualCompletionController?: AbortController
|
|
78
80
|
discovery: AuthDiscovery
|
|
79
81
|
authStorageOptions: AuthStorageOptions
|
|
80
82
|
}
|
|
@@ -238,6 +240,25 @@ export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
|
|
|
238
240
|
}
|
|
239
241
|
config.logoUri = logoUri
|
|
240
242
|
}
|
|
243
|
+
if (definition.oauth?.authServerMetadataUrl !== undefined) {
|
|
244
|
+
if (typeof definition.oauth.authServerMetadataUrl !== "string") {
|
|
245
|
+
throw new Error("OAuth authServerMetadataUrl must be a string")
|
|
246
|
+
}
|
|
247
|
+
const authServerMetadataUrl = interpolateEnvVars(definition.oauth.authServerMetadataUrl).trim()
|
|
248
|
+
if (!authServerMetadataUrl) {
|
|
249
|
+
throw new Error("OAuth authServerMetadataUrl must not be empty")
|
|
250
|
+
}
|
|
251
|
+
let parsed: URL
|
|
252
|
+
try {
|
|
253
|
+
parsed = new URL(authServerMetadataUrl)
|
|
254
|
+
} catch {
|
|
255
|
+
throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
|
|
256
|
+
}
|
|
257
|
+
if (parsed.protocol !== "https:") {
|
|
258
|
+
throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
|
|
259
|
+
}
|
|
260
|
+
config.authServerMetadataUrl = authServerMetadataUrl
|
|
261
|
+
}
|
|
241
262
|
if (definition.oauth?.skipIssuerMetadataValidation !== undefined) {
|
|
242
263
|
if (typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
|
|
243
264
|
throw new Error("OAuth skipIssuerMetadataValidation must be a boolean")
|
|
@@ -288,7 +309,11 @@ async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, s
|
|
|
288
309
|
}
|
|
289
310
|
}
|
|
290
311
|
|
|
291
|
-
|
|
312
|
+
type OAuthRedirectTarget =
|
|
313
|
+
| { mode: "local"; port: number; callbackHost: string; callbackPath: string }
|
|
314
|
+
| { mode: "manual" }
|
|
315
|
+
|
|
316
|
+
function parseOAuthRedirectUri(redirectUri: string): OAuthRedirectTarget {
|
|
292
317
|
let url: URL
|
|
293
318
|
try {
|
|
294
319
|
url = new URL(redirectUri)
|
|
@@ -296,12 +321,6 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
296
321
|
throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
|
|
297
322
|
}
|
|
298
323
|
|
|
299
|
-
const hostname = url.hostname.toLowerCase()
|
|
300
|
-
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
301
|
-
if (url.protocol !== "http:" || !isLocalhost) {
|
|
302
|
-
throw new Error("OAuth redirectUri must be an http:// localhost or loopback URI")
|
|
303
|
-
}
|
|
304
|
-
|
|
305
324
|
if (url.username || url.password) {
|
|
306
325
|
throw new Error("OAuth redirectUri must not include username or password")
|
|
307
326
|
}
|
|
@@ -310,17 +329,32 @@ function parseOAuthRedirectUri(redirectUri: string): { port: number; callbackHos
|
|
|
310
329
|
throw new Error("OAuth redirectUri must not include a fragment")
|
|
311
330
|
}
|
|
312
331
|
|
|
332
|
+
const hostname = url.hostname.toLowerCase()
|
|
333
|
+
const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
|
|
334
|
+
if (url.port) {
|
|
335
|
+
const parsedPort = Number.parseInt(url.port, 10)
|
|
336
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
|
|
337
|
+
throw new Error("OAuth redirectUri port must be a positive numeric port")
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (url.protocol === "https:" && !isLocalhost) {
|
|
341
|
+
return { mode: "manual" }
|
|
342
|
+
}
|
|
343
|
+
if (url.protocol !== "http:" || !isLocalhost) {
|
|
344
|
+
throw new Error("OAuth redirectUri must be an https:// URI or an http:// localhost or loopback URI")
|
|
345
|
+
}
|
|
346
|
+
|
|
313
347
|
if (!url.port) {
|
|
314
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
348
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
315
349
|
}
|
|
316
350
|
|
|
317
351
|
const port = Number.parseInt(url.port, 10)
|
|
318
352
|
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
319
|
-
throw new Error("OAuth redirectUri must include an explicit numeric port")
|
|
353
|
+
throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
|
|
320
354
|
}
|
|
321
355
|
|
|
322
356
|
const callbackHost = hostname === "[::1]" ? "::1" : hostname
|
|
323
|
-
return { port, callbackHost, callbackPath: url.pathname }
|
|
357
|
+
return { mode: "local", port, callbackHost, callbackPath: url.pathname }
|
|
324
358
|
}
|
|
325
359
|
|
|
326
360
|
/**
|
|
@@ -374,25 +408,30 @@ export async function startAuth(
|
|
|
374
408
|
return { authorizationUrl: existingPendingAuth.authorizationUrl }
|
|
375
409
|
}
|
|
376
410
|
|
|
377
|
-
const
|
|
411
|
+
const redirectTarget = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
|
|
412
|
+
const manualRedirect = redirectTarget?.mode === "manual"
|
|
378
413
|
const oauthState = generateState()
|
|
379
414
|
|
|
380
|
-
|
|
381
|
-
await ensureCallbackServer({
|
|
382
|
-
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
383
|
-
oauthState,
|
|
384
|
-
reserveState: true,
|
|
385
|
-
...(redirectCallback ? { port: redirectCallback.port, callbackHost: redirectCallback.callbackHost, callbackPath: redirectCallback.callbackPath } : {}),
|
|
386
|
-
})
|
|
387
|
-
throwIfAborted(signal)
|
|
388
|
-
} catch (error) {
|
|
389
|
-
releaseCallbackServer(oauthState)
|
|
415
|
+
if (!manualRedirect) {
|
|
390
416
|
try {
|
|
391
|
-
await
|
|
392
|
-
|
|
393
|
-
|
|
417
|
+
await ensureCallbackServer({
|
|
418
|
+
strictPort: Boolean(config.clientId) || config.redirectUri !== undefined,
|
|
419
|
+
oauthState,
|
|
420
|
+
reserveState: true,
|
|
421
|
+
...(redirectTarget?.mode === "local"
|
|
422
|
+
? { port: redirectTarget.port, callbackHost: redirectTarget.callbackHost, callbackPath: redirectTarget.callbackPath }
|
|
423
|
+
: {}),
|
|
424
|
+
})
|
|
425
|
+
throwIfAborted(signal)
|
|
426
|
+
} catch (error) {
|
|
427
|
+
releaseCallbackServer(oauthState)
|
|
428
|
+
try {
|
|
429
|
+
await cleanupAndReleaseCallbackServerIfIdle(() => clearOAuthState(serverName, authStorageOptions))
|
|
430
|
+
} catch (cleanupError) {
|
|
431
|
+
throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
|
|
432
|
+
}
|
|
433
|
+
throw error
|
|
394
434
|
}
|
|
395
|
-
throw error
|
|
396
435
|
}
|
|
397
436
|
|
|
398
437
|
let capturedUrl: URL | undefined
|
|
@@ -436,7 +475,16 @@ export async function startAuth(
|
|
|
436
475
|
if (!capturedUrl) {
|
|
437
476
|
throw new UnauthorizedError("OAuth authorization URL was not provided")
|
|
438
477
|
}
|
|
439
|
-
await setPendingAuth(runtime, serverName, {
|
|
478
|
+
await setPendingAuth(runtime, serverName, {
|
|
479
|
+
serverName,
|
|
480
|
+
authProvider,
|
|
481
|
+
serverUrl,
|
|
482
|
+
authorizationUrl: capturedUrl.toString(),
|
|
483
|
+
manualRedirect,
|
|
484
|
+
...(manualRedirect ? { manualCompletionController: new AbortController() } : {}),
|
|
485
|
+
discovery,
|
|
486
|
+
authStorageOptions,
|
|
487
|
+
}, oauthState, signal, generation)
|
|
440
488
|
return { authorizationUrl: capturedUrl.toString() }
|
|
441
489
|
} catch (error) {
|
|
442
490
|
authProvider.deactivate()
|
|
@@ -465,7 +513,13 @@ async function setPendingAuth(
|
|
|
465
513
|
state.pendingAuths.set(key, pendingAuth)
|
|
466
514
|
state.pendingAuthStates.set(key, oauthState)
|
|
467
515
|
const cleanupTimer = setTimeout(() => {
|
|
468
|
-
void clearPendingAuthAndReleaseIfIdle(
|
|
516
|
+
void clearPendingAuthAndReleaseIfIdle(
|
|
517
|
+
runtime,
|
|
518
|
+
serverName,
|
|
519
|
+
oauthState,
|
|
520
|
+
pendingAuth.authStorageOptions,
|
|
521
|
+
new Error("OAuth authorization timeout - authorization took too long"),
|
|
522
|
+
).catch(error => {
|
|
469
523
|
console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
|
|
470
524
|
})
|
|
471
525
|
}, MANUAL_AUTH_TIMEOUT_MS)
|
|
@@ -473,7 +527,13 @@ async function setPendingAuth(
|
|
|
473
527
|
state.pendingAuthCleanupTimers.set(key, cleanupTimer)
|
|
474
528
|
}
|
|
475
529
|
|
|
476
|
-
async function clearPendingAuth(
|
|
530
|
+
async function clearPendingAuth(
|
|
531
|
+
runtime: McpOAuthRuntime,
|
|
532
|
+
serverName: string,
|
|
533
|
+
oauthState?: string,
|
|
534
|
+
fallbackStorageOptions: AuthStorageOptions = {},
|
|
535
|
+
reason: Error = new Error("Authorization cancelled"),
|
|
536
|
+
): Promise<void> {
|
|
477
537
|
const state = getRuntimeState(runtime)
|
|
478
538
|
const key = getPendingAuthKey(serverName, fallbackStorageOptions)
|
|
479
539
|
const pendingAuth = state.pendingAuths.get(key)
|
|
@@ -487,6 +547,7 @@ async function clearPendingAuth(runtime: McpOAuthRuntime, serverName: string, oa
|
|
|
487
547
|
state.pendingAuthCleanupTimers.delete(key)
|
|
488
548
|
}
|
|
489
549
|
|
|
550
|
+
pendingAuth?.manualCompletionController?.abort(reason)
|
|
490
551
|
pendingAuth?.authProvider.deactivate()
|
|
491
552
|
state.pendingAuths.delete(key)
|
|
492
553
|
state.pendingAuthStates.delete(key)
|
|
@@ -505,9 +566,10 @@ async function clearPendingAuthAndReleaseIfIdle(
|
|
|
505
566
|
serverName: string,
|
|
506
567
|
oauthState: string | undefined,
|
|
507
568
|
fallbackStorageOptions: AuthStorageOptions = {},
|
|
569
|
+
reason?: Error,
|
|
508
570
|
): Promise<void> {
|
|
509
571
|
await cleanupAndReleaseCallbackServerIfIdle(
|
|
510
|
-
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions),
|
|
572
|
+
() => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions, reason),
|
|
511
573
|
)
|
|
512
574
|
}
|
|
513
575
|
|
|
@@ -610,6 +672,19 @@ type AuthorizationResponse = {
|
|
|
610
672
|
source: "callback" | "manual"
|
|
611
673
|
}
|
|
612
674
|
|
|
675
|
+
function waitForManualCompletionCancellation(signal: AbortSignal): Promise<AuthorizationCodeInput> {
|
|
676
|
+
return new Promise((_, reject) => {
|
|
677
|
+
const rejectFromSignal = () => {
|
|
678
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Authorization cancelled"))
|
|
679
|
+
}
|
|
680
|
+
if (signal.aborted) {
|
|
681
|
+
rejectFromSignal()
|
|
682
|
+
return
|
|
683
|
+
}
|
|
684
|
+
signal.addEventListener("abort", rejectFromSignal, { once: true })
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
|
|
613
688
|
/**
|
|
614
689
|
* Wait for either the localhost callback or a manually pasted redirect URL.
|
|
615
690
|
* The manual input prompt is dismissed as soon as either path finishes.
|
|
@@ -667,7 +742,11 @@ export async function completeAuthFromInput(
|
|
|
667
742
|
throwIfAborted(signal)
|
|
668
743
|
const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
|
|
669
744
|
const oauthState = runtimeState.pendingAuthStates.get(key)
|
|
745
|
+
const pendingAuth = runtimeState.pendingAuths.get(key)
|
|
670
746
|
throwIfAborted(signal)
|
|
747
|
+
if (pendingAuth?.manualRedirect && !getSearchParamsFromInput(input.trim())) {
|
|
748
|
+
throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
|
|
749
|
+
}
|
|
671
750
|
const parsed = parseAuthorizationRedirectInput(input, oauthState)
|
|
672
751
|
return completeAuth(serverName, parsed, options)
|
|
673
752
|
}
|
|
@@ -786,15 +865,28 @@ export async function authenticate(
|
|
|
786
865
|
try {
|
|
787
866
|
// Get the state that was already generated and stored in startAuth().
|
|
788
867
|
// Keep this lookup and its abort check inside the cleanup boundary because
|
|
789
|
-
// startAuth
|
|
868
|
+
// startAuth already owns the pending flow at this point.
|
|
790
869
|
oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
791
870
|
throwIfAborted(signal)
|
|
792
871
|
if (!oauthState) {
|
|
793
872
|
throw new Error("OAuth state not found - this should not happen")
|
|
794
873
|
}
|
|
795
874
|
|
|
796
|
-
|
|
797
|
-
|
|
875
|
+
const pendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
|
|
876
|
+
if (!pendingAuth) {
|
|
877
|
+
throw new Error(`No pending OAuth flow for server: ${serverName}`)
|
|
878
|
+
}
|
|
879
|
+
if (pendingAuth.manualRedirect && !options.onAuthorizationInput) {
|
|
880
|
+
throw new Error(
|
|
881
|
+
`OAuth for ${serverName} uses a remote redirect URI. Complete it with auth-start/auth-complete or /mcp-auth.`,
|
|
882
|
+
)
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Register the localhost callback before opening the browser. Remote
|
|
886
|
+
// pre-registered callbacks are completed by pasting their full URL.
|
|
887
|
+
const callbackPromise: Promise<AuthorizationCodeInput> = pendingAuth.manualRedirect
|
|
888
|
+
? waitForManualCompletionCancellation(pendingAuth.manualCompletionController!.signal)
|
|
889
|
+
: waitForCallback(oauthState)
|
|
798
890
|
void callbackPromise.catch(() => {})
|
|
799
891
|
|
|
800
892
|
// Open browser. Always surface the URL first so remote/headless users can copy it
|
|
@@ -43,8 +43,6 @@ export class BearerCredentialStoreError extends Error {
|
|
|
43
43
|
readonly code = "BEARER_CREDENTIAL_STORE_UNAVAILABLE";
|
|
44
44
|
readonly operation: "read" | "write" | "remove";
|
|
45
45
|
|
|
46
|
-
// No parameter properties: cli.js loads this module under plain Node
|
|
47
|
-
// type stripping, which rejects non-erasable TypeScript syntax.
|
|
48
46
|
constructor(message: string, operation: "read" | "write" | "remove", cause: unknown) {
|
|
49
47
|
super(message, { cause });
|
|
50
48
|
this.name = "BearerCredentialStoreError";
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
type StoredTokens,
|
|
30
30
|
type StoredClientInfo,
|
|
31
31
|
} from "./mcp-auth.ts"
|
|
32
|
+
import { OAuthMetadataSchema, OpenIdProviderDiscoveryMetadataSchema } from "@modelcontextprotocol/core"
|
|
32
33
|
import { resolveCommandSecret } from "./utils.ts"
|
|
33
34
|
import { getAppClientUri, getAppName } from "./agent-dir.ts"
|
|
34
35
|
|
|
@@ -130,6 +131,7 @@ export interface McpOAuthConfig {
|
|
|
130
131
|
clientName?: string
|
|
131
132
|
clientUri?: string
|
|
132
133
|
logoUri?: string
|
|
134
|
+
authServerMetadataUrl?: string
|
|
133
135
|
skipIssuerMetadataValidation?: boolean
|
|
134
136
|
}
|
|
135
137
|
|
|
@@ -161,6 +163,85 @@ export interface McpOAuthCallbacks {
|
|
|
161
163
|
onRedirect: (url: URL) => void | Promise<void>
|
|
162
164
|
}
|
|
163
165
|
|
|
166
|
+
function inferIssuerFromMetadataUrl(metadataUrl: string): string | undefined {
|
|
167
|
+
const url = new URL(metadataUrl)
|
|
168
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/"
|
|
169
|
+
const oauthPrefix = "/.well-known/oauth-authorization-server"
|
|
170
|
+
if (pathname === oauthPrefix || pathname.startsWith(`${oauthPrefix}/`)) {
|
|
171
|
+
const issuerPath = pathname.slice(oauthPrefix.length) || "/"
|
|
172
|
+
return new URL(issuerPath, url.origin).toString()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const oidcPath = "/.well-known/openid-configuration"
|
|
176
|
+
if (pathname === oidcPath || pathname.startsWith(`${oidcPath}/`)) {
|
|
177
|
+
const issuerPath = pathname.slice(oidcPath.length) || "/"
|
|
178
|
+
return new URL(issuerPath, url.origin).toString()
|
|
179
|
+
}
|
|
180
|
+
if (pathname.endsWith(oidcPath)) {
|
|
181
|
+
const issuerPath = pathname.slice(0, -oidcPath.length) || "/"
|
|
182
|
+
return new URL(issuerPath, url.origin).toString()
|
|
183
|
+
}
|
|
184
|
+
return undefined
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function validateConfiguredIssuer(metadataUrl: string, issuer: string, skipIssuerValidation: boolean): void {
|
|
188
|
+
let parsedIssuer: URL
|
|
189
|
+
try {
|
|
190
|
+
parsedIssuer = new URL(issuer)
|
|
191
|
+
} catch (error) {
|
|
192
|
+
throw new Error("OAuth authorization-server metadata issuer must be an absolute URL", { cause: error })
|
|
193
|
+
}
|
|
194
|
+
if (parsedIssuer.protocol !== "http:" && parsedIssuer.protocol !== "https:") {
|
|
195
|
+
throw new Error("OAuth authorization-server metadata issuer must use http:// or https://")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const expectedIssuer = inferIssuerFromMetadataUrl(metadataUrl)
|
|
199
|
+
if (skipIssuerValidation) return
|
|
200
|
+
if (expectedIssuer !== undefined && !issuersMatch(expectedIssuer, issuer)) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`OAuth authorization-server metadata issuer does not match authServerMetadataUrl: expected ${expectedIssuer}`,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
if (expectedIssuer === undefined && !issuersMatch(new URL(metadataUrl).origin, issuer)) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`OAuth authorization-server metadata issuer does not match authServerMetadataUrl origin: expected ${new URL(metadataUrl).origin}`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function loadConfiguredDiscoveryState(
|
|
213
|
+
metadataUrl: string,
|
|
214
|
+
serverUrl: string,
|
|
215
|
+
skipIssuerValidation: boolean,
|
|
216
|
+
signal?: AbortSignal,
|
|
217
|
+
): Promise<OAuthDiscoveryState> {
|
|
218
|
+
const response = await fetch(metadataUrl, {
|
|
219
|
+
headers: { accept: "application/json" },
|
|
220
|
+
...(signal !== undefined ? { signal } : {}),
|
|
221
|
+
})
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
await response.text().catch(() => {})
|
|
224
|
+
throw new Error(`OAuth authServerMetadataUrl request failed with HTTP ${response.status}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const payload = await response.json()
|
|
228
|
+
const oauthResult = OAuthMetadataSchema.safeParse(payload)
|
|
229
|
+
const metadata = oauthResult.success
|
|
230
|
+
? oauthResult.data
|
|
231
|
+
: OpenIdProviderDiscoveryMetadataSchema.parse(payload)
|
|
232
|
+
validateConfiguredIssuer(metadataUrl, metadata.issuer, skipIssuerValidation)
|
|
233
|
+
|
|
234
|
+
const resource = new URL(serverUrl)
|
|
235
|
+
resource.hash = ""
|
|
236
|
+
return {
|
|
237
|
+
authorizationServerUrl: metadata.issuer,
|
|
238
|
+
authorizationServerMetadata: metadata,
|
|
239
|
+
// The configured AS document is authoritative, so avoid a second PRM
|
|
240
|
+
// lookup while still binding the token request to the configured resource.
|
|
241
|
+
resourceMetadata: { resource: resource.toString() },
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
164
245
|
/**
|
|
165
246
|
* OAuth provider implementation for MCP servers.
|
|
166
247
|
* Implements the OAuthClientProvider interface from the MCP SDK.
|
|
@@ -525,6 +606,14 @@ export class McpOAuthProvider implements OAuthClientProvider {
|
|
|
525
606
|
|
|
526
607
|
async discoveryState(): Promise<OAuthDiscoveryState | undefined> {
|
|
527
608
|
this.throwIfInactive()
|
|
609
|
+
if (!this.flowDiscoveryState && this.config.authServerMetadataUrl !== undefined) {
|
|
610
|
+
this.flowDiscoveryState = await loadConfiguredDiscoveryState(
|
|
611
|
+
this.config.authServerMetadataUrl,
|
|
612
|
+
this.serverUrl,
|
|
613
|
+
this.config.skipIssuerMetadataValidation === true,
|
|
614
|
+
this.runtimeSignal,
|
|
615
|
+
)
|
|
616
|
+
}
|
|
528
617
|
return this.flowDiscoveryState ? structuredClone(this.flowDiscoveryState) : undefined
|
|
529
618
|
}
|
|
530
619
|
|
|
@@ -32,9 +32,17 @@ type DirectNameEntry = { name: string; originalName: string };
|
|
|
32
32
|
type CachedServer = { serverName: string; definition: ServerEntry; entry: ServerCacheEntry; prefix: ToolPrefix };
|
|
33
33
|
|
|
34
34
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
35
|
+
const ENCODED_NAMESPACE_MARKER = "_mcpns_";
|
|
36
|
+
|
|
37
|
+
function namespaceServerPart(serverName: string): string {
|
|
38
|
+
const normalized = serverName.replace(/-/g, "_");
|
|
39
|
+
if (normalized === "" || (/^[A-Za-z0-9_]+$/.test(normalized) && !normalized.startsWith(ENCODED_NAMESPACE_MARKER))) return normalized;
|
|
40
|
+
const codePoints = Array.from(normalized, char => char.codePointAt(0)!.toString(16)).join("_");
|
|
41
|
+
return `${ENCODED_NAMESPACE_MARKER}${codePoints}`;
|
|
42
|
+
}
|
|
35
43
|
|
|
36
44
|
export function namespaceProxyName(serverName: string): string {
|
|
37
|
-
return `mcp__${serverName
|
|
45
|
+
return `mcp__${namespaceServerPart(serverName)}`;
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
export function parseMcpReference(raw: string): ParsedMcpReference {
|
|
@@ -93,6 +93,24 @@ function getSingleToolMatch(metadata: ToolMetadata[] | undefined, toolName: stri
|
|
|
93
93
|
return matches.length > 1 ? "ambiguous" : matches[0];
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
type ServerScopedToolMatch = { tool: ToolMetadata; precedence: number } | "ambiguous";
|
|
97
|
+
|
|
98
|
+
function getServerScopedToolMatch(metadata: ToolMetadata[] | undefined, toolName: string): ServerScopedToolMatch | undefined {
|
|
99
|
+
if (!metadata) return undefined;
|
|
100
|
+
const normalizedName = toolName.replace(/-/g, "_");
|
|
101
|
+
const matchesByPrecedence = [
|
|
102
|
+
metadata.filter((tool) => tool.name === toolName),
|
|
103
|
+
metadata.filter((tool) => tool.originalName === toolName),
|
|
104
|
+
metadata.filter((tool) => tool.name.replace(/-/g, "_") === normalizedName),
|
|
105
|
+
metadata.filter((tool) => tool.originalName.replace(/-/g, "_") === normalizedName),
|
|
106
|
+
];
|
|
107
|
+
for (const [precedence, matches] of matchesByPrecedence.entries()) {
|
|
108
|
+
if (matches.length > 1) return "ambiguous";
|
|
109
|
+
if (matches.length === 1) return { tool: matches[0]!, precedence };
|
|
110
|
+
}
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
function ambiguousToolResult(mode: "call" | "describe", toolName: string): ProxyToolResult {
|
|
97
115
|
const message = `Tool "${toolName}" matches multiple servers. Specify a server.`;
|
|
98
116
|
return {
|
|
@@ -125,22 +143,30 @@ function getAuthFailedMessage(state: McpExtensionState, serverName: string, mess
|
|
|
125
143
|
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
126
144
|
}
|
|
127
145
|
|
|
128
|
-
function
|
|
146
|
+
function getRedirectDetails(authorizationUrl: string): { port?: number; remote: boolean } {
|
|
129
147
|
try {
|
|
130
148
|
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
131
|
-
if (!redirectUri) return
|
|
132
|
-
const
|
|
133
|
-
|
|
149
|
+
if (!redirectUri) return { remote: false };
|
|
150
|
+
const redirect = new URL(redirectUri);
|
|
151
|
+
const hostname = redirect.hostname.toLowerCase();
|
|
152
|
+
const local = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1";
|
|
153
|
+
const port = Number.parseInt(redirect.port, 10);
|
|
154
|
+
return {
|
|
155
|
+
...(Number.isInteger(port) ? { port } : {}),
|
|
156
|
+
remote: !local,
|
|
157
|
+
};
|
|
134
158
|
} catch {
|
|
135
|
-
return
|
|
159
|
+
return { remote: false };
|
|
136
160
|
}
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
function formatManualAuthInstructions(serverName: string, authorizationUrl: string): string {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
?
|
|
143
|
-
:
|
|
164
|
+
const redirect = getRedirectDetails(authorizationUrl);
|
|
165
|
+
const redirectNote = redirect.remote
|
|
166
|
+
? "The provider uses a pre-registered HTTPS callback. Copy its full URL from the browser address bar, even if the destination page reports an error."
|
|
167
|
+
: redirect.port
|
|
168
|
+
? `The redirect URL will use local port ${redirect.port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
|
|
169
|
+
: "";
|
|
144
170
|
|
|
145
171
|
return [
|
|
146
172
|
`MCP OAuth required for "${serverName}".`,
|
|
@@ -149,11 +175,13 @@ function formatManualAuthInstructions(serverName: string, authorizationUrl: stri
|
|
|
149
175
|
"",
|
|
150
176
|
authorizationUrl,
|
|
151
177
|
"",
|
|
152
|
-
"After approving, copy the full
|
|
178
|
+
"After approving, copy the full callback URL from your browser address bar and send it back with:",
|
|
153
179
|
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
154
180
|
"",
|
|
155
|
-
|
|
156
|
-
|
|
181
|
+
redirect.remote
|
|
182
|
+
? "Remote HTTPS callbacks must include the full callback URL so the OAuth state can be checked. JSON-string args remain supported."
|
|
183
|
+
: 'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
|
|
184
|
+
redirectNote,
|
|
157
185
|
].filter(Boolean).join("\n");
|
|
158
186
|
}
|
|
159
187
|
|
|
@@ -894,9 +922,9 @@ export async function executeCall(
|
|
|
894
922
|
};
|
|
895
923
|
}
|
|
896
924
|
if (serverName) {
|
|
897
|
-
const match =
|
|
925
|
+
const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
898
926
|
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
899
|
-
toolMeta = match;
|
|
927
|
+
toolMeta = match?.tool;
|
|
900
928
|
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
901
929
|
return disabledCallResult(serverName, toolMeta);
|
|
902
930
|
}
|
|
@@ -938,9 +966,15 @@ export async function executeCall(
|
|
|
938
966
|
if (serverName && !toolMeta) {
|
|
939
967
|
const connected = await lazyConnect(state, serverName, ownedSignal);
|
|
940
968
|
if (connected) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
969
|
+
if (serverOverride) {
|
|
970
|
+
const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
971
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
972
|
+
toolMeta = match?.tool;
|
|
973
|
+
} else {
|
|
974
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
975
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
976
|
+
toolMeta = match;
|
|
977
|
+
}
|
|
944
978
|
} else {
|
|
945
979
|
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
946
980
|
if (needsAuthConnection?.status === "needs-auth") {
|
|
@@ -958,9 +992,9 @@ export async function executeCall(
|
|
|
958
992
|
clearFailure(state, serverName);
|
|
959
993
|
const connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
|
|
960
994
|
if (connectedAfterAuth) {
|
|
961
|
-
const match =
|
|
995
|
+
const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
962
996
|
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
963
|
-
toolMeta = match;
|
|
997
|
+
toolMeta = match?.tool;
|
|
964
998
|
if (!toolMeta) {
|
|
965
999
|
const suggestions = rankSuggestions(state, toolName, 5);
|
|
966
1000
|
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
@@ -1157,9 +1191,15 @@ export async function executeCall(
|
|
|
1157
1191
|
if (!restored) notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
|
|
1158
1192
|
markKeepAliveAfterConnect(state, serverName);
|
|
1159
1193
|
updateStatusBar(state);
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1194
|
+
if (serverOverride) {
|
|
1195
|
+
const match = getServerScopedToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
1196
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
1197
|
+
toolMeta = match?.tool;
|
|
1198
|
+
} else {
|
|
1199
|
+
const match = getSingleToolMatch(state.toolMetadata.get(serverName), toolName);
|
|
1200
|
+
if (match === "ambiguous") return ambiguousToolResult("call", toolName);
|
|
1201
|
+
toolMeta = match;
|
|
1202
|
+
}
|
|
1163
1203
|
if (!toolMeta) {
|
|
1164
1204
|
const available = getToolNames(state, serverName);
|
|
1165
1205
|
const hint = available.length > 0
|
|
@@ -92,7 +92,7 @@ function killRequestHeadersCommand(child: ChildProcess, trackedPosixDescendantPi
|
|
|
92
92
|
encoding: "utf8",
|
|
93
93
|
windowsHide: true,
|
|
94
94
|
});
|
|
95
|
-
if (result.status === 0 || isTaskkillNoSuchProcess(result)) return;
|
|
95
|
+
if (result.status === 0 || result.status === 128 || isTaskkillNoSuchProcess(result)) return;
|
|
96
96
|
throw new Error(`HTTP request headers command cleanup failed: taskkill exited with code ${result.status ?? "unknown"}`);
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
import { resolveNpxBinary } from "./npx-resolver.ts";
|
|
35
35
|
import { createJsonSchemaValidator } from "./json-schema-validator.ts";
|
|
36
36
|
import { logger } from "./logger.ts";
|
|
37
|
+
import { RESOURCE_MIME_TYPE } from "./ui-app-bridge-helpers.ts";
|
|
37
38
|
import { McpOAuthProvider } from "./mcp-oauth-provider.ts";
|
|
38
39
|
import { extractOAuthConfig, supportsOAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
39
40
|
import { invalidateAuthEntryCache, type AuthStorageOptions } from "./mcp-auth.ts";
|
|
@@ -725,6 +726,11 @@ export class McpServerManager {
|
|
|
725
726
|
|
|
726
727
|
private buildClientCapabilities() {
|
|
727
728
|
return {
|
|
729
|
+
extensions: {
|
|
730
|
+
"io.modelcontextprotocol/ui": {
|
|
731
|
+
mimeTypes: [RESOURCE_MIME_TYPE],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
728
734
|
...(this.samplingConfig ? { sampling: {} } : {}),
|
|
729
735
|
...(this.elicitationConfig
|
|
730
736
|
? {
|
|
@@ -378,7 +378,7 @@ export interface OAuthConfig {
|
|
|
378
378
|
scope?: string;
|
|
379
379
|
/** Extra authorization URL parameters for provider-specific extensions. Flow-owned parameters cannot be overridden. */
|
|
380
380
|
authorizationParams?: Record<string, string>;
|
|
381
|
-
/** Exact authorization-code redirect URI for pre-registered clients */
|
|
381
|
+
/** Exact authorization-code redirect URI for pre-registered clients. HTTPS redirects use manual callback URL completion. */
|
|
382
382
|
redirectUri?: string;
|
|
383
383
|
/** Client display name for dynamic registration */
|
|
384
384
|
clientName?: string;
|
|
@@ -386,6 +386,8 @@ export interface OAuthConfig {
|
|
|
386
386
|
clientUri?: string;
|
|
387
387
|
/** Client logo URL for dynamic registration; shown on consent screens */
|
|
388
388
|
logoUri?: string;
|
|
389
|
+
/** HTTPS URL for an authorization-server metadata document used instead of MCP discovery */
|
|
390
|
+
authServerMetadataUrl?: string;
|
|
389
391
|
/** Security-weakening escape hatch for known-misconfigured authorization servers. */
|
|
390
392
|
skipIssuerMetadataValidation?: boolean;
|
|
391
393
|
}
|
|
@@ -4,6 +4,48 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.27.0] - 2026-08-28
|
|
8
|
+
|
|
9
|
+
### Highlights
|
|
10
|
+
|
|
11
|
+
- `fetch_content` now lets you tune direct HTTP and Jina Reader timeouts from config.
|
|
12
|
+
- Answer mode can use a configured default model while still allowing per-call overrides.
|
|
13
|
+
- HTML fallback parsing is quieter for pages with relative canonical links.
|
|
14
|
+
- GitHub repository fetching is safer when more than one Pi process is running.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Added opt-in `fetch.timeout` configuration in seconds for the direct HTTP and Jina Reader `fetch_content` paths, with per-call timeout overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #327.
|
|
19
|
+
- Added opt-in `fetch.answerProvider` and `fetch.answerModel` defaults for `fetch_content` answer mode, with per-call `answerModel` overrides taking precedence. Thanks to [@linuxtextadventurer](https://github.com/linuxtextadventurer) for PR #328.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Set the Defuddle fallback document URL before parsing pages with relative canonical links, preventing `ERR_INVALID_URL` warnings. Thanks to [@bin115885](https://github.com/bin115885) for issue #322.
|
|
24
|
+
- Isolated GitHub clone workdirs per extension runtime so cleanup in one process cannot delete another process's clone. Thanks to [@MDGChamomile](https://github.com/MDGChamomile) for PR #323.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## [0.26.0] - 2026-08-28
|
|
28
|
+
|
|
29
|
+
### Highlights
|
|
30
|
+
- `web_search` now includes XCrawl as an explicit search provider.
|
|
31
|
+
- XCrawl results now produce safer clickable links, including relative redirect links from the API.
|
|
32
|
+
- Local models can send multiple web-search queries as a JSON string and still get separate searches.
|
|
33
|
+
- HTML fallback extraction is quieter and more accurate when Defuddle cannot process a page.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Added XCrawl as an explicit-only search provider (`provider: "xcrawl"`) with `xcrawlApiKey` / `XCRAWL_API_KEY` credentials, client-side domain filtering, fallback titles, and retriable provider-timeout errors. Thanks to [@zeroicey](https://github.com/zeroicey) for #312 and #313.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Refined the XCrawl provider docs, parser shape, availability metadata, and focused tests.
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- Expanded JSON-array strings supplied in the singular `web_search` `query` field into independent searches, with a clear no-query error for empty arrays. Thanks to [@alex-rs](https://github.com/alex-rs) for #317 and [@zeroicey](https://github.com/zeroicey) for PR #319.
|
|
46
|
+
- Kept Defuddle selector-processing failures out of the Pi console and stopped treating the raw page body as a successful extraction. Thanks to [@riabiy](https://github.com/riabiy) for #315.
|
|
47
|
+
- Resolved API-origin-relative XCrawl result links to absolute URLs and rejected blank result links instead of emitting fabricated source URLs. Thanks to [@zeroicey](https://github.com/zeroicey) for #318.
|
|
48
|
+
|
|
7
49
|
## [0.25.0] - 2026-08-25
|
|
8
50
|
|
|
9
51
|
### Highlights
|