@herbertgao/pi-extensions 2026.9.16 → 2026.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/README.md +14 -15
  2. package/node_modules/@herbertgao/pi-bark/package.json +2 -2
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +4 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/README.md +4 -1
  5. package/node_modules/@herbertgao/pi-cc-extensions/package.json +75 -75
  6. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  7. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  8. package/node_modules/@herbertgao/resume-from/package.json +4 -2
  9. package/node_modules/@herbertgao/sol-pi/package.json +6 -6
  10. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
  11. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +99 -99
  12. package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
  13. package/node_modules/@tifan/pi-handoff/package.json +1 -1
  14. package/node_modules/@tifan/pi-handoff/src/index.ts +56 -26
  15. package/node_modules/@tifan/pi-mermaid-open/package.json +1 -1
  16. package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
  17. package/node_modules/@tifan/pi-recap/package.json +1 -1
  18. package/node_modules/@tifan/pi-rename/package.json +1 -1
  19. package/node_modules/pi-antigravity/package.json +2 -2
  20. package/node_modules/pi-lens/package.json +158 -158
  21. package/node_modules/pi-multi-account/package.json +2 -2
  22. package/node_modules/pi-typesafe/README.md +1 -1
  23. package/node_modules/pi-typesafe/dist/auth.d.ts +9 -5
  24. package/node_modules/pi-typesafe/dist/auth.js +7 -3
  25. package/node_modules/pi-typesafe/dist/backends.d.ts +33 -3
  26. package/node_modules/pi-typesafe/dist/backends.js +95 -1
  27. package/node_modules/pi-typesafe/dist/client.d.ts +5 -5
  28. package/node_modules/pi-typesafe/dist/client.js +12 -9
  29. package/node_modules/pi-typesafe/dist/credentials.d.ts +14 -8
  30. package/node_modules/pi-typesafe/dist/credentials.js +10 -7
  31. package/node_modules/pi-typesafe/dist/errors.d.ts +2 -2
  32. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  33. package/node_modules/pi-typesafe/dist/index.js +1 -1
  34. package/node_modules/pi-typesafe/dist/login.d.ts +2 -2
  35. package/node_modules/pi-typesafe/dist/login.js +2 -2
  36. package/node_modules/pi-typesafe/package.json +5 -5
  37. package/node_modules/pi-web-access/package.json +2 -2
  38. package/node_modules/remote-pi/package.json +5 -1
  39. package/package.json +16 -28
  40. package/node_modules/pi-mcp-adapter/CHANGELOG.md +0 -1013
  41. package/node_modules/pi-mcp-adapter/LICENSE +0 -21
  42. package/node_modules/pi-mcp-adapter/OAUTH.md +0 -369
  43. package/node_modules/pi-mcp-adapter/README.md +0 -1021
  44. package/node_modules/pi-mcp-adapter/abort.ts +0 -34
  45. package/node_modules/pi-mcp-adapter/agent-dir.ts +0 -68
  46. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +0 -416
  47. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +0 -36
  48. package/node_modules/pi-mcp-adapter/app-bridge.bundle.js +0 -67
  49. package/node_modules/pi-mcp-adapter/banner.png +0 -0
  50. package/node_modules/pi-mcp-adapter/bearer-command-resolver.ts +0 -193
  51. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +0 -365
  52. package/node_modules/pi-mcp-adapter/cli.js +0 -454
  53. package/node_modules/pi-mcp-adapter/commands.ts +0 -861
  54. package/node_modules/pi-mcp-adapter/config.ts +0 -1565
  55. package/node_modules/pi-mcp-adapter/consent-manager.ts +0 -95
  56. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +0 -244
  57. package/node_modules/pi-mcp-adapter/direct-tools.ts +0 -428
  58. package/node_modules/pi-mcp-adapter/dist/abort.d.ts +0 -2
  59. package/node_modules/pi-mcp-adapter/dist/abort.js +0 -36
  60. package/node_modules/pi-mcp-adapter/dist/abort.js.map +0 -1
  61. package/node_modules/pi-mcp-adapter/dist/agent-dir.d.ts +0 -10
  62. package/node_modules/pi-mcp-adapter/dist/agent-dir.js +0 -63
  63. package/node_modules/pi-mcp-adapter/dist/agent-dir.js.map +0 -1
  64. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.d.ts +0 -8
  65. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +0 -398
  66. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +0 -1
  67. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +0 -1
  68. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +0 -31
  69. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +0 -1
  70. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.d.ts +0 -6
  71. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js +0 -194
  72. package/node_modules/pi-mcp-adapter/dist/bearer-command-resolver.js.map +0 -1
  73. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +0 -9
  74. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +0 -362
  75. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +0 -1
  76. package/node_modules/pi-mcp-adapter/dist/config.d.ts +0 -134
  77. package/node_modules/pi-mcp-adapter/dist/config.js +0 -1361
  78. package/node_modules/pi-mcp-adapter/dist/config.js.map +0 -1
  79. package/node_modules/pi-mcp-adapter/dist/consent-manager.d.ts +0 -18
  80. package/node_modules/pi-mcp-adapter/dist/consent-manager.js +0 -88
  81. package/node_modules/pi-mcp-adapter/dist/consent-manager.js.map +0 -1
  82. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.d.ts +0 -17
  83. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js +0 -316
  84. package/node_modules/pi-mcp-adapter/dist/elicitation-handler.js.map +0 -1
  85. package/node_modules/pi-mcp-adapter/dist/errors.d.ts +0 -131
  86. package/node_modules/pi-mcp-adapter/dist/errors.js +0 -278
  87. package/node_modules/pi-mcp-adapter/dist/errors.js.map +0 -1
  88. package/node_modules/pi-mcp-adapter/dist/http-ca.d.ts +0 -8
  89. package/node_modules/pi-mcp-adapter/dist/http-ca.js +0 -88
  90. package/node_modules/pi-mcp-adapter/dist/http-ca.js.map +0 -1
  91. package/node_modules/pi-mcp-adapter/dist/jev-client.d.ts +0 -14
  92. package/node_modules/pi-mcp-adapter/dist/jev-client.js +0 -367
  93. package/node_modules/pi-mcp-adapter/dist/jev-client.js.map +0 -1
  94. package/node_modules/pi-mcp-adapter/dist/jev-contracts.d.ts +0 -77
  95. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js +0 -2
  96. package/node_modules/pi-mcp-adapter/dist/jev-contracts.js.map +0 -1
  97. package/node_modules/pi-mcp-adapter/dist/jev-key-store.d.ts +0 -51
  98. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js +0 -205
  99. package/node_modules/pi-mcp-adapter/dist/jev-key-store.js.map +0 -1
  100. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.d.ts +0 -2
  101. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +0 -68
  102. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +0 -1
  103. package/node_modules/pi-mcp-adapter/dist/lifecycle.d.ts +0 -56
  104. package/node_modules/pi-mcp-adapter/dist/lifecycle.js +0 -410
  105. package/node_modules/pi-mcp-adapter/dist/lifecycle.js.map +0 -1
  106. package/node_modules/pi-mcp-adapter/dist/logger.d.ts +0 -51
  107. package/node_modules/pi-mcp-adapter/dist/logger.js +0 -131
  108. package/node_modules/pi-mcp-adapter/dist/logger.js.map +0 -1
  109. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.d.ts +0 -26
  110. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js +0 -103
  111. package/node_modules/pi-mcp-adapter/dist/mcp-auth-fetch.js.map +0 -1
  112. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.d.ts +0 -118
  113. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js +0 -1097
  114. package/node_modules/pi-mcp-adapter/dist/mcp-auth-flow.js.map +0 -1
  115. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +0 -170
  116. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +0 -1142
  117. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +0 -1
  118. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +0 -24
  119. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +0 -203
  120. package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +0 -1
  121. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.d.ts +0 -53
  122. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js +0 -441
  123. package/node_modules/pi-mcp-adapter/dist/mcp-callback-server.js.map +0 -1
  124. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.d.ts +0 -148
  125. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js +0 -689
  126. package/node_modules/pi-mcp-adapter/dist/mcp-oauth-provider.js.map +0 -1
  127. package/node_modules/pi-mcp-adapter/dist/mcp-probe.d.ts +0 -6
  128. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js +0 -166
  129. package/node_modules/pi-mcp-adapter/dist/mcp-probe.js.map +0 -1
  130. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.d.ts +0 -102
  131. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js +0 -369
  132. package/node_modules/pi-mcp-adapter/dist/mcp-tasks.js.map +0 -1
  133. package/node_modules/pi-mcp-adapter/dist/mcp-trace.d.ts +0 -95
  134. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js +0 -242
  135. package/node_modules/pi-mcp-adapter/dist/mcp-trace.js.map +0 -1
  136. package/node_modules/pi-mcp-adapter/dist/metadata-cache.d.ts +0 -19
  137. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +0 -301
  138. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +0 -1
  139. package/node_modules/pi-mcp-adapter/dist/npx-resolver.d.ts +0 -6
  140. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js +0 -505
  141. package/node_modules/pi-mcp-adapter/dist/npx-resolver.js.map +0 -1
  142. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.d.ts +0 -2
  143. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +0 -144
  144. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +0 -1
  145. package/node_modules/pi-mcp-adapter/dist/request-headers-command.d.ts +0 -10
  146. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js +0 -312
  147. package/node_modules/pi-mcp-adapter/dist/request-headers-command.js.map +0 -1
  148. package/node_modules/pi-mcp-adapter/dist/resource-tools.d.ts +0 -1
  149. package/node_modules/pi-mcp-adapter/dist/resource-tools.js +0 -15
  150. package/node_modules/pi-mcp-adapter/dist/resource-tools.js.map +0 -1
  151. package/node_modules/pi-mcp-adapter/dist/runtime-owner.d.ts +0 -13
  152. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js +0 -97
  153. package/node_modules/pi-mcp-adapter/dist/runtime-owner.js.map +0 -1
  154. package/node_modules/pi-mcp-adapter/dist/sampling-handler.d.ts +0 -17
  155. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js +0 -203
  156. package/node_modules/pi-mcp-adapter/dist/sampling-handler.js.map +0 -1
  157. package/node_modules/pi-mcp-adapter/dist/secure-keyring.d.ts +0 -11
  158. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js +0 -172
  159. package/node_modules/pi-mcp-adapter/dist/secure-keyring.js.map +0 -1
  160. package/node_modules/pi-mcp-adapter/dist/server-manager.d.ts +0 -157
  161. package/node_modules/pi-mcp-adapter/dist/server-manager.js +0 -1745
  162. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +0 -1
  163. package/node_modules/pi-mcp-adapter/dist/session-approvals.d.ts +0 -34
  164. package/node_modules/pi-mcp-adapter/dist/session-approvals.js +0 -140
  165. package/node_modules/pi-mcp-adapter/dist/session-approvals.js.map +0 -1
  166. package/node_modules/pi-mcp-adapter/dist/session-recovery.d.ts +0 -26
  167. package/node_modules/pi-mcp-adapter/dist/session-recovery.js +0 -142
  168. package/node_modules/pi-mcp-adapter/dist/session-recovery.js.map +0 -1
  169. package/node_modules/pi-mcp-adapter/dist/state.d.ts +0 -73
  170. package/node_modules/pi-mcp-adapter/dist/state.js +0 -2
  171. package/node_modules/pi-mcp-adapter/dist/state.js.map +0 -1
  172. package/node_modules/pi-mcp-adapter/dist/types.d.ts +0 -642
  173. package/node_modules/pi-mcp-adapter/dist/types.js +0 -331
  174. package/node_modules/pi-mcp-adapter/dist/types.js.map +0 -1
  175. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.d.ts +0 -6
  176. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js +0 -37
  177. package/node_modules/pi-mcp-adapter/dist/ui-app-bridge-helpers.js.map +0 -1
  178. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.d.ts +0 -17
  179. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js +0 -219
  180. package/node_modules/pi-mcp-adapter/dist/ui-resource-handler.js.map +0 -1
  181. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.d.ts +0 -104
  182. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js +0 -59
  183. package/node_modules/pi-mcp-adapter/dist/ui-stream-types.js.map +0 -1
  184. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.d.ts +0 -4
  185. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js +0 -27
  186. package/node_modules/pi-mcp-adapter/dist/ui-tool-visibility.js.map +0 -1
  187. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.d.ts +0 -15
  188. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js +0 -85
  189. package/node_modules/pi-mcp-adapter/dist/unix-socket-transport.js.map +0 -1
  190. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +0 -41
  191. package/node_modules/pi-mcp-adapter/dist/utils.js +0 -417
  192. package/node_modules/pi-mcp-adapter/dist/utils.js.map +0 -1
  193. package/node_modules/pi-mcp-adapter/elicitation-handler.ts +0 -358
  194. package/node_modules/pi-mcp-adapter/error-signal.ts +0 -32
  195. package/node_modules/pi-mcp-adapter/errors.ts +0 -364
  196. package/node_modules/pi-mcp-adapter/examples/jev-accessibility-loop.mjs +0 -85
  197. package/node_modules/pi-mcp-adapter/examples/jev-semantic-filter.mjs +0 -34
  198. package/node_modules/pi-mcp-adapter/failure-backoff.ts +0 -23
  199. package/node_modules/pi-mcp-adapter/glimpse-ui.ts +0 -80
  200. package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -515
  201. package/node_modules/pi-mcp-adapter/http-ca.ts +0 -84
  202. package/node_modules/pi-mcp-adapter/index.ts +0 -2106
  203. package/node_modules/pi-mcp-adapter/init.ts +0 -721
  204. package/node_modules/pi-mcp-adapter/jev-client.ts +0 -300
  205. package/node_modules/pi-mcp-adapter/jev-contracts.ts +0 -53
  206. package/node_modules/pi-mcp-adapter/jev-key-store.ts +0 -192
  207. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +0 -83
  208. package/node_modules/pi-mcp-adapter/lazy-loader.ts +0 -7
  209. package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -457
  210. package/node_modules/pi-mcp-adapter/logger.ts +0 -169
  211. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +0 -113
  212. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +0 -1296
  213. package/node_modules/pi-mcp-adapter/mcp-auth.ts +0 -1287
  214. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -250
  215. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +0 -534
  216. package/node_modules/pi-mcp-adapter/mcp-code.ts +0 -495
  217. package/node_modules/pi-mcp-adapter/mcp-install.ts +0 -60
  218. package/node_modules/pi-mcp-adapter/mcp-keyring-helper.cjs +0 -89
  219. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +0 -819
  220. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +0 -511
  221. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +0 -104
  222. package/node_modules/pi-mcp-adapter/mcp-panel.ts +0 -1099
  223. package/node_modules/pi-mcp-adapter/mcp-probe.ts +0 -201
  224. package/node_modules/pi-mcp-adapter/mcp-references.ts +0 -373
  225. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +0 -137
  226. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +0 -747
  227. package/node_modules/pi-mcp-adapter/mcp-status.ts +0 -105
  228. package/node_modules/pi-mcp-adapter/mcp-tasks.ts +0 -467
  229. package/node_modules/pi-mcp-adapter/mcp-trace.ts +0 -306
  230. package/node_modules/pi-mcp-adapter/metadata-cache.ts +0 -362
  231. package/node_modules/pi-mcp-adapter/namespace-tools.ts +0 -334
  232. package/node_modules/pi-mcp-adapter/npx-resolver.ts +0 -543
  233. package/node_modules/pi-mcp-adapter/oauth-handler.ts +0 -30
  234. package/node_modules/pi-mcp-adapter/oauth.ts +0 -52
  235. package/node_modules/pi-mcp-adapter/onboarding-state.ts +0 -76
  236. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +0 -156
  237. package/node_modules/pi-mcp-adapter/package.json +0 -219
  238. package/node_modules/pi-mcp-adapter/panel-keys.ts +0 -53
  239. package/node_modules/pi-mcp-adapter/prompts.ts +0 -385
  240. package/node_modules/pi-mcp-adapter/proxy-modes.ts +0 -1735
  241. package/node_modules/pi-mcp-adapter/request-headers-command.ts +0 -333
  242. package/node_modules/pi-mcp-adapter/resource-tools.ts +0 -17
  243. package/node_modules/pi-mcp-adapter/runtime-owner.ts +0 -106
  244. package/node_modules/pi-mcp-adapter/sampling-handler.ts +0 -261
  245. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +0 -144
  246. package/node_modules/pi-mcp-adapter/search-ranking.ts +0 -309
  247. package/node_modules/pi-mcp-adapter/secure-keyring.ts +0 -170
  248. package/node_modules/pi-mcp-adapter/semantic-search.ts +0 -186
  249. package/node_modules/pi-mcp-adapter/server-manager.ts +0 -2088
  250. package/node_modules/pi-mcp-adapter/session-approvals.ts +0 -201
  251. package/node_modules/pi-mcp-adapter/session-recovery.ts +0 -158
  252. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +0 -49
  253. package/node_modules/pi-mcp-adapter/state.ts +0 -71
  254. package/node_modules/pi-mcp-adapter/tool-approval.ts +0 -205
  255. package/node_modules/pi-mcp-adapter/tool-metadata.ts +0 -302
  256. package/node_modules/pi-mcp-adapter/tool-registrar.ts +0 -255
  257. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +0 -573
  258. package/node_modules/pi-mcp-adapter/ts-shape.ts +0 -149
  259. package/node_modules/pi-mcp-adapter/types.ts +0 -1120
  260. package/node_modules/pi-mcp-adapter/ui-app-bridge-helpers.ts +0 -40
  261. package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +0 -266
  262. package/node_modules/pi-mcp-adapter/ui-server.ts +0 -993
  263. package/node_modules/pi-mcp-adapter/ui-session.ts +0 -624
  264. package/node_modules/pi-mcp-adapter/ui-stream-types.ts +0 -89
  265. package/node_modules/pi-mcp-adapter/ui-tool-visibility.ts +0 -25
  266. package/node_modules/pi-mcp-adapter/unix-socket-transport.ts +0 -85
  267. package/node_modules/pi-mcp-adapter/utils.ts +0 -464
@@ -1,1296 +0,0 @@
1
- /**
2
- * MCP Auth Flow
3
- *
4
- * High-level OAuth flow management using the MCP SDK's built-in auth functions.
5
- */
6
-
7
- import {
8
- auth as runSdkAuth,
9
- extractWWWAuthenticateParams,
10
- LATEST_PROTOCOL_VERSION,
11
- UnauthorizedError,
12
- validateClientMetadataUrl,
13
- type AuthOptions,
14
- } from "@modelcontextprotocol/client"
15
- import open from "open"
16
- import {
17
- getOAuthCallbackPort,
18
- McpOAuthProvider,
19
- type McpOAuthConfig,
20
- } from "./mcp-oauth-provider.ts"
21
- import {
22
- ensureCallbackServer,
23
- waitForCallback,
24
- cancelPendingCallback,
25
- stopCallbackServer,
26
- stopCallbackServerIfIdle,
27
- releaseCallbackServer,
28
- } from "./mcp-callback-server.ts"
29
- import {
30
- getAuthForUrl,
31
- getAuthEntry,
32
- clearAllCredentials,
33
- clearClientInfo,
34
- clearCodeVerifier,
35
- getOAuthState,
36
- clearOAuthState,
37
- getAuthBaseDir,
38
- beginOAuthRevocation,
39
- captureOAuthAuthority,
40
- OAuthCredentialStoreError,
41
- type AuthStorageOptions,
42
- type OAuthAuthority,
43
- type StoredTokens,
44
- } from "./mcp-auth.ts"
45
- import { isServerDisabled, type ServerEntry } from "./types.ts"
46
- import { formatTerminalError, interpolateEnvVars } from "./utils.ts"
47
- import { createOAuthFetch, oauthHeaderResolver, resolveOAuthHeaders } from "./mcp-auth-fetch.ts"
48
- import { isBuiltInAgentPlugin } from "./agent-plugin-provenance.ts"
49
- import { abortable, throwIfAborted } from "./abort.ts"
50
- import { combineAbortSignals, isAbortError } from "./runtime-owner.ts"
51
-
52
- /** Auth status for a server */
53
- export type AuthStatus = "authenticated" | "expired" | "not_authenticated"
54
-
55
- export interface McpOAuthRuntime {
56
- readonly signal: AbortSignal
57
- }
58
-
59
- export interface AuthenticateOptions {
60
- onAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>
61
- openAuthorizationUrl?: (authorizationUrl: string) => void | Promise<void>
62
- onAuthorizationInput?: (
63
- authorizationUrl: string,
64
- signal: AbortSignal,
65
- ) => Promise<string | undefined>
66
- authStorageOptions?: AuthStorageOptions
67
- signal?: AbortSignal
68
- runtime?: McpOAuthRuntime
69
- skipIssuerMetadataValidation?: boolean
70
- definition?: Pick<ServerEntry, "headers" | "oauth">
71
- }
72
-
73
- type AuthDiscovery = Pick<AuthOptions, "resourceMetadataUrl" | "scope" | "skipIssuerMetadataValidation">
74
-
75
- function pluginAwareOAuthHeaders(definition?: ServerEntry): () => Headers {
76
- return oauthHeaderResolver(definition?.headers, {
77
- literal: definition ? isBuiltInAgentPlugin(definition, "headers") : false,
78
- })
79
- }
80
-
81
- function applyOAuthConfig(discovery: AuthDiscovery, config: McpOAuthConfig): AuthDiscovery {
82
- return {
83
- ...discovery,
84
- ...(config.scope !== undefined ? { scope: config.scope } : {}),
85
- ...(config.skipIssuerMetadataValidation === true ? { skipIssuerMetadataValidation: true } : {}),
86
- }
87
- }
88
-
89
- type PendingAuth = {
90
- serverName: string
91
- authProvider: McpOAuthProvider
92
- serverUrl: string
93
- authorizationUrl: string
94
- manualRedirect: boolean
95
- manualCompletionController?: AbortController
96
- discovery: AuthDiscovery
97
- getHeaders: () => Headers
98
- authStorageOptions: AuthStorageOptions
99
- authority: OAuthAuthority
100
- }
101
-
102
- type PendingAuthentication = { promise: Promise<AuthStatus>; authority: OAuthAuthority }
103
-
104
- type RuntimeState = {
105
- controller: AbortController
106
- generation: number
107
- pendingAuths: Map<string, PendingAuth>
108
- pendingAuthStates: Map<string, string>
109
- pendingAuthCleanupTimers: Map<string, ReturnType<typeof setTimeout>>
110
- pendingAuthentications: Map<string, PendingAuthentication>
111
- }
112
-
113
- const runtimeStates = new WeakMap<McpOAuthRuntime, RuntimeState>()
114
- const activeRuntimes = new Set<McpOAuthRuntime>()
115
-
116
- export function createOAuthRuntime(signal?: AbortSignal): McpOAuthRuntime {
117
- const controller = new AbortController()
118
- const runtime = { signal: combineAbortSignals(signal, controller.signal)! } satisfies McpOAuthRuntime
119
- runtimeStates.set(runtime, {
120
- controller,
121
- generation: 0,
122
- pendingAuths: new Map(),
123
- pendingAuthStates: new Map(),
124
- pendingAuthCleanupTimers: new Map(),
125
- pendingAuthentications: new Map(),
126
- })
127
- activeRuntimes.add(runtime)
128
- return runtime
129
- }
130
-
131
- let legacyRuntime = createOAuthRuntime()
132
- activeRuntimes.delete(legacyRuntime)
133
-
134
- function getRuntime(options?: AuthenticateOptions): McpOAuthRuntime {
135
- if (options?.runtime) {
136
- options.runtime.signal.throwIfAborted()
137
- activeRuntimes.add(options.runtime)
138
- return options.runtime
139
- }
140
- if (legacyRuntime.signal.aborted) legacyRuntime = createOAuthRuntime()
141
- activeRuntimes.add(legacyRuntime)
142
- return legacyRuntime
143
- }
144
-
145
- function getRuntimeState(runtime: McpOAuthRuntime): RuntimeState {
146
- const state = runtimeStates.get(runtime)
147
- if (!state) throw new Error("Unknown OAuth runtime")
148
- return state
149
- }
150
-
151
- function getAuthStorageIdentity(options: AuthStorageOptions): ["encrypted-file"] | ["os", string] {
152
- return options.credentialStore === "encrypted-file"
153
- ? ["encrypted-file"]
154
- : ["os", getAuthBaseDir(options)]
155
- }
156
-
157
- function getPendingAuthKey(serverName: string, options: AuthStorageOptions): string {
158
- return JSON.stringify([serverName, ...getAuthStorageIdentity(options)])
159
- }
160
-
161
- function hasOAuthAuthority(authority: OAuthAuthority): boolean {
162
- try {
163
- authority()
164
- return true
165
- } catch {
166
- return false
167
- }
168
- }
169
-
170
- export function hasPendingAuth(serverName: string, options?: AuthStorageOptions, runtime?: McpOAuthRuntime): boolean {
171
- const state = getRuntimeState(runtime ?? legacyRuntime)
172
- if (options) {
173
- return state.pendingAuths.has(getPendingAuthKey(serverName, options))
174
- }
175
- return Array.from(state.pendingAuths.values()).some(pendingAuth => pendingAuth.serverName === serverName)
176
- }
177
-
178
- /** Timeout for manual auth completion (5 minutes) */
179
- const MANUAL_AUTH_TIMEOUT_MS = 5 * 60 * 1000
180
-
181
- /**
182
- * Generate a cryptographically secure random state parameter.
183
- */
184
- function generateState(): string {
185
- return Array.from(crypto.getRandomValues(new Uint8Array(32)))
186
- .map((b) => b.toString(16).padStart(2, "0"))
187
- .join("")
188
- }
189
-
190
- /**
191
- * Extract OAuth configuration from a ServerEntry.
192
- */
193
- export function extractOAuthConfig(definition: ServerEntry): McpOAuthConfig {
194
- if (definition.oauth === false) {
195
- return {}
196
- }
197
-
198
- const config: McpOAuthConfig = {}
199
- if (definition.oauth?.grantType !== undefined) config.grantType = definition.oauth.grantType
200
- if (definition.oauth?.clientId !== undefined) {
201
- if (typeof definition.oauth.clientId !== "string") throw new Error("OAuth clientId must be a string")
202
- config.clientId = interpolateEnvVars(definition.oauth.clientId)
203
- }
204
- if (definition.oauth?.clientSecret !== undefined) {
205
- if (typeof definition.oauth.clientSecret !== "string") throw new Error("OAuth clientSecret must be a string")
206
- // Preserve command expressions for the provider; interpolation remains eager for ordinary values.
207
- config.clientSecret = definition.oauth.clientSecret.startsWith("!")
208
- ? definition.oauth.clientSecret
209
- : interpolateEnvVars(definition.oauth.clientSecret)
210
- }
211
- if (definition.oauth?.clientMetadataUrl !== undefined) {
212
- if (typeof definition.oauth.clientMetadataUrl !== "string") {
213
- throw new Error("OAuth clientMetadataUrl must be a string")
214
- }
215
- const clientMetadataUrl = interpolateEnvVars(definition.oauth.clientMetadataUrl).trim()
216
- if (!clientMetadataUrl) {
217
- throw new Error("OAuth clientMetadataUrl must not be empty")
218
- }
219
- validateClientMetadataUrl(clientMetadataUrl)
220
- config.clientMetadataUrl = clientMetadataUrl
221
- }
222
- if (config.clientMetadataUrl !== undefined && config.clientSecret !== undefined && !config.clientId) {
223
- throw new Error("OAuth clientSecret requires an explicit clientId when clientMetadataUrl is configured")
224
- }
225
- if (definition.oauth?.scope !== undefined) {
226
- if (typeof definition.oauth.scope !== "string") throw new Error("OAuth scope must be a string")
227
- config.scope = interpolateEnvVars(definition.oauth.scope)
228
- }
229
- if (definition.oauth?.authorizationParams !== undefined) {
230
- const params = definition.oauth.authorizationParams
231
- if (!params || typeof params !== "object" || Array.isArray(params)) {
232
- throw new Error("OAuth authorizationParams must be an object")
233
- }
234
- config.authorizationParams = {}
235
- for (const [key, value] of Object.entries(params)) {
236
- if (!key) throw new Error("OAuth authorizationParams keys must not be empty")
237
- if (typeof value !== "string") throw new Error(`OAuth authorizationParams.${key} must be a string`)
238
- config.authorizationParams[key] = interpolateEnvVars(value)
239
- }
240
- }
241
- if (definition.oauth?.redirectUri !== undefined) {
242
- if (typeof definition.oauth.redirectUri !== "string") {
243
- throw new Error("OAuth redirectUri must be a string")
244
- }
245
- const redirectUri = interpolateEnvVars(definition.oauth.redirectUri).trim()
246
- if (!redirectUri) {
247
- throw new Error("OAuth redirectUri must not be empty")
248
- }
249
- config.redirectUri = redirectUri
250
- }
251
- if (definition.oauth?.clientName !== undefined) {
252
- if (typeof definition.oauth.clientName !== "string") {
253
- throw new Error("OAuth clientName must be a string")
254
- }
255
- const clientName = interpolateEnvVars(definition.oauth.clientName).trim()
256
- if (!clientName) {
257
- throw new Error("OAuth clientName must not be empty")
258
- }
259
- config.clientName = clientName
260
- }
261
- if (definition.oauth?.clientUri !== undefined) {
262
- if (typeof definition.oauth.clientUri !== "string") {
263
- throw new Error("OAuth clientUri must be a string")
264
- }
265
- const clientUri = interpolateEnvVars(definition.oauth.clientUri).trim()
266
- if (!clientUri) {
267
- throw new Error("OAuth clientUri must not be empty")
268
- }
269
- config.clientUri = clientUri
270
- }
271
- if (definition.oauth?.logoUri !== undefined) {
272
- if (typeof definition.oauth.logoUri !== "string") {
273
- throw new Error("OAuth logoUri must be a string")
274
- }
275
- const logoUri = interpolateEnvVars(definition.oauth.logoUri).trim()
276
- if (!logoUri) {
277
- throw new Error("OAuth logoUri must not be empty")
278
- }
279
- // Consent screens fetch this server-side, so a local path silently renders
280
- // nothing. Fail here instead, where the message can say why.
281
- let parsed: URL
282
- try {
283
- parsed = new URL(logoUri)
284
- } catch {
285
- throw new Error("OAuth logoUri must be an absolute http(s) URL")
286
- }
287
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
288
- throw new Error("OAuth logoUri must be an absolute http(s) URL")
289
- }
290
- config.logoUri = logoUri
291
- }
292
- if (definition.oauth?.authServerMetadataUrl !== undefined) {
293
- if (typeof definition.oauth.authServerMetadataUrl !== "string") {
294
- throw new Error("OAuth authServerMetadataUrl must be a string")
295
- }
296
- const authServerMetadataUrl = interpolateEnvVars(definition.oauth.authServerMetadataUrl).trim()
297
- if (!authServerMetadataUrl) {
298
- throw new Error("OAuth authServerMetadataUrl must not be empty")
299
- }
300
- let parsed: URL
301
- try {
302
- parsed = new URL(authServerMetadataUrl)
303
- } catch {
304
- throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
305
- }
306
- if (parsed.protocol !== "https:") {
307
- throw new Error("OAuth authServerMetadataUrl must be an absolute https:// URL")
308
- }
309
- config.authServerMetadataUrl = authServerMetadataUrl
310
- }
311
- if (definition.oauth?.skipIssuerMetadataValidation !== undefined) {
312
- if (typeof definition.oauth.skipIssuerMetadataValidation !== "boolean") {
313
- throw new Error("OAuth skipIssuerMetadataValidation must be a boolean")
314
- }
315
- config.skipIssuerMetadataValidation = definition.oauth.skipIssuerMetadataValidation
316
- }
317
- return config
318
- }
319
-
320
- async function probeAuthDiscovery(serverUrl: string, definition?: ServerEntry, signal?: AbortSignal): Promise<AuthDiscovery> {
321
- // The preliminary probe is command-free; real SDK discovery resolves commands.
322
- const serviceHeaders = resolveOAuthHeaders(definition?.headers, {
323
- commands: false,
324
- literal: definition ? isBuiltInAgentPlugin(definition, "headers") : false,
325
- })
326
- const probeFetch = createOAuthFetch(serverUrl, () => serviceHeaders, signal, { timeout: false })
327
- const headers = new Headers({ "content-type": "application/json" })
328
-
329
- const controller = new AbortController()
330
- const discoverySignal = combineAbortSignals(signal, controller.signal)
331
- const timer = setTimeout(() => controller.abort(), 5000)
332
-
333
- try {
334
- headers.set("accept", "application/json, text/event-stream")
335
-
336
- const response = await probeFetch(new URL(serverUrl), {
337
- method: "POST",
338
- headers,
339
- body: JSON.stringify({
340
- jsonrpc: "2.0",
341
- id: 0,
342
- method: "initialize",
343
- params: {
344
- protocolVersion: LATEST_PROTOCOL_VERSION,
345
- capabilities: {},
346
- clientInfo: { name: "pi-mcp-adapter", version: "2.11.0" },
347
- },
348
- }),
349
- ...(discoverySignal ? { signal: discoverySignal } : {}),
350
- })
351
- const { resourceMetadataUrl, scope } = extractWWWAuthenticateParams(response)
352
- await response.body?.cancel().catch(() => {})
353
- return { ...(resourceMetadataUrl ? { resourceMetadataUrl } : {}), ...(scope ? { scope } : {}) }
354
- } catch (error) {
355
- if (signal?.aborted) throwIfAborted(signal)
356
- return {}
357
- } finally {
358
- clearTimeout(timer)
359
- }
360
- }
361
-
362
- type OAuthRedirectTarget =
363
- | {
364
- mode: "local"
365
- port?: number
366
- callbackHost: string
367
- callbackPath: string
368
- dynamicPort: boolean
369
- resolveRedirectUri: (port: number) => string
370
- }
371
- | { mode: "manual" }
372
-
373
- function parseOAuthRedirectUri(redirectUri: string): OAuthRedirectTarget {
374
- const dynamicPortPlaceholder = "{port}"
375
- const placeholderCount = redirectUri.split(dynamicPortPlaceholder).length - 1
376
- if (placeholderCount > 1) {
377
- throw new Error("OAuth redirectUri may contain at most one {port} placeholder")
378
- }
379
-
380
- let parsedRedirectUri = redirectUri
381
- const dynamicPort = placeholderCount === 1
382
- if (dynamicPort) {
383
- const authorityStart = redirectUri.indexOf("://") + 3
384
- const pathStart = redirectUri.slice(authorityStart).search(/[/?#]/)
385
- const authorityEnd = pathStart === -1 ? redirectUri.length : authorityStart + pathStart
386
- const authority = redirectUri.slice(authorityStart, authorityEnd)
387
- if (authorityStart < 3 || !authority.endsWith(`:${dynamicPortPlaceholder}`)) {
388
- throw new Error("OAuth redirectUri {port} placeholder must be the loopback URI port")
389
- }
390
- // Parse with a real port, then replace the placeholder only after the OS
391
- // assigns the callback listener's port.
392
- parsedRedirectUri = redirectUri.replace(dynamicPortPlaceholder, "1")
393
- }
394
-
395
- let url: URL
396
- try {
397
- url = new URL(parsedRedirectUri)
398
- } catch (error) {
399
- throw new Error(`Invalid OAuth redirectUri: ${redirectUri}`, { cause: error })
400
- }
401
-
402
- if (url.username || url.password) {
403
- throw new Error("OAuth redirectUri must not include username or password")
404
- }
405
-
406
- if (url.hash) {
407
- throw new Error("OAuth redirectUri must not include a fragment")
408
- }
409
-
410
- const hostname = url.hostname.toLowerCase()
411
- const isLocalhost = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname === "::1"
412
- if (dynamicPort && (url.protocol !== "http:" || !isLocalhost)) {
413
- throw new Error("OAuth redirectUri {port} placeholder is allowed only for an http:// localhost or loopback URI")
414
- }
415
- if (url.port) {
416
- const parsedPort = Number.parseInt(url.port, 10)
417
- if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
418
- throw new Error("OAuth redirectUri port must be a positive numeric port")
419
- }
420
- }
421
- if (url.protocol === "https:" && !isLocalhost) {
422
- return { mode: "manual" }
423
- }
424
- if (url.protocol !== "http:" || !isLocalhost) {
425
- throw new Error("OAuth redirectUri must be an https:// URI or an http:// localhost or loopback URI")
426
- }
427
-
428
- if (!url.port) {
429
- throw new Error("OAuth localhost redirectUri must include an explicit numeric port")
430
- }
431
-
432
- const port = Number.parseInt(url.port, 10)
433
- const callbackHost = hostname === "[::1]" ? "::1" : hostname
434
- return {
435
- mode: "local",
436
- ...(dynamicPort ? {} : { port }),
437
- callbackHost,
438
- callbackPath: url.pathname,
439
- dynamicPort,
440
- resolveRedirectUri: assignedPort => dynamicPort
441
- ? redirectUri.replace(dynamicPortPlaceholder, String(assignedPort))
442
- : redirectUri,
443
- }
444
- }
445
-
446
- /**
447
- * Start OAuth authentication flow for a server.
448
- * Returns the authorization URL when browser authorization is required.
449
- */
450
- export async function startAuth(
451
- serverName: string,
452
- serverUrl: string,
453
- definition?: ServerEntry,
454
- options: AuthenticateOptions = {},
455
- operationAuthority?: OAuthAuthority,
456
- ): Promise<{ authorizationUrl: string }> {
457
- if (isServerDisabled(definition)) throw new Error(`MCP server "${serverName}" is disabled`)
458
- const authority = operationAuthority ?? captureOAuthAuthority(serverName)
459
- authority()
460
- const runtime = getRuntime(options)
461
- const runtimeState = getRuntimeState(runtime)
462
- const config = definition ? extractOAuthConfig(definition) : {}
463
- const authStorageOptions = options.authStorageOptions ?? {}
464
- const signal = combineAbortSignals(runtime.signal, options.signal)
465
- const generation = runtimeState.generation
466
- throwIfAborted(signal)
467
-
468
- if (config.grantType === "client_credentials") {
469
- const storedAuth = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
470
- authority()
471
- if (storedAuth?.clientInfo && !storedAuth.tokens && !config.clientId) {
472
- clearClientInfo(serverName, authStorageOptions)
473
- clearCodeVerifier(serverName, authStorageOptions)
474
- clearOAuthState(serverName, authStorageOptions)
475
- }
476
-
477
- authority()
478
- const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
479
- onRedirect: async () => {
480
- throw new Error("Browser redirect is not used for client_credentials flow")
481
- },
482
- }, authStorageOptions, runtime.signal, undefined, authority)
483
- try {
484
- const fetchFn = createOAuthFetch(serverUrl, pluginAwareOAuthHeaders(definition), signal)
485
- authProvider.setAuthFetch(fetchFn)
486
- const discovery = applyOAuthConfig(await probeAuthDiscovery(serverUrl, definition, signal), config)
487
- authority()
488
- throwIfAborted(signal)
489
- const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery, fetchFn }), signal)
490
- authority()
491
- throwIfAborted(signal)
492
- if (result !== "AUTHORIZED") {
493
- throw new UnauthorizedError("Failed to authorize")
494
- }
495
- return { authorizationUrl: "" }
496
- } finally {
497
- authProvider.deactivate()
498
- }
499
- }
500
-
501
- const existingPendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
502
- if (existingPendingAuth?.serverUrl === serverUrl) {
503
- existingPendingAuth.authority()
504
- return { authorizationUrl: existingPendingAuth.authorizationUrl }
505
- }
506
-
507
- const redirectTarget = config.redirectUri !== undefined ? parseOAuthRedirectUri(config.redirectUri) : undefined
508
- const manualRedirect = redirectTarget?.mode === "manual"
509
- const oauthState = generateState()
510
-
511
- if (!manualRedirect) {
512
- try {
513
- await ensureCallbackServer({
514
- strictPort: redirectTarget?.mode === "local"
515
- ? !redirectTarget.dynamicPort
516
- : Boolean(config.clientId),
517
- oauthState,
518
- reserveState: true,
519
- ...(redirectTarget?.mode === "local"
520
- ? {
521
- ...(redirectTarget.port !== undefined ? { port: redirectTarget.port } : {}),
522
- callbackHost: redirectTarget.callbackHost,
523
- callbackPath: redirectTarget.callbackPath,
524
- }
525
- : {}),
526
- })
527
- authority()
528
- throwIfAborted(signal)
529
- if (redirectTarget?.mode === "local" && redirectTarget.dynamicPort) {
530
- config.redirectUri = redirectTarget.resolveRedirectUri(getOAuthCallbackPort())
531
- }
532
- } catch (error) {
533
- releaseCallbackServer(oauthState)
534
- try {
535
- await cleanupAndReleaseCallbackServerIfIdle(() => {
536
- if (hasOAuthAuthority(authority)) clearOAuthState(serverName, authStorageOptions)
537
- })
538
- } catch (cleanupError) {
539
- throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
540
- }
541
- throw error
542
- }
543
- }
544
-
545
- let capturedUrl: URL | undefined
546
- authority()
547
- const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
548
- onRedirect: async (url) => {
549
- capturedUrl = url
550
- },
551
- }, authStorageOptions, runtime.signal, oauthState, authority)
552
-
553
- try {
554
- const storedAuth = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
555
- authority()
556
- if (storedAuth?.clientInfo && !config.clientId) {
557
- if (!storedAuth.tokens) {
558
- clearClientInfo(serverName, authStorageOptions)
559
- clearCodeVerifier(serverName, authStorageOptions)
560
- clearOAuthState(serverName, authStorageOptions)
561
- } else {
562
- const redirectUris = storedAuth.clientInfo.redirectUris
563
- const redirectUriMatches = Array.isArray(redirectUris)
564
- && redirectUris.includes(authProvider.redirectUrl ?? "")
565
- if (!redirectUriMatches && !storedAuth.tokens.refreshToken) {
566
- // A stale redirect URI only blocks the interactive leg; refresh does
567
- // not send redirect_uri, so keep refresh-capable credentials intact.
568
- clearClientInfo(serverName, authStorageOptions)
569
- clearCodeVerifier(serverName, authStorageOptions)
570
- clearOAuthState(serverName, authStorageOptions)
571
- }
572
- }
573
- }
574
-
575
- throwIfAborted(signal)
576
-
577
- const getHeaders = pluginAwareOAuthHeaders(definition)
578
- const fetchFn = createOAuthFetch(serverUrl, getHeaders, signal)
579
- authProvider.setAuthFetch(fetchFn)
580
- const discovery = applyOAuthConfig(await probeAuthDiscovery(serverUrl, definition, signal), config)
581
- authority()
582
- throwIfAborted(signal)
583
- const result = await abortable(runSdkAuth(authProvider, { serverUrl, ...discovery, fetchFn }), signal)
584
- authority()
585
- throwIfAborted(signal)
586
- if (result === "AUTHORIZED") {
587
- authProvider.deactivate()
588
- releaseCallbackServer(oauthState)
589
- clearOAuthState(serverName, authStorageOptions)
590
- await stopCallbackServerIfIdle()
591
- authority()
592
- return { authorizationUrl: "" }
593
- }
594
- if (!capturedUrl) {
595
- throw new UnauthorizedError("OAuth authorization URL was not provided")
596
- }
597
- await setPendingAuth(runtime, serverName, {
598
- serverName,
599
- authProvider,
600
- serverUrl,
601
- authorizationUrl: capturedUrl.toString(),
602
- manualRedirect,
603
- ...(manualRedirect ? { manualCompletionController: new AbortController() } : {}),
604
- discovery,
605
- getHeaders,
606
- authStorageOptions,
607
- authority,
608
- }, oauthState, signal, generation)
609
- authority()
610
- return { authorizationUrl: capturedUrl.toString() }
611
- } catch (error) {
612
- authProvider.deactivate()
613
- try {
614
- await clearPendingAuthAndReleaseIfIdle(runtime, serverName, oauthState, authStorageOptions)
615
- } catch (cleanupError) {
616
- throw new AggregateError([error, cleanupError], "OAuth startup cleanup failed")
617
- }
618
- throw error
619
- }
620
- }
621
-
622
- async function setPendingAuth(
623
- runtime: McpOAuthRuntime,
624
- serverName: string,
625
- pendingAuth: PendingAuth,
626
- oauthState: string,
627
- signal?: AbortSignal,
628
- generation = getRuntimeState(runtime).generation,
629
- ): Promise<void> {
630
- const state = getRuntimeState(runtime)
631
- const key = getPendingAuthKey(serverName, pendingAuth.authStorageOptions)
632
- pendingAuth.authority()
633
- await clearPendingAuth(runtime, serverName, undefined, pendingAuth.authStorageOptions)
634
- pendingAuth.authority()
635
- throwIfAborted(signal)
636
- if (generation !== state.generation) throw new Error("OAuth runtime stopped")
637
- state.pendingAuths.set(key, pendingAuth)
638
- state.pendingAuthStates.set(key, oauthState)
639
- const cleanupTimer = setTimeout(() => {
640
- void clearPendingAuthAndReleaseIfIdle(
641
- runtime,
642
- serverName,
643
- oauthState,
644
- pendingAuth.authStorageOptions,
645
- new Error("OAuth authorization timeout - authorization took too long"),
646
- ).catch(error => {
647
- console.error(`MCP Auth: Timed-out flow cleanup failed: ${formatTerminalError(error)}`)
648
- })
649
- }, MANUAL_AUTH_TIMEOUT_MS)
650
- cleanupTimer.unref?.()
651
- state.pendingAuthCleanupTimers.set(key, cleanupTimer)
652
- }
653
-
654
- async function clearPendingAuth(
655
- runtime: McpOAuthRuntime,
656
- serverName: string,
657
- oauthState?: string,
658
- fallbackStorageOptions: AuthStorageOptions = {},
659
- reason: Error = new Error("Authorization cancelled"),
660
- ): Promise<void> {
661
- const state = getRuntimeState(runtime)
662
- const key = getPendingAuthKey(serverName, fallbackStorageOptions)
663
- const pendingState = state.pendingAuthStates.get(key)
664
- if (oauthState && pendingState !== oauthState) {
665
- cancelPendingCallback(oauthState)
666
- return
667
- }
668
-
669
- const { pendingAuth } = detachPending(state, key, reason)
670
- const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackStorageOptions
671
- const stateToRelease = pendingState ?? oauthState
672
- if (stateToRelease) {
673
- if (!pendingState) cancelPendingCallback(stateToRelease)
674
- if (pendingAuth && hasOAuthAuthority(pendingAuth.authority)) {
675
- const storedState = getOAuthState(serverName, authStorageOptions)
676
- if (storedState === stateToRelease) clearOAuthState(serverName, authStorageOptions)
677
- }
678
- }
679
- }
680
-
681
- function detachPending(state: RuntimeState, key: string, reason: Error) {
682
- const pendingAuth = state.pendingAuths.get(key)
683
- const oauthState = state.pendingAuthStates.get(key)
684
- const timer = state.pendingAuthCleanupTimers.get(key)
685
- if (timer) clearTimeout(timer)
686
- state.pendingAuthCleanupTimers.delete(key)
687
- state.pendingAuths.delete(key)
688
- state.pendingAuthStates.delete(key)
689
- pendingAuth?.manualCompletionController?.abort(reason)
690
- pendingAuth?.authProvider.deactivate()
691
- if (oauthState) cancelPendingCallback(oauthState)
692
- return { pendingAuth, oauthState }
693
- }
694
-
695
- async function clearPendingAuthAndReleaseIfIdle(
696
- runtime: McpOAuthRuntime,
697
- serverName: string,
698
- oauthState: string | undefined,
699
- fallbackStorageOptions: AuthStorageOptions = {},
700
- reason?: Error,
701
- ): Promise<void> {
702
- await cleanupAndReleaseCallbackServerIfIdle(
703
- () => clearPendingAuth(runtime, serverName, oauthState, fallbackStorageOptions, reason),
704
- )
705
- }
706
-
707
- async function cleanupAndReleaseCallbackServerIfIdle(cleanup: () => void | Promise<void>): Promise<void> {
708
- let cleanupFailure: { error: unknown } | undefined
709
- try {
710
- await cleanup()
711
- } catch (error) {
712
- cleanupFailure = { error }
713
- }
714
-
715
- try {
716
- await stopCallbackServerIfIdle()
717
- } catch (releaseError) {
718
- if (cleanupFailure) {
719
- throw new AggregateError([cleanupFailure.error, releaseError], "OAuth callback cleanup failed")
720
- }
721
- throw releaseError
722
- }
723
-
724
- if (cleanupFailure) throw cleanupFailure.error
725
- }
726
-
727
- function detachPendingAuthsForServer(serverName: string, reason: Error): void {
728
- for (const runtime of activeRuntimes) {
729
- const state = getRuntimeState(runtime)
730
- for (const [key, pendingAuth] of state.pendingAuths) {
731
- if (pendingAuth.serverName !== serverName) continue
732
- if (state.pendingAuths.get(key) !== pendingAuth) continue
733
- detachPending(state, key, reason)
734
- }
735
- }
736
- }
737
-
738
- function getSearchParamsFromInput(input: string): URLSearchParams | undefined {
739
- try {
740
- const url = new URL(input)
741
- const params = new URLSearchParams(url.search)
742
- if (url.hash) {
743
- const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash
744
- const hashParams = new URLSearchParams(hash)
745
- for (const [key, value] of hashParams) {
746
- if (!params.has(key)) params.set(key, value)
747
- }
748
- }
749
- return params
750
- } catch {
751
- const query = input.includes("?") ? input.slice(input.indexOf("?") + 1) : input
752
- const params = new URLSearchParams(query.startsWith("#") ? query.slice(1) : query)
753
- return params.has("code") || params.has("state") || params.has("error") ? params : undefined
754
- }
755
- }
756
-
757
- /** Authorization code plus the optional RFC 9207 `iss` callback parameter. */
758
- export interface AuthorizationCodeInput {
759
- code: string
760
- iss?: string
761
- }
762
-
763
- /**
764
- * Extract an OAuth authorization code (and the RFC 9207 `iss` parameter, when
765
- * present) from either a raw code, a query string, or the full localhost
766
- * redirect URL copied from the browser address bar.
767
- */
768
- export function parseAuthorizationRedirectInput(input: string, expectedState?: string): AuthorizationCodeInput {
769
- const trimmed = input.trim()
770
- if (!trimmed) {
771
- throw new Error("Authorization code or redirect URL is required")
772
- }
773
-
774
- const params = getSearchParamsFromInput(trimmed)
775
- if (params) {
776
- const error = params.get("error")
777
- if (error) {
778
- const description = params.get("error_description")
779
- throw new Error(description ? `${error}: ${description}` : error)
780
- }
781
-
782
- const state = params.get("state")
783
- if (expectedState && !state) {
784
- throw new Error("OAuth state missing from redirect URL")
785
- }
786
- if (expectedState && state !== expectedState) {
787
- throw new Error("OAuth state mismatch - potential CSRF attack")
788
- }
789
-
790
- const code = params.get("code")
791
- if (code) {
792
- const iss = params.get("iss")
793
- return { code, ...(iss !== null ? { iss } : {}) }
794
- }
795
- }
796
-
797
- if (/^[A-Za-z0-9._~+/=-]+$/.test(trimmed)) {
798
- return { code: trimmed }
799
- }
800
-
801
- throw new Error("Could not find an OAuth authorization code in the provided input")
802
- }
803
-
804
- /**
805
- * Extract an OAuth authorization code from either a raw code, a query string,
806
- * or the full localhost redirect URL copied from the browser address bar.
807
- */
808
- export function parseAuthorizationCodeInput(input: string, expectedState?: string): string {
809
- return parseAuthorizationRedirectInput(input, expectedState).code
810
- }
811
-
812
- type AuthorizationResponse = {
813
- input: AuthorizationCodeInput
814
- source: "callback" | "manual"
815
- }
816
-
817
- function waitForManualCompletionCancellation(signal: AbortSignal): Promise<AuthorizationCodeInput> {
818
- return new Promise((_, reject) => {
819
- const rejectFromSignal = () => {
820
- reject(signal.reason instanceof Error ? signal.reason : new Error("Authorization cancelled"))
821
- }
822
- if (signal.aborted) {
823
- rejectFromSignal()
824
- return
825
- }
826
- signal.addEventListener("abort", rejectFromSignal, { once: true })
827
- })
828
- }
829
-
830
- /**
831
- * Wait for either the localhost callback or a manually pasted redirect URL.
832
- * The manual input prompt is dismissed as soon as either path finishes.
833
- */
834
- export async function waitForAuthorizationResponse(
835
- callbackPromise: Promise<AuthorizationCodeInput>,
836
- authorizationUrl: string,
837
- expectedState: string,
838
- onAuthorizationInput?: AuthenticateOptions["onAuthorizationInput"],
839
- signal?: AbortSignal,
840
- ): Promise<AuthorizationResponse> {
841
- if (!onAuthorizationInput) {
842
- return {
843
- input: await abortable(callbackPromise, signal),
844
- source: "callback",
845
- }
846
- }
847
-
848
- const inputController = new AbortController()
849
- try {
850
- const response = await abortable(Promise.race([
851
- callbackPromise.then((input) => ({ input, source: "callback" as const })),
852
- onAuthorizationInput(authorizationUrl, inputController.signal).then((input) => ({
853
- input,
854
- source: "manual" as const,
855
- })),
856
- ]), signal)
857
-
858
- if (response.source === "callback") return response
859
- if (!response.input?.trim()) throw new Error("OAuth authentication cancelled")
860
- if (!getSearchParamsFromInput(response.input.trim())) {
861
- throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
862
- }
863
- return {
864
- input: parseAuthorizationRedirectInput(response.input, expectedState),
865
- source: "manual",
866
- }
867
- } finally {
868
- inputController.abort()
869
- }
870
- }
871
-
872
- /**
873
- * Complete OAuth authentication from manual user input.
874
- */
875
- export async function completeAuthFromInput(
876
- serverName: string,
877
- input: string,
878
- options: AuthenticateOptions = {},
879
- ): Promise<AuthStatus> {
880
- const runtime = getRuntime(options)
881
- const runtimeState = getRuntimeState(runtime)
882
- const fallbackAuthStorageOptions = options.authStorageOptions ?? {}
883
- const signal = combineAbortSignals(runtime.signal, options.signal)
884
- throwIfAborted(signal)
885
- const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
886
- const oauthState = runtimeState.pendingAuthStates.get(key)
887
- const pendingAuth = runtimeState.pendingAuths.get(key)
888
- throwIfAborted(signal)
889
- if (pendingAuth?.manualRedirect && !getSearchParamsFromInput(input.trim())) {
890
- throw new Error("Paste the full OAuth callback URL, including its code and state parameters")
891
- }
892
- const parsed = parseAuthorizationRedirectInput(input, oauthState)
893
- return completeAuth(serverName, parsed, options)
894
- }
895
-
896
- /**
897
- * Complete OAuth authentication with the authorization code.
898
- */
899
- export async function completeAuth(
900
- serverName: string,
901
- authorizationCode: string | AuthorizationCodeInput,
902
- options: AuthenticateOptions = {},
903
- ): Promise<AuthStatus> {
904
- const runtime = getRuntime(options)
905
- const runtimeState = getRuntimeState(runtime)
906
- const { code, iss } = typeof authorizationCode === "string"
907
- ? { code: authorizationCode, iss: undefined }
908
- : authorizationCode
909
- const fallbackAuthStorageOptions = options.authStorageOptions ?? {}
910
- const signal = combineAbortSignals(runtime.signal, options.signal)
911
- throwIfAborted(signal)
912
- const key = getPendingAuthKey(serverName, fallbackAuthStorageOptions)
913
- const pendingAuth = runtimeState.pendingAuths.get(key)
914
- const authStorageOptions = pendingAuth?.authStorageOptions ?? fallbackAuthStorageOptions
915
- if (!pendingAuth) {
916
- throw new Error(`No pending OAuth flow for server: ${serverName}`)
917
- }
918
- pendingAuth.authority()
919
-
920
- const oauthState = runtimeState.pendingAuthStates.get(key)
921
- throwIfAborted(signal)
922
-
923
- let keepPendingForRetry = false
924
- let caughtError: unknown
925
- try {
926
- const fetchFn = createOAuthFetch(pendingAuth.serverUrl, pendingAuth.getHeaders, signal)
927
- pendingAuth.authProvider.setAuthFetch(fetchFn)
928
- const discoveryState = await pendingAuth.authProvider.discoveryState()
929
- pendingAuth.authority()
930
- const metadata = discoveryState?.authorizationServerMetadata
931
- const expectedIssuer = metadata?.issuer ?? discoveryState?.authorizationServerUrl
932
- const requiresIssuer = (metadata as { authorization_response_iss_parameter_supported?: unknown } | undefined)
933
- ?.authorization_response_iss_parameter_supported === true
934
- if (expectedIssuer !== undefined && iss === undefined && requiresIssuer) {
935
- keepPendingForRetry = true
936
- throw new Error(
937
- `The authorization server for ${serverName} requires the RFC 9207 "iss" parameter. ` +
938
- "Paste the full redirect URL from the browser address bar (not just the authorization code).",
939
- )
940
- }
941
- if (expectedIssuer !== undefined && iss !== undefined && iss !== expectedIssuer) {
942
- throw new Error(`The OAuth authorization response issuer does not match the discovered issuer for ${serverName}.`)
943
- }
944
-
945
- const result = await abortable(runSdkAuth(pendingAuth.authProvider, {
946
- serverUrl: pendingAuth.serverUrl,
947
- authorizationCode: code,
948
- ...(iss !== undefined ? { iss } : {}),
949
- ...pendingAuth.discovery,
950
- fetchFn,
951
- }), signal)
952
- pendingAuth.authority()
953
- throwIfAborted(signal)
954
- if (result !== "AUTHORIZED") {
955
- throw new UnauthorizedError("Failed to authorize")
956
- }
957
- return "authenticated"
958
- } catch (error) {
959
- caughtError = error
960
- throw error
961
- } finally {
962
- if (!keepPendingForRetry) {
963
- try {
964
- await clearPendingAuthAndReleaseIfIdle(runtime, serverName, oauthState, authStorageOptions)
965
- } catch (cleanupError) {
966
- if (caughtError !== undefined) {
967
- throw new AggregateError([caughtError, cleanupError], "OAuth completion cleanup failed")
968
- }
969
- throw cleanupError
970
- }
971
- if (caughtError === undefined) pendingAuth.authority()
972
- }
973
- }
974
- }
975
-
976
- /**
977
- * Perform the complete OAuth authentication flow for a server.
978
- *
979
- * @param serverName - The name of the MCP server
980
- * @param serverUrl - The URL of the MCP server
981
- * @param definition - The server definition (optional)
982
- * @returns The final auth status
983
- */
984
- export async function authenticate(
985
- serverName: string,
986
- serverUrl: string,
987
- definition?: ServerEntry,
988
- options: AuthenticateOptions = {},
989
- ): Promise<AuthStatus> {
990
- if (isServerDisabled(definition)) throw new Error(`MCP server "${serverName}" is disabled`)
991
- const authority = captureOAuthAuthority(serverName)
992
- const runtime = getRuntime(options)
993
- const runtimeState = getRuntimeState(runtime)
994
- const authStorageOptions = options.authStorageOptions ?? {}
995
- const signal = combineAbortSignals(runtime.signal, options.signal)
996
- throwIfAborted(signal)
997
- const authKey = JSON.stringify([serverName, serverUrl, ...getAuthStorageIdentity(authStorageOptions)])
998
- const inFlight = runtimeState.pendingAuthentications.get(authKey)
999
- if (inFlight) {
1000
- try {
1001
- inFlight.authority()
1002
- return inFlight.promise
1003
- } catch {
1004
- if (runtimeState.pendingAuthentications.get(authKey) === inFlight) {
1005
- runtimeState.pendingAuthentications.delete(authKey)
1006
- }
1007
- }
1008
- }
1009
-
1010
- const operation = (async (): Promise<AuthStatus> => {
1011
- const { authorizationUrl } = await startAuth(serverName, serverUrl, definition, {
1012
- ...options,
1013
- ...(signal ? { signal } : {}),
1014
- runtime,
1015
- }, authority)
1016
- authority()
1017
-
1018
- if (!authorizationUrl) {
1019
- return "authenticated"
1020
- }
1021
-
1022
- let oauthState: string | undefined
1023
- try {
1024
- // Get the state that was already generated and stored in startAuth().
1025
- // Keep this lookup and its abort check inside the cleanup boundary because
1026
- // startAuth already owns the pending flow at this point.
1027
- oauthState = runtimeState.pendingAuthStates.get(getPendingAuthKey(serverName, authStorageOptions))
1028
- throwIfAborted(signal)
1029
- if (!oauthState) {
1030
- throw new Error("OAuth state not found - this should not happen")
1031
- }
1032
-
1033
- const pendingAuth = runtimeState.pendingAuths.get(getPendingAuthKey(serverName, authStorageOptions))
1034
- if (!pendingAuth) {
1035
- throw new Error(`No pending OAuth flow for server: ${serverName}`)
1036
- }
1037
- if (pendingAuth.manualRedirect && !options.onAuthorizationInput) {
1038
- throw new Error(
1039
- `OAuth for ${serverName} uses a remote redirect URI. Complete it with auth-start/auth-complete or /mcp-auth.`,
1040
- )
1041
- }
1042
-
1043
- // Register the localhost callback before opening the browser. Remote
1044
- // pre-registered callbacks are completed by pasting their full URL.
1045
- const callbackPromise: Promise<AuthorizationCodeInput> = pendingAuth.manualRedirect
1046
- ? waitForManualCompletionCancellation(pendingAuth.manualCompletionController!.signal)
1047
- : waitForCallback(oauthState)
1048
- void callbackPromise.catch(() => {})
1049
-
1050
- // Open browser. Always surface the URL first so remote/headless users can copy it
1051
- // even when the OS browser handoff is unavailable or invisible.
1052
- if (options.onAuthorizationUrl) {
1053
- await abortable(Promise.resolve(options.onAuthorizationUrl(authorizationUrl)), signal)
1054
- } else {
1055
- console.log(`MCP Auth: Open this URL to authenticate ${serverName}:\n${authorizationUrl}`)
1056
- }
1057
- try {
1058
- await abortable(Promise.resolve(
1059
- options.openAuthorizationUrl
1060
- ? options.openAuthorizationUrl(authorizationUrl)
1061
- : open(authorizationUrl),
1062
- ), signal)
1063
- } catch (error) {
1064
- if (isAbortError(error, signal)) throw error
1065
- console.warn(`MCP Auth: Failed to open browser for ${serverName}; waiting for manual callback`, { error })
1066
- }
1067
-
1068
- const authorizationResponse = await waitForAuthorizationResponse(
1069
- callbackPromise,
1070
- authorizationUrl,
1071
- oauthState,
1072
- options.onAuthorizationInput,
1073
- signal,
1074
- )
1075
- if (authorizationResponse.source === "manual") {
1076
- cancelPendingCallback(oauthState)
1077
- }
1078
-
1079
- // The callback server accepted only the flow-local reserved state. Manual
1080
- // input is checked against the same state before token exchange.
1081
- throwIfAborted(signal)
1082
-
1083
- return await completeAuth(serverName, authorizationResponse.input, {
1084
- ...options,
1085
- ...(signal ? { signal } : {}),
1086
- runtime,
1087
- })
1088
- } catch (error) {
1089
- if (oauthState) cancelPendingCallback(oauthState)
1090
- try {
1091
- await clearPendingAuthAndReleaseIfIdle(runtime, serverName, oauthState, authStorageOptions)
1092
- } catch (cleanupError) {
1093
- throw new AggregateError([error, cleanupError], "OAuth cancellation cleanup failed")
1094
- }
1095
- throw error
1096
- }
1097
- })()
1098
-
1099
- const pendingAuthentication = { promise: operation, authority }
1100
- runtimeState.pendingAuthentications.set(authKey, pendingAuthentication)
1101
-
1102
- try {
1103
- const result = await operation
1104
- authority()
1105
- return result
1106
- } finally {
1107
- if (runtimeState.pendingAuthentications.get(authKey) === pendingAuthentication) {
1108
- runtimeState.pendingAuthentications.delete(authKey)
1109
- }
1110
- }
1111
- }
1112
-
1113
- /**
1114
- * Get a valid access token for a server, refreshing if necessary.
1115
- *
1116
- * @param serverName - The name of the MCP server
1117
- * @param serverUrl - The URL of the MCP server
1118
- * @returns The valid tokens or null if not authenticated
1119
- */
1120
- export async function getValidToken(
1121
- serverName: string,
1122
- serverUrl: string,
1123
- options: AuthenticateOptions = {},
1124
- ): Promise<StoredTokens | null> {
1125
- const authority = captureOAuthAuthority(serverName)
1126
- const runtime = getRuntime(options)
1127
- const authStorageOptions = options.authStorageOptions ?? {}
1128
- const signal = combineAbortSignals(runtime.signal, options.signal)
1129
- throwIfAborted(signal)
1130
- const entry = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
1131
- if (!hasOAuthAuthority(authority)) return null
1132
- throwIfAborted(signal)
1133
- if (!entry?.tokens) {
1134
- return null
1135
- }
1136
-
1137
- const expired = entry.tokens.expiresAt
1138
- ? entry.tokens.expiresAt < Date.now() / 1000
1139
- : false
1140
- if (expired === false) {
1141
- authority()
1142
- return entry.tokens
1143
- }
1144
-
1145
- if (expired === true && entry.tokens.refreshToken) {
1146
- console.log(`MCP Auth: Token expired for ${serverName}, attempting refresh`)
1147
-
1148
- try {
1149
- const config = options.definition ? extractOAuthConfig(options.definition) : {}
1150
- const fetchFn = createOAuthFetch(serverUrl, pluginAwareOAuthHeaders(options.definition), signal)
1151
- authority()
1152
- const authProvider = new McpOAuthProvider(serverName, serverUrl, config, {
1153
- onRedirect: async () => {},
1154
- }, authStorageOptions, runtime.signal, undefined, authority)
1155
-
1156
- try {
1157
- authProvider.setAuthFetch(fetchFn)
1158
- const clientInfo = await authProvider.clientInformation()
1159
- authority()
1160
- throwIfAborted(signal)
1161
- if (!clientInfo) {
1162
- console.log(`MCP Auth: No client info for refresh for ${serverName}`)
1163
- return null
1164
- }
1165
-
1166
- const discovery = applyOAuthConfig(await probeAuthDiscovery(serverUrl, options.definition, signal), config)
1167
- authority()
1168
- throwIfAborted(signal)
1169
- const result = await abortable(runSdkAuth(authProvider, {
1170
- serverUrl,
1171
- ...discovery,
1172
- ...(options.skipIssuerMetadataValidation === true ? { skipIssuerMetadataValidation: true } : {}),
1173
- fetchFn,
1174
- }), signal)
1175
- authority()
1176
- throwIfAborted(signal)
1177
- if (result !== "AUTHORIZED") {
1178
- return null
1179
- }
1180
- const refreshed = await getAuthForUrl(serverName, serverUrl, authStorageOptions)
1181
- authority()
1182
- throwIfAborted(signal)
1183
- return refreshed?.tokens ?? null
1184
- } finally {
1185
- authProvider.deactivate()
1186
- }
1187
- } catch (error) {
1188
- if (isAbortError(error, signal) || error instanceof OAuthCredentialStoreError) throw error
1189
- console.error(`MCP Auth: Token refresh failed for ${serverName}`)
1190
- return null
1191
- }
1192
- }
1193
-
1194
- authority()
1195
- return null
1196
- }
1197
-
1198
- /**
1199
- * Check the authentication status for a server.
1200
- *
1201
- * @param serverName - The name of the MCP server
1202
- * @returns The current auth status
1203
- */
1204
- export async function getAuthStatus(serverName: string, options: AuthenticateOptions = {}): Promise<AuthStatus> {
1205
- getRuntime(options)
1206
- const authStorageOptions = options.authStorageOptions ?? {}
1207
- const entry = getAuthEntry(serverName, authStorageOptions)
1208
- if (!entry?.tokens) return "not_authenticated"
1209
- return entry.tokens.expiresAt && entry.tokens.expiresAt < Date.now() / 1000 ? "expired" : "authenticated"
1210
- }
1211
-
1212
- /**
1213
- * Remove all OAuth credentials for a server.
1214
- *
1215
- * @param serverName - The name of the MCP server
1216
- */
1217
- export async function removeAuth(serverName: string, options: AuthenticateOptions = {}): Promise<void> {
1218
- const runtime = getRuntime(options)
1219
- const signal = combineAbortSignals(runtime.signal, options.signal)
1220
- throwIfAborted(signal)
1221
- const authStorageOptions = options.authStorageOptions ?? {}
1222
- const releaseRevocation = beginOAuthRevocation(serverName)
1223
- try {
1224
- detachPendingAuthsForServer(serverName, new Error("Authorization cancelled by logout"))
1225
- const storedOAuthState = getOAuthState(serverName, authStorageOptions)
1226
- if (storedOAuthState) cancelPendingCallback(storedOAuthState)
1227
- await stopCallbackServerIfIdle()
1228
- throwIfAborted(signal)
1229
- clearAllCredentials(serverName, authStorageOptions)
1230
- console.log(`MCP Auth: Removed credentials for ${serverName}`)
1231
- } finally {
1232
- releaseRevocation()
1233
- }
1234
- }
1235
-
1236
- /**
1237
- * Check if OAuth is supported for a server configuration.
1238
- * OAuth is supported for HTTP servers unless explicitly disabled.
1239
- *
1240
- * @param definition - The server definition
1241
- * @returns True if OAuth is supported
1242
- */
1243
- export function supportsOAuth(definition: ServerEntry): boolean {
1244
- // OAuth requires a URL
1245
- if (!definition.url) return false
1246
-
1247
- // Explicitly disabled via auth: false or oauth: false
1248
- if (definition.auth === false) return false
1249
- if (definition.oauth === false) return false
1250
- if (definition.auth === "oauth") return true
1251
-
1252
- // Configured custom headers take precedence over implicit OAuth auto-detection.
1253
- if (definition.headers && Object.keys(definition.headers).length > 0) return false
1254
-
1255
- // OAuth is enabled when auth is not specified (auto-detect)
1256
- return definition.auth === undefined
1257
- }
1258
-
1259
- /**
1260
- * Initialize the OAuth system on startup.
1261
- * OAuth callback binding is lazy and starts from startAuth() only.
1262
- */
1263
- export async function initializeOAuth(
1264
- runtimeOrSignal?: McpOAuthRuntime | AbortSignal,
1265
- ): Promise<McpOAuthRuntime> {
1266
- if (runtimeOrSignal && "signal" in runtimeOrSignal) {
1267
- runtimeOrSignal.signal.throwIfAborted()
1268
- activeRuntimes.add(runtimeOrSignal)
1269
- return runtimeOrSignal
1270
- }
1271
-
1272
- await shutdownOAuth(legacyRuntime)
1273
- legacyRuntime = createOAuthRuntime(runtimeOrSignal as AbortSignal | undefined)
1274
- return legacyRuntime
1275
- }
1276
-
1277
- /**
1278
- * Shutdown one OAuth runtime. The callback server remains process-shared while
1279
- * another runtime has pending/reserved callback state or is still active.
1280
- */
1281
- export async function shutdownOAuth(runtime: McpOAuthRuntime = legacyRuntime): Promise<void> {
1282
- const state = getRuntimeState(runtime)
1283
- if (state.controller.signal.aborted) return
1284
- state.generation += 1
1285
- state.controller.abort(new Error("OAuth runtime stopped"))
1286
- for (const callbackState of Array.from(state.pendingAuthStates.values())) cancelPendingCallback(callbackState)
1287
- for (const pendingAuth of Array.from(state.pendingAuths.values())) {
1288
- await clearPendingAuth(runtime, pendingAuth.serverName, undefined, pendingAuth.authStorageOptions)
1289
- }
1290
- state.pendingAuthentications.clear()
1291
- activeRuntimes.delete(runtime)
1292
-
1293
- if (activeRuntimes.size === 0) {
1294
- await stopCallbackServer()
1295
- }
1296
- }