mixdog 1.2.6 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -33,11 +33,17 @@ import {
33
33
  import { createLazyOutputValidator } from './lazy-output-validator.mjs';
34
34
  import { appendMcpStderrTail, createMcpTransport, hasConfiguredMcpAuth } from './client-transport.mjs';
35
35
  import { listAllMcpTools, MCP_CONNECT_CANCELLED, runBoundedHandshake } from './client-handshake.mjs';
36
+ import { rememberServerConnector } from './server-connectors.mjs';
37
+ import { mcpOAuthClientFor } from './oauth-client.mjs';
38
+ import { googleMcpService, withGoogleMcp } from '../../../connectors/google-mcp.mjs';
36
39
  import {
37
40
  clearMcpOAuth,
41
+ commitMcpOAuthSwitch,
38
42
  hasStoredMcpOAuthTokens,
39
43
  isMcpOAuthReauthenticationRequired,
40
44
  MCP_OAUTH_CALLBACK_NOT_PENDING,
45
+ startMcpOAuthSwitch,
46
+ storedMcpOAuthIdentity,
41
47
  } from './oauth.mjs';
42
48
  export { probeMcpServerAuth } from './oauth-probe.mjs';
43
49
 
@@ -67,6 +73,14 @@ const _connectAbortGenerations = new Map();
67
73
  const _pendingConnects = new Set();
68
74
  // scopeId -> root path advertised by that scope's scope-following servers.
69
75
  const _scopeRoots = new Map();
76
+ // One stdio process per server config, shared by every scope that connects
77
+ // the same server with the same resolved config: share key -> connection
78
+ // { key, client, transport, liveness, stderrTail, warnings, tools,
79
+ // capabilities, instructions, users: Set<registryKey> }. Every session
80
+ // runtime has its own scope, so per-scope processes multiplied with the
81
+ // number of retained sessions. A scope's close releases only its own use; the
82
+ // process tree is shut down when its last user releases it.
83
+ const _sharedStdio = new Map();
70
84
  let mcpSdkPromise = null;
71
85
  // Memo for mcpToolHasField(name, field) — keyed by `${scopeId}|${toolName}|${field}`.
72
86
  // The lookup (regex parse + servers Map get + tools.find + schema property
@@ -99,6 +113,24 @@ function scopedServerEntries(scopeId) {
99
113
  const normalized = normalizeMcpScopeId(scopeId);
100
114
  return [...servers.entries()].filter(([, server]) => server?.scopeId === normalized);
101
115
  }
116
+ // Share key of a stdio server, or null when its connection must stay private
117
+ // to one scope: remote transports, and servers whose advertised roots follow
118
+ // their scope's cwd.
119
+ function sharedStdioKey(name, cfg) {
120
+ if (cfg?._mixdogRootsFollowScope === true) return null;
121
+ try {
122
+ if (resolveMcpTransportKind(cfg) !== 'stdio') return null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ return `${String(name || '')}\u0000${JSON.stringify(cfg)}`;
127
+ }
128
+ // A connection that failed a call is never handed to a later connect; scopes
129
+ // still holding it keep it until they release it themselves.
130
+ function retireSharedConnection(server) {
131
+ const shared = server?.shared;
132
+ if (shared && _sharedStdio.get(shared.key) === shared) _sharedStdio.delete(shared.key);
133
+ }
102
134
  function currentConnectAbortGeneration(scopeId) {
103
135
  return _connectAbortGenerations.get(normalizeMcpScopeId(scopeId)) || 0;
104
136
  }
@@ -411,6 +443,7 @@ async function reconnectMcpServer(scopeId, serverName, failedServer) {
411
443
  if (!current || genAtStart !== currentConnectAbortGeneration(scopeId) || current.liveness?.closing) {
412
444
  throw new Error(`MCP server "${serverName}" was disconnected`);
413
445
  }
446
+ retireSharedConnection(current);
414
447
  await replaceServerConnection(scopeId, serverName, current, failedServer.cfg, genAtStart);
415
448
  });
416
449
  const replacement = servers.get(registryKey);
@@ -1223,22 +1256,52 @@ export function getMcpServerOAuthState(cfg) {
1223
1256
  return null;
1224
1257
  }
1225
1258
  if (!url || hasConfiguredMcpAuth(cfg)) return null;
1226
- return { signedIn: hasStoredMcpOAuthTokens(url) };
1259
+ const signedIn = hasStoredMcpOAuthTokens(url);
1260
+ if (!signedIn) return { signedIn };
1261
+ // Who it is signed in as (never guessed: empty when the server named nobody)
1262
+ // and whether 계정 바꾸기 can make the provider show its account choice.
1263
+ const who = storedMcpOAuthIdentity(url);
1264
+ return {
1265
+ signedIn,
1266
+ account: typeof who?.account === 'string' ? who.account : '',
1267
+ workspace: typeof who?.workspace === 'string' ? who.workspace : '',
1268
+ forcedAccountChoice: who?.forcedChoice === true,
1269
+ };
1270
+ }
1271
+ /** 계정 바꾸기 of an HTTP/SSE server: a browser sign-in beside the stored one
1272
+ * (oauth.mjs startMcpOAuthSwitch); `launch` opens the authorization URL. */
1273
+ export function startMcpServerOAuthSwitch(cfg, launch, serverName) {
1274
+ const url = resolveMcpOAuthUrl(cfg);
1275
+ if (!url) throw new Error(`MCP server "${serverName}" does not use OAuth sign-in`);
1276
+ return startMcpOAuthSwitch({
1277
+ url,
1278
+ launch,
1279
+ serverName,
1280
+ oauthClient: mcpOAuthClientFor(cfg, url),
1281
+ ...(googleMcpService(url) ? { fetchImpl: withGoogleMcp() } : {}),
1282
+ });
1283
+ }
1284
+ /** Store a switch's sign-in for the server's URL; resolves its rollback. */
1285
+ export function commitMcpServerOAuthSwitch(cfg, signIn) {
1286
+ return commitMcpOAuthSwitch({ url: resolveMcpOAuthUrl(cfg), signIn });
1227
1287
  }
1228
1288
  /**
1229
1289
  * Log out of an HTTP/SSE server URL: delete the OAuth tokens and client
1230
1290
  * registration stored for it, shared by every server configured with that
1231
- * exact URL, and drop them from live connections.
1291
+ * exact URL, and drop them from live connections. `options.revoke` also
1292
+ * revokes the grant where the authorization server documents how; resolves
1293
+ * `{ revoked }`.
1232
1294
  */
1233
- export async function clearMcpServerOAuth(cfg) {
1295
+ export async function clearMcpServerOAuth(cfg, options = {}) {
1234
1296
  let url;
1235
1297
  try {
1236
1298
  url = resolveMcpOAuthUrl(cfg);
1237
1299
  } catch {
1238
1300
  // A config without a valid remote URL never connected, so never logged in.
1239
- return;
1301
+ return { revoked: false };
1240
1302
  }
1241
- if (url) await clearMcpOAuth({ url });
1303
+ if (url) return clearMcpOAuth({ url, revoke: options.revoke === true });
1304
+ return { revoked: false };
1242
1305
  }
1243
1306
  /**
1244
1307
  * Close a single server: for stdio transports first shut down the full child
@@ -1248,6 +1311,13 @@ export async function clearMcpServerOAuth(cfg) {
1248
1311
  * only kills the direct child and discards the pid we need to walk the tree.
1249
1312
  */
1250
1313
  async function _closeServer(server) {
1314
+ // A shared stdio connection closes only when its last scope releases it.
1315
+ const shared = server?.shared;
1316
+ if (shared) {
1317
+ shared.users.delete(server.registryKey);
1318
+ if (shared.users.size > 0) return;
1319
+ retireSharedConnection(server);
1320
+ }
1251
1321
  // A deliberate close is not a crash: suppress the unexpected-close report.
1252
1322
  if (server?.liveness) server.liveness.closing = true;
1253
1323
  const transport = server?.transport;
@@ -1266,6 +1336,19 @@ async function _closeServer(server) {
1266
1336
  /* ignore */
1267
1337
  }
1268
1338
  }
1339
+ // Register `name` in `scopeId` on the live shared connection for `shareKey`,
1340
+ // when there is one. Returns whether it did.
1341
+ function adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart) {
1342
+ const shared = shareKey ? _sharedStdio.get(shareKey) : null;
1343
+ if (!shared || shared.liveness.alive === false || shared.liveness.closing) return false;
1344
+ const registryKey = mcpServerRegistryKey(scopeId, name);
1345
+ const { key: _key, users, ...connection } = shared;
1346
+ users.add(registryKey);
1347
+ rememberServerConnector(name, cfg);
1348
+ servers.set(registryKey, { scopeId, registryKey, name, ...connection, cfg, shared, generation: genAtStart });
1349
+ _invalidateMcpToolFieldMemo();
1350
+ return true;
1351
+ }
1269
1352
  // Connects whose handshake has not finished yet: disconnectAll() must be able
1270
1353
  // to see (and tear down) their transports, because `servers` only lists fully
1271
1354
  // handshaken entries. Generation token aborts a connect that outlives a
@@ -1285,6 +1368,8 @@ async function connectServer(
1285
1368
  // abort before creating a transport/child at all.
1286
1369
  throw new Error(`MCP server "${name}" connect aborted by shutdown`);
1287
1370
  }
1371
+ const shareKey = sharedStdioKey(name, cfg);
1372
+ if (adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart)) return;
1288
1373
  const client = createMcpClient(sdk, name, cfg, scopeId);
1289
1374
  const { transport, autoDetectAdvert, stderrTail, warnings } = createMcpTransport({
1290
1375
  name,
@@ -1329,26 +1414,42 @@ async function connectServer(
1329
1414
  }
1330
1415
  throw new Error(`MCP server "${name}" connect aborted by shutdown`);
1331
1416
  }
1417
+ // Another scope finished connecting the same server meanwhile: use its
1418
+ // process and release this one.
1419
+ if (adoptSharedConnection(shareKey, name, cfg, scopeId, genAtStart)) {
1420
+ await _closeServer({ client, transport, liveness });
1421
+ return;
1422
+ }
1332
1423
  const tools = mcpServerTools(name, capabilities, toolsResult.tools, cfg);
1333
1424
  const registryKey = mcpServerRegistryKey(scopeId, name);
1334
- servers.set(registryKey, {
1335
- scopeId,
1336
- registryKey,
1337
- name,
1425
+ const connection = {
1338
1426
  client,
1339
1427
  transport,
1340
1428
  tools,
1341
- cfg,
1342
1429
  instructions,
1343
1430
  capabilities,
1344
1431
  liveness,
1345
1432
  stderrTail,
1346
1433
  warnings: warnings || [],
1434
+ };
1435
+ const shared = shareKey ? { ...connection, key: shareKey, users: new Set([registryKey]) } : null;
1436
+ if (shared) _sharedStdio.set(shareKey, shared);
1437
+ rememberServerConnector(name, cfg);
1438
+ servers.set(registryKey, {
1439
+ scopeId,
1440
+ registryKey,
1441
+ name,
1442
+ ...connection,
1443
+ cfg,
1444
+ shared,
1347
1445
  generation: genAtStart,
1348
1446
  });
1349
1447
  if (capabilities?.tools?.listChanged && sdk.ToolListChangedNotificationSchema) {
1448
+ // A shared connection refreshes the tools of every scope using it.
1350
1449
  client.setNotificationHandler(sdk.ToolListChangedNotificationSchema, () =>
1351
- refreshServerTools(registryKey, client, name, cfg, capabilities)
1450
+ Promise.all(
1451
+ [...(shared?.users ?? [registryKey])].map((key) => refreshServerTools(key, client, name, cfg, capabilities))
1452
+ )
1352
1453
  );
1353
1454
  }
1354
1455
  _invalidateMcpToolFieldMemo();
@@ -1517,6 +1618,8 @@ async function runToolRefresh(registryKey, live, name, cfg, capabilities) {
1517
1618
  }
1518
1619
  if (servers.get(registryKey) !== live) return;
1519
1620
  live.tools = mcpServerTools(name, capabilities, refreshed.tools, cfg);
1621
+ // A scope adopting the connection later starts from the current listing.
1622
+ if (live.shared) live.shared.tools = live.tools;
1520
1623
  _invalidateMcpToolFieldMemo();
1521
1624
  } catch (error) {
1522
1625
  mcpLog(
@@ -1570,6 +1673,7 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
1570
1673
  generation: currentConnectAbortGeneration(normalizedScopeId),
1571
1674
  };
1572
1675
  _knownMcpScopes.add(normalizedScopeId);
1676
+ rememberServerConnector(name, server.cfg);
1573
1677
  servers.set(registryKey, server);
1574
1678
  _invalidateMcpToolFieldMemo();
1575
1679
  return server;
@@ -0,0 +1,97 @@
1
+ // The account side of an MCP OAuth sign-in: which documented authorization
2
+ // parameter makes a provider show its account choice, and who a sign-in
3
+ // belongs to, read only from what the authorization server returned.
4
+ //
5
+ // Parameters are sent only where the provider documents them (keyed by the
6
+ // authorization endpoint's host); everywhere else nothing is added:
7
+ // Google prompt=select_account developers.google.com/identity/protocols/oauth2/web-server#creatingclient
8
+ // GitHub prompt=select_account docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
9
+ // Atlassian (Jira, Confluence, Trello) prompt=consent developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/
10
+ // Linear prompt=consent linear.app/developers/oauth-2-0-authentication
11
+ // Dropbox force_reapprove=true www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize
12
+
13
+ const SELECT_ACCOUNT = Object.freeze({ prompt: 'select_account' });
14
+ // A normal connect shows the chooser only where it is the documented choice.
15
+ const CONNECT_PARAMS = Object.freeze({
16
+ 'accounts.google.com': SELECT_ACCOUNT,
17
+ 'github.com': SELECT_ACCOUNT,
18
+ });
19
+ // 계정 바꾸기: the provider's documented way to force the choice (or the grant).
20
+ const SWITCH_PARAMS = Object.freeze({
21
+ ...CONNECT_PARAMS,
22
+ 'auth.atlassian.com': Object.freeze({ prompt: 'consent' }),
23
+ 'linear.app': Object.freeze({ prompt: 'consent' }),
24
+ 'mcp.linear.app': Object.freeze({ prompt: 'consent' }),
25
+ 'www.dropbox.com': Object.freeze({ force_reapprove: 'true' }),
26
+ });
27
+
28
+ const hostOf = (url) => {
29
+ try {
30
+ return new URL(url).host.toLowerCase();
31
+ } catch {
32
+ return '';
33
+ }
34
+ };
35
+
36
+ /** `authorizationUrl` with the account parameters of `mode` (`connect`,
37
+ * `reauth`, `switch`); a repair (`reauth`) adds nothing. A `prompt` the
38
+ * request already carries (the SDK's `consent` for offline access) is kept. */
39
+ export function withAccountParams(authorizationUrl, mode = 'connect') {
40
+ const target = new URL(authorizationUrl);
41
+ const table = mode === 'switch' ? SWITCH_PARAMS : mode === 'connect' ? CONNECT_PARAMS : {};
42
+ const params = table[target.host.toLowerCase()];
43
+ if (!params) return target;
44
+ for (const [key, value] of Object.entries(params)) {
45
+ if (key !== 'prompt') {
46
+ target.searchParams.set(key, value);
47
+ continue;
48
+ }
49
+ const values = new Set((target.searchParams.get('prompt') || '').split(' ').filter(Boolean));
50
+ values.add(value);
51
+ target.searchParams.set('prompt', [...values].join(' '));
52
+ }
53
+ return target;
54
+ }
55
+
56
+ /** Whether 계정 바꾸기 can make the provider of this authorization endpoint
57
+ * show its choice (otherwise the user switches at the provider first). */
58
+ export function canForceAccountChoice(authorizationEndpoint) {
59
+ return Object.hasOwn(SWITCH_PARAMS, hostOf(authorizationEndpoint));
60
+ }
61
+
62
+ const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : '');
63
+
64
+ function idTokenClaims(idToken) {
65
+ const payload = typeof idToken === 'string' ? idToken.split('.')[1] : '';
66
+ if (!payload) return {};
67
+ try {
68
+ const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
69
+ return claims && typeof claims === 'object' ? claims : {};
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+
75
+ /** `{ account, workspace }` from a token endpoint response: the OIDC id_token
76
+ * claims and the identity fields providers return beside the tokens (Notion's
77
+ * workspace and owner, Slack's team, a `user` / `organization` object). Null
78
+ * when the response names nobody: an identity is never guessed. */
79
+ export function identityFromTokenResponse(body) {
80
+ if (!body || typeof body !== 'object') return null;
81
+ const claims = idTokenClaims(body.id_token);
82
+ const user = body.user && typeof body.user === 'object' ? body.user : {};
83
+ const person = body.owner?.user?.person;
84
+ const account =
85
+ text(claims.email) ||
86
+ text(claims.preferred_username) ||
87
+ text(body.email) ||
88
+ text(user.email) ||
89
+ text(user.login) ||
90
+ text(user.username) ||
91
+ text(person?.email) ||
92
+ text(body.owner?.user?.name);
93
+ const workspace =
94
+ text(body.workspace_name) || text(body.team?.name) || text(body.organization?.name) || text(body.org?.name);
95
+ if (!account && !workspace) return null;
96
+ return { account, workspace };
97
+ }
@@ -0,0 +1,49 @@
1
+ // A pre-registered OAuth client for an MCP server, used as static client
2
+ // information in place of dynamic client registration:
3
+ // - the user's own (custom connector → 고급 설정 → 앱 번호/비밀값): the id
4
+ // in the server config (`oauthClientId`, not a secret), the secret only in
5
+ // the keychain under mcpOAuthClientSecretAccount(url);
6
+ // - else the catalog connector's (connector-catalog.mjs `oauthClient`), with
7
+ // its scopes and the provider's offline-access parameter.
8
+ import { createHash } from 'node:crypto';
9
+ import keychain from '../../../../lib/keychain-cjs.cjs';
10
+ import { connectorById, connectorOfServer } from '../../../shared/connector-catalog.mjs';
11
+ import { GOOGLE_AUTH_PARAMS, googleOAuthClient } from '../../../shared/google-oauth-client.mjs';
12
+ import { normalizeMcpTransportUrl } from './client-config.mjs';
13
+
14
+ const BUILT_IN_CLIENTS = {
15
+ google: () => {
16
+ const client = googleOAuthClient();
17
+ return client && { ...client, authParams: GOOGLE_AUTH_PARAMS };
18
+ },
19
+ };
20
+
21
+ /** The keychain account of the user's client secret for one server URL. */
22
+ export function mcpOAuthClientSecretAccount(url) {
23
+ const key = normalizeMcpTransportUrl(String(url));
24
+ return `mcp-oauth-client.${createHash('sha256').update(key).digest('hex')}`;
25
+ }
26
+
27
+ /** Store (or, with '', delete) the user's client secret for server `url`. */
28
+ export async function saveMcpOAuthClientSecret(url, secret, credentialStore = keychain) {
29
+ const account = mcpOAuthClientSecretAccount(url);
30
+ if (secret) await credentialStore.setSecretAsync(account, secret);
31
+ else await credentialStore.deleteSecretAsync(account);
32
+ }
33
+
34
+ /**
35
+ * `{ clientId, clientSecret?, secretAccount?, scopes?, authParams? }` for
36
+ * server config `cfg` at `url`, or null to register a client dynamically.
37
+ */
38
+ export function mcpOAuthClientFor(cfg, url) {
39
+ const backend = connectorById(connectorOfServer(cfg))?.backend;
40
+ const catalog = backend?.kind === 'oauth' ? backend : null;
41
+ const scopes = catalog?.scopes ? [...catalog.scopes] : undefined;
42
+ const own = typeof cfg?.oauthClientId === 'string' ? cfg.oauthClientId.trim() : '';
43
+ if (own) {
44
+ const authParams = catalog?.oauthClient === 'google' ? GOOGLE_AUTH_PARAMS : undefined;
45
+ return { clientId: own, secretAccount: mcpOAuthClientSecretAccount(url), scopes, authParams };
46
+ }
47
+ const builtIn = catalog?.oauthClient ? BUILT_IN_CLIENTS[catalog.oauthClient]?.() : null;
48
+ return builtIn ? { ...builtIn, scopes } : null;
49
+ }
@@ -1,8 +1,10 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
2
+ import { readFileSync } from 'node:fs';
3
+ import { readFile, rm } from 'node:fs/promises';
3
4
  import { join } from 'node:path';
4
5
  import { withFileLock, writeFileAtomicAsync } from '../../../shared/atomic-file.mjs';
5
6
  import { resolvePluginData } from '../../../shared/plugin-paths.mjs';
7
+ import { canForceAccountChoice } from './oauth-account.mjs';
6
8
  import { reauthenticationRequired } from './oauth-shared.mjs';
7
9
 
8
10
  // Same lease as the provider OAuth refresh (openai-oauth-tokens.mjs): a
@@ -35,6 +37,31 @@ function readRevisionMarker(account) {
35
37
  return readFile(credentialFile(account, 'rev'), 'utf8').catch(() => null);
36
38
  }
37
39
 
40
+ // Who the stored sign-in belongs to, beside the entry (never a credential), so
41
+ // the status lists it without decrypting the keychain:
42
+ // `{ account, workspace, forcedChoice }`; absent when unknown or signed out.
43
+ export function writeIdentityFile(account, creds) {
44
+ const file = credentialFile(account, 'who');
45
+ if (!creds?.tokens) return rm(file, { force: true });
46
+ const identity = creds.identity || {};
47
+ return writeFileAtomicAsync(
48
+ file,
49
+ JSON.stringify({
50
+ account: identity.account || '',
51
+ workspace: identity.workspace || '',
52
+ forcedChoice: canForceAccountChoice(creds.discovery?.authorizationServerMetadata?.authorization_endpoint),
53
+ })
54
+ );
55
+ }
56
+ export function readIdentityFile(account) {
57
+ try {
58
+ const value = JSON.parse(readFileSync(credentialFile(account, 'who'), 'utf8'));
59
+ return value && typeof value === 'object' ? value : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+
38
65
  // The stored value as another process may have just written it, not this
39
66
  // process's cached copy.
40
67
  export function readStoredCredentials(credentialStore, account) {
@@ -57,6 +84,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
57
84
  clientInformation: undefined,
58
85
  redirectUrl: undefined,
59
86
  discovery: undefined,
87
+ identity: undefined,
60
88
  observed: undefined,
61
89
  loaded: undefined,
62
90
  };
@@ -74,10 +102,25 @@ export function createCredentialCache({ url, credentialStore, account }) {
74
102
  clientInformation: creds.clientInformation,
75
103
  redirectUrl: creds.redirectUrl,
76
104
  discovery: creds.discovery,
105
+ identity: creds.identity,
77
106
  } = value);
78
107
  creds.observed = revisionOf(saved);
79
108
  hold();
80
109
  };
110
+ // Whether the stored entry is still the one this process last read or wrote:
111
+ // its revision marker says so without a keychain read (on Windows each read
112
+ // is a DPAPI PowerShell round trip of ~250 ms). Every writer changes the
113
+ // marker under the credential lock before the entry, so under that lock an
114
+ // unchanged marker is an unchanged entry.
115
+ const unchanged = async (expected = creds.observed) =>
116
+ expected !== undefined &&
117
+ expected === creds.observed &&
118
+ (await readRevisionMarker(account)) === markerOf(expected, Boolean(creds.tokens));
119
+ // The stored entry as of now, adopted (skipped when unchanged).
120
+ async function adoptStored() {
121
+ if (await unchanged()) return;
122
+ adopt(await readStoredCredentials(credentialStore, account));
123
+ }
81
124
  // A failed read (locked keychain) is retried by the next request.
82
125
  const ready = () =>
83
126
  (creds.loaded ??= credentialStore
@@ -93,25 +136,31 @@ export function createCredentialCache({ url, credentialStore, account }) {
93
136
  // the newer entry, or its deletion by a log-out, and changes nothing.
94
137
  function update(change, { expected = creds.observed, remove = false } = {}) {
95
138
  return withCredentialLock(account, async () => {
96
- const saved = await readStoredCredentials(credentialStore, account);
97
- if (revisionOf(saved) !== expected) {
98
- adopt(saved);
99
- return false;
139
+ let exists = creds.observed !== null;
140
+ if (!(await unchanged(expected))) {
141
+ const saved = await readStoredCredentials(credentialStore, account);
142
+ if (revisionOf(saved) !== expected) {
143
+ adopt(saved);
144
+ return false;
145
+ }
146
+ exists = Boolean(saved);
100
147
  }
101
148
  change();
102
149
  hold();
103
150
  if (remove) {
104
151
  await writeRevisionMarker(account, null);
105
- if (saved) await credentialStore.deleteSecretAsync(account);
152
+ if (exists) await credentialStore.deleteSecretAsync(account);
153
+ await writeIdentityFile(account, null);
106
154
  creds.observed = null;
107
155
  } else {
108
156
  const revision = randomBytes(16).toString('hex');
109
157
  await writeRevisionMarker(account, revision, Boolean(creds.tokens));
110
- const { tokens, clientInformation, redirectUrl, discovery } = creds;
158
+ const { tokens, clientInformation, redirectUrl, discovery, identity } = creds;
111
159
  await credentialStore.setSecretAsync(
112
160
  account,
113
- JSON.stringify({ url, revision, tokens, clientInformation, redirectUrl, discovery })
161
+ JSON.stringify({ url, revision, tokens, clientInformation, redirectUrl, discovery, identity })
114
162
  );
163
+ await writeIdentityFile(account, creds);
115
164
  creds.observed = revision;
116
165
  }
117
166
  return true;
@@ -133,7 +182,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
133
182
  }
134
183
  // Forget the in-memory credentials; the next request re-reads the keychain.
135
184
  function clear() {
136
- creds.tokens = creds.clientInformation = creds.redirectUrl = creds.discovery = undefined;
185
+ creds.tokens = creds.clientInformation = creds.redirectUrl = creds.discovery = creds.identity = undefined;
137
186
  creds.loaded = creds.observed = undefined;
138
187
  }
139
188
  return {
@@ -141,6 +190,7 @@ export function createCredentialCache({ url, credentialStore, account }) {
141
190
  held,
142
191
  ready,
143
192
  adopt,
193
+ adoptStored,
144
194
  update,
145
195
  observeStoredRevision,
146
196
  clear,
@@ -1,6 +1,7 @@
1
1
  import { createServer } from 'node:http';
2
2
  import { randomBytes } from 'node:crypto';
3
3
  import { normalizeMcpTransportUrl } from './client-config.mjs';
4
+ import { identityFromTokenResponse } from './oauth-account.mjs';
4
5
  import { answerOAuthCallback } from './oauth-callback.mjs';
5
6
  import { anySignal, loginFailed, reauthenticationRequired } from './oauth-shared.mjs';
6
7
 
@@ -49,12 +50,34 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
49
50
  flow.browserOpened = false;
50
51
  flow.launchBrowser = browser || openBrowser;
51
52
  const signal = AbortSignal.any([lifetimeSignal, requestSignal, AbortSignal.timeout(timeoutMs)]);
52
- const safeFetch = (input, init = {}) =>
53
- fetchImpl(normalizeMcpTransportUrl(input), {
53
+ // Who this login's tokens belong to (oauth-account.mjs): the SDK keeps only
54
+ // the token fields, so the token endpoint's answer is read here.
55
+ flow.identity = undefined;
56
+ const safeFetch = async (input, init = {}) => {
57
+ const response = await fetchImpl(normalizeMcpTransportUrl(input), {
54
58
  ...init,
55
59
  redirect: 'error',
56
60
  signal: anySignal(signal, init.signal),
57
61
  });
62
+ if (response.ok && /(^|&)grant_type=/.test(String(init.body ?? ''))) {
63
+ flow.identity = identityFromTokenResponse(
64
+ await response
65
+ .clone()
66
+ .json()
67
+ .catch(() => null)
68
+ );
69
+ }
70
+ return response;
71
+ };
72
+ // The caller's progress hooks (mcp-oauth-login.mjs): the callback arrived,
73
+ // then the tokens are stored.
74
+ const progress = (hook) => {
75
+ try {
76
+ browser?.[hook]?.();
77
+ } catch {
78
+ /* progress reporting never fails the login */
79
+ }
80
+ };
58
81
  let abort;
59
82
  const callback = new Promise((resolve, reject) => {
60
83
  const finish = (code) => (code ? resolve(code) : reject(loginFailed()));
@@ -72,9 +95,10 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
72
95
  // refreshed (and rotated) the tokens this request was rejected with:
73
96
  // decide on the stored credentials, under the cross-process lock.
74
97
  const result = await cache.withLock(async () => {
75
- cache.adopt(await cache.readStored());
98
+ await cache.adoptStored();
76
99
  if (creds.tokens && creds.tokens.access_token !== attemptedToken?.access_token) return 'AUTHORIZED';
77
- if (!interactive && (!creds.tokens?.refresh_token || !creds.clientInformation || !creds.redirectUrl)) {
100
+ const registered = provider.preRegistered || creds.clientInformation;
101
+ if (!interactive && (!creds.tokens?.refresh_token || !registered || !creds.redirectUrl)) {
78
102
  throw reauthenticationRequired();
79
103
  }
80
104
  flow.state = randomBytes(32).toString('base64url');
@@ -85,16 +109,17 @@ export function createLoginFlow({ url, cache, flow, provider, lifetimeSignal, op
85
109
  if (result === 'REDIRECT') {
86
110
  const code = await callback;
87
111
  signal.throwIfAborted();
112
+ progress('onCallback');
88
113
  const exchanged = await cache.withLock(async () => {
89
114
  // The entry this login registered in is gone only after a log-out
90
115
  // since it began, which wins: never recreate it.
91
- const saved = await cache.readStored();
92
- if (!saved) throw loginFailed();
93
- cache.adopt(saved);
116
+ await cache.adoptStored();
117
+ if (creds.observed === null) throw loginFailed();
94
118
  return auth(provider, { ...options, authorizationCode: code });
95
119
  });
96
120
  if (exchanged !== 'AUTHORIZED') throw loginFailed();
97
121
  }
122
+ progress('onAuthorized');
98
123
  signal.throwIfAborted();
99
124
  } catch (error) {
100
125
  // SDK errors may contain authorization server response bodies/secrets.