@reclaimprotocol/agent 0.1.36 → 0.1.37

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 (101) hide show
  1. package/README.md +8 -8
  2. package/lib/authoring/backend.d.ts +12 -1
  3. package/lib/authoring/backend.js +3 -0
  4. package/lib/authoring/tools.js +33 -5
  5. package/lib/authoring/util.d.ts +43 -0
  6. package/lib/authoring/util.js +54 -0
  7. package/lib/cdp/chrome-for-testing.d.ts +14 -0
  8. package/lib/cdp/chrome-for-testing.js +43 -0
  9. package/lib/cdp/geo.d.ts +3 -0
  10. package/lib/cdp/geo.js +27 -15
  11. package/lib/cdp/launcher.d.ts +79 -1
  12. package/lib/cdp/launcher.js +244 -24
  13. package/lib/logger.js +3 -1
  14. package/lib/mcp/instructions.js +3 -3
  15. package/lib/mcp/start.js +17 -2
  16. package/lib/mcp/tools/agent/attach.d.ts +6 -0
  17. package/lib/mcp/tools/agent/attach.js +58 -118
  18. package/lib/mcp/tools/agent/build.js +6 -0
  19. package/lib/mcp/tools/agent/capture.js +33 -10
  20. package/lib/mcp/tools/agent/check-attestor.js +15 -7
  21. package/lib/mcp/tools/agent/draft-tools.d.ts +3 -0
  22. package/lib/mcp/tools/agent/draft-tools.js +170 -0
  23. package/lib/mcp/tools/agent/pod-proof.js +5 -1
  24. package/lib/mcp/tools/agent/proof.d.ts +28 -0
  25. package/lib/mcp/tools/agent/proof.js +72 -0
  26. package/lib/mcp/tools/agent/shared.d.ts +8 -1
  27. package/lib/mcp/tools/agent/shared.js +98 -19
  28. package/lib/mcp/tools/agent/start-integration.d.ts +2 -1
  29. package/lib/mcp/tools/agent/start-integration.js +10 -6
  30. package/lib/mcp/tools/authenticate/authenticate.d.ts +48 -10
  31. package/lib/mcp/tools/authenticate/authenticate.js +212 -116
  32. package/lib/mcp/tools/authenticate/build.d.ts +2 -2
  33. package/lib/mcp/tools/authenticate/build.js +6 -3
  34. package/lib/mcp/tools/authenticate/builder-access.js +5 -2
  35. package/lib/mcp/tools/authenticate/eth-key.d.ts +13 -2
  36. package/lib/mcp/tools/authenticate/eth-key.js +29 -5
  37. package/lib/mcp/tools/authenticate/index.d.ts +1 -1
  38. package/lib/mcp/tools/authenticate/index.js +1 -1
  39. package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +10 -0
  40. package/lib/mcp/tools/authenticate/issue-org-keypair.js +82 -0
  41. package/lib/mcp/tools/authenticate/oauth-config.d.ts +31 -0
  42. package/lib/mcp/tools/authenticate/oauth-config.js +67 -0
  43. package/lib/mcp/tools/authenticate/resolve-owner-key.d.ts +4 -5
  44. package/lib/mcp/tools/authenticate/resolve-owner-key.js +40 -36
  45. package/lib/mcp/tools/authenticate/session-cache.d.ts +25 -1
  46. package/lib/mcp/tools/authenticate/session-cache.js +95 -4
  47. package/lib/mcp/tools/mode/mode.js +6 -6
  48. package/lib/old/login-cdp.js +2 -3
  49. package/lib/proof/attestor.js +2 -3
  50. package/lib/proof/check-attestor.d.ts +12 -2
  51. package/lib/proof/check-attestor.js +40 -10
  52. package/lib/proof/check-matchers.d.ts +36 -0
  53. package/lib/proof/check-matchers.js +161 -0
  54. package/lib/proof/zk-files.d.ts +50 -11
  55. package/lib/proof/zk-files.js +259 -63
  56. package/lib/provider/draft.d.ts +24 -1
  57. package/lib/provider/draft.js +135 -0
  58. package/lib/provider/matcher.js +171 -4
  59. package/package.json +6 -4
  60. package/skill/SKILL.builder.md +7 -2
  61. package/skill/SKILL.common.md +90 -70
  62. package/skill/SKILL.old.md +3 -3
  63. package/skill/builder-mode.md +35 -5
  64. package/skill/old-mode.md +18 -13
  65. package/skill/start-integration.md +48 -33
  66. package/skill/verification-client-integration.md +21 -6
  67. package/src/authoring/backend.ts +12 -1
  68. package/src/authoring/tools.ts +48 -7
  69. package/src/authoring/util.ts +64 -0
  70. package/src/cdp/chrome-for-testing.ts +66 -0
  71. package/src/cdp/geo.ts +36 -21
  72. package/src/cdp/launcher.ts +328 -24
  73. package/src/logger.ts +3 -1
  74. package/src/mcp/instructions.ts +3 -3
  75. package/src/mcp/start.ts +20 -1
  76. package/src/mcp/tools/agent/attach.ts +65 -128
  77. package/src/mcp/tools/agent/build.ts +7 -0
  78. package/src/mcp/tools/agent/capture.ts +54 -11
  79. package/src/mcp/tools/agent/check-attestor.ts +15 -7
  80. package/src/mcp/tools/agent/draft-tools.ts +234 -0
  81. package/src/mcp/tools/agent/pod-proof.ts +5 -1
  82. package/src/mcp/tools/agent/proof.ts +110 -1
  83. package/src/mcp/tools/agent/shared.ts +130 -19
  84. package/src/mcp/tools/agent/start-integration.ts +10 -6
  85. package/src/mcp/tools/authenticate/authenticate.ts +276 -131
  86. package/src/mcp/tools/authenticate/build.ts +6 -3
  87. package/src/mcp/tools/authenticate/builder-access.ts +5 -2
  88. package/src/mcp/tools/authenticate/eth-key.ts +41 -7
  89. package/src/mcp/tools/authenticate/index.ts +1 -1
  90. package/src/mcp/tools/authenticate/issue-org-keypair.ts +106 -0
  91. package/src/mcp/tools/authenticate/oauth-config.ts +85 -0
  92. package/src/mcp/tools/authenticate/resolve-owner-key.ts +42 -39
  93. package/src/mcp/tools/authenticate/session-cache.ts +125 -5
  94. package/src/mcp/tools/mode/mode.ts +6 -6
  95. package/src/old/login-cdp.ts +2 -3
  96. package/src/proof/attestor.ts +2 -3
  97. package/src/proof/check-attestor.ts +55 -12
  98. package/src/proof/check-matchers.ts +227 -0
  99. package/src/proof/zk-files.ts +357 -65
  100. package/src/provider/draft.ts +215 -7
  101. package/src/provider/matcher.ts +237 -4
@@ -1,9 +1,36 @@
1
+ import {
2
+ conflict,
3
+ failedDependency,
4
+ gatewayTimeout,
5
+ preconditionFailed,
6
+ } from '@hapi/boom'
7
+ import assert from 'node:assert'
1
8
  import { type ChildProcess, spawn } from 'node:child_process'
2
- import { existsSync, mkdirSync } from 'node:fs'
3
- import { homedir, platform } from 'node:os'
9
+ import {
10
+ existsSync,
11
+ lstatSync,
12
+ mkdirSync,
13
+ readlinkSync,
14
+ unlinkSync
15
+ } from 'node:fs'
16
+ import { homedir, hostname, platform } from 'node:os'
4
17
  import { join } from 'node:path'
5
18
  import { chromePath, localAppData } from '../consts.ts'
19
+ import { LOGGER } from '../logger.ts'
6
20
  import { reclaimHomeDirPath } from '../paths.ts'
21
+ import { installChromeForTesting } from './chrome-for-testing.ts'
22
+
23
+ /** How long a launched browser gets to answer CDP. A cold first start on a
24
+ * slow disk, or Chrome for Testing's first run, can take well over 10s. */
25
+ export const CDP_READY_TIMEOUT_MS = 30_000
26
+
27
+ /** How much of the browser's stderr to keep for error messages. */
28
+ const STDERR_TAIL_CHARS = 4096
29
+
30
+ /** The files Chrome uses on POSIX to claim a profile. `SingletonLock` is a
31
+ * symlink to `<hostname>-<pid>`; the other two point at the IPC socket and a
32
+ * random cookie, and are only meaningful while that process lives. */
33
+ const SINGLETON_FILES = ['SingletonLock', 'SingletonSocket', 'SingletonCookie']
7
34
 
8
35
  export interface LaunchOptions {
9
36
  /** Port for --remote-debugging-port. */
@@ -13,6 +40,38 @@ export interface LaunchOptions {
13
40
  /** If true, launch without a visible window (--headless=new). Useful
14
41
  * for tests and CI; user-driven flows want it false. */
15
42
  headless?: boolean
43
+ /** How long to wait for CDP to answer. */
44
+ timeoutMs?: number
45
+ }
46
+
47
+ export type LocalBrowserSource = 'installed' | 'override' | 'chrome-for-testing'
48
+
49
+ export interface LocalBrowser {
50
+ binary: string
51
+ source: LocalBrowserSource
52
+ process: ChildProcess
53
+ /** Chrome for Testing was downloaded by this launch. */
54
+ downloaded: boolean
55
+ /** Why the installed browser was passed over for Chrome for Testing. */
56
+ fallbackReason?: string
57
+ }
58
+
59
+ export interface LocalBrowserDependencies {
60
+ platform: () => NodeJS.Platform
61
+ env: () => NodeJS.ProcessEnv
62
+ /** Explicit binary override (`RECLAIM_AGENT_CHROME_PATH`). */
63
+ chromePath: () => string | undefined
64
+ /** Installed Chromium-family browsers, most preferred first. */
65
+ findInstalled: () => string[]
66
+ installChromeForTesting: typeof installChromeForTesting
67
+ }
68
+
69
+ const DEFAULT_DEPENDENCIES: LocalBrowserDependencies = {
70
+ platform,
71
+ env: () => process.env,
72
+ chromePath,
73
+ findInstalled: () => findInstalledBrowsers(),
74
+ installChromeForTesting,
16
75
  }
17
76
 
18
77
  /**
@@ -26,8 +85,8 @@ export interface LaunchOptions {
26
85
  * without admin rights ("Install for me only"). Skipping this was the
27
86
  * #1 first-run failure observed in the field.
28
87
  */
29
- function browserCandidates(): string[] {
30
- switch (platform()) {
88
+ export function browserCandidates(os = platform()): string[] {
89
+ switch (os) {
31
90
  case 'darwin':
32
91
  return [
33
92
  '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
@@ -67,16 +126,20 @@ function browserCandidates(): string[] {
67
126
  }
68
127
  }
69
128
 
129
+ /** The candidates that exist on this machine, in preference order. */
130
+ export function findInstalledBrowsers(candidates = browserCandidates()) {
131
+ return candidates.filter((c) => existsSync(c))
132
+ }
133
+
70
134
  export function defaultProfileDir(): string {
71
135
  return join(reclaimHomeDirPath(), 'chrome-profile')
72
136
  }
73
137
 
74
138
  export function findChromeBinary(): string {
75
139
  const candidates = browserCandidates()
76
- for(const c of candidates) {
77
- if(existsSync(c)) {
78
- return c
79
- }
140
+ const found = chromePath() ?? findInstalledBrowsers(candidates)[0]
141
+ if(found) {
142
+ return found
80
143
  }
81
144
 
82
145
  throw new Error(
@@ -87,11 +150,105 @@ export function findChromeBinary(): string {
87
150
  )
88
151
  }
89
152
 
90
- export function launchChrome(opts: LaunchOptions): ChildProcess {
91
- const binary = chromePath() ?? findChromeBinary()
92
- if(!existsSync(opts.profileDir)) {
93
- mkdirSync(opts.profileDir, { recursive: true })
153
+ /**
154
+ * Why a visible browser cannot open here, or undefined when it can. Only
155
+ * Linux needs a display server named in the environment; an MCP server
156
+ * started outside the desktop session (a sandboxed agent, a service) often
157
+ * has neither variable, and Chrome then exits before CDP ever answers.
158
+ */
159
+ export function missingDisplayReason(
160
+ os: NodeJS.Platform = platform(),
161
+ env: NodeJS.ProcessEnv = process.env,
162
+ ) {
163
+ if(os !== 'linux' || env.WAYLAND_DISPLAY || env.DISPLAY) {
164
+ return undefined
165
+ }
166
+
167
+ return 'No display: neither WAYLAND_DISPLAY nor DISPLAY is set in the '
168
+ + 'Reclaim MCP server\'s environment, so a visible browser cannot open. '
169
+ + 'Start the agent from the desktop session, or pass the variables to '
170
+ + 'the MCP server (for example DISPLAY=:0, or WAYLAND_DISPLAY=wayland-0 '
171
+ + 'with XDG_RUNTIME_DIR), then restart the agent.'
172
+ }
173
+
174
+ /**
175
+ * Launch a visible browser on this machine with the given profile, and wait
176
+ * until it answers CDP.
177
+ *
178
+ * Uses `RECLAIM_AGENT_CHROME_PATH` when set, else the first installed browser
179
+ * (Chrome, then Chromium, then Edge). When none is installed, or that one
180
+ * fails to start, it falls back to Chrome for Testing, downloaded once into
181
+ * `~/.reclaim/browsers`. A missing display fails fast instead: no browser can
182
+ * open without one, so a download would only waste time.
183
+ */
184
+ export async function launchLocalBrowser(
185
+ opts: LaunchOptions,
186
+ dependencies: Partial<LocalBrowserDependencies> = {},
187
+ ): Promise<LocalBrowser> {
188
+ const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies }
189
+ const noDisplay = missingDisplayReason(deps.platform(), deps.env())
190
+ assert(!noDisplay, preconditionFailed(noDisplay))
191
+
192
+ // A live owner would make every launch below fail the same way, so refuse
193
+ // once with the reason instead of trying Chrome for Testing too.
194
+ releaseStaleProfileLock(opts.profileDir, deps.platform())
195
+
196
+ const override = deps.chromePath()
197
+ const preferred = override ?? deps.findInstalled()[0]
198
+ let fallbackReason = 'No Chrome, Chromium, or Edge is installed.'
199
+ if(preferred) {
200
+ try {
201
+ return {
202
+ binary: preferred,
203
+ source: override ? 'override' : 'installed',
204
+ process: await startChrome(preferred, opts),
205
+ downloaded: false,
206
+ }
207
+ } catch(err) {
208
+ fallbackReason = errorMessage(err)
209
+ LOGGER.warn({ binary: preferred, reason: fallbackReason },
210
+ 'local browser failed to start; trying Chrome for Testing')
211
+ }
212
+ }
213
+
214
+ let cft: Awaited<ReturnType<typeof installChromeForTesting>>
215
+ try {
216
+ cft = await deps.installChromeForTesting()
217
+ } catch(err) {
218
+ throw failedDependency(
219
+ `${fallbackReason}\nChrome for Testing could not be installed into `
220
+ + `~/.reclaim/browsers: ${errorMessage(err)}`,
221
+ )
222
+ }
223
+
224
+ try {
225
+ return {
226
+ binary: cft.executablePath,
227
+ source: 'chrome-for-testing',
228
+ process: await startChrome(cft.executablePath, opts),
229
+ downloaded: cft.downloaded,
230
+ fallbackReason,
231
+ }
232
+ } catch(err) {
233
+ throw failedDependency(
234
+ `${fallbackReason}\nChrome for Testing (${cft.executablePath}) also `
235
+ + `failed: ${errorMessage(err)}`,
236
+ )
94
237
  }
238
+ }
239
+
240
+ /**
241
+ * Spawn `binary` on `opts.profileDir` and wait for CDP. On failure the
242
+ * process is stopped and the error says why: it could not be spawned, it
243
+ * exited (with its code and the tail of its stderr), or it never answered.
244
+ */
245
+ export async function startChrome(
246
+ binary: string,
247
+ opts: LaunchOptions,
248
+ ): Promise<ChildProcess> {
249
+ const timeoutMs = opts.timeoutMs ?? CDP_READY_TIMEOUT_MS
250
+ mkdirSync(opts.profileDir, { recursive: true })
251
+ releaseStaleProfileLock(opts.profileDir)
95
252
 
96
253
  const args = [
97
254
  `--remote-debugging-port=${opts.port}`,
@@ -104,31 +261,178 @@ export function launchChrome(opts: LaunchOptions): ChildProcess {
104
261
  }
105
262
 
106
263
  const child = spawn(binary, args, {
107
- stdio: 'ignore',
264
+ stdio: ['ignore', 'ignore', 'pipe'],
108
265
  detached: false,
109
266
  })
110
- return child
267
+ // Drained for the browser's whole life, not just until it is ready: a
268
+ // full pipe would block its writes.
269
+ let stderr = ''
270
+ child.stderr?.setEncoding('utf8')
271
+ child.stderr?.on('data', (chunk: string) => {
272
+ stderr = (stderr + chunk).slice(-STDERR_TAIL_CHARS)
273
+ })
274
+ let spawnError: Error | undefined
275
+ child.once('error', (err) => {
276
+ spawnError = err
277
+ })
278
+
279
+ const deadline = Date.now() + timeoutMs
280
+ while(Date.now() < deadline) {
281
+ if(await cdpAnswers(opts.port)) {
282
+ return child
283
+ }
284
+
285
+ if(spawnError) {
286
+ throw failedDependency(
287
+ `Could not start ${binary}: ${spawnError.message}`,
288
+ )
289
+ }
290
+
291
+ if(child.exitCode !== null || child.signalCode !== null) {
292
+ throw failedDependency(
293
+ `${binary} exited with ${child.exitCode !== null
294
+ ? `code ${child.exitCode}`
295
+ : `signal ${child.signalCode}`} before CDP answered on port `
296
+ + `${opts.port}.${stderrTail()}`,
297
+ )
298
+ }
299
+
300
+ await sleep(150)
301
+ }
302
+
303
+ await stopProcess(child)
304
+ throw gatewayTimeout(
305
+ `${binary} did not answer CDP on port ${opts.port} within `
306
+ + `${timeoutMs}ms.${stderrTail()}`,
307
+ )
308
+
309
+ function stderrTail() {
310
+ const tail = stderr.trim()
311
+ return tail ? `\nBrowser stderr (last ${tail.length} chars):\n${tail}` : ''
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Remove a dead browser's claim on `profileDir` so a new launch can take it.
317
+ *
318
+ * Chrome on POSIX claims a profile with `SingletonLock -> <hostname>-<pid>`.
319
+ * If that process is gone (a crash, a killed session) the links stay behind
320
+ * and the next launch hands off to the missing owner and exits. A lock whose
321
+ * PID is alive on this host belongs to a running browser, and is left alone:
322
+ * that browser must be closed first. A lock from another hostname is treated
323
+ * as stale: `~/.reclaim` is local, and a sandbox or container that changes the
324
+ * hostname between runs is the usual way to get one. Callers reach this only
325
+ * after nothing answered CDP on the port.
326
+ */
327
+ export function releaseStaleProfileLock(
328
+ profileDir: string,
329
+ os: NodeJS.Platform = platform(),
330
+ ) {
331
+ // Windows locks the profile with a plain file the OS releases on exit.
332
+ if(os === 'win32') {
333
+ return
334
+ }
335
+
336
+ const target = readLink(join(profileDir, 'SingletonLock'))
337
+ const owner = target?.match(/^(.+)-(\d+)$/)
338
+ if(owner?.[1] === hostname() && processAlive(Number(owner[2]))) {
339
+ throw conflict(
340
+ `The browser profile ${profileDir} is in use by a running browser `
341
+ + `(PID ${owner[2]}) that is not answering CDP on the requested port. `
342
+ + 'Close that browser window, then retry.',
343
+ )
344
+ }
345
+
346
+ const removed = SINGLETON_FILES.filter((name) => {
347
+ const path = join(profileDir, name)
348
+ if(readLink(path) === undefined) {
349
+ return false
350
+ }
351
+
352
+ try {
353
+ unlinkSync(path)
354
+ return true
355
+ } catch(err) {
356
+ LOGGER.warn({ err, path }, 'could not remove a stale profile lock')
357
+ return false
358
+ }
359
+ })
360
+ if(removed.length) {
361
+ LOGGER.info({ profileDir, removed, owner: target },
362
+ 'removed a stale browser profile lock')
363
+ }
111
364
  }
112
365
 
366
+ /**
367
+ * Wait until something answers CDP on `port`, polling every 150ms.
368
+ */
113
369
  export async function waitForCdpReady(
114
370
  port: number,
115
- timeoutMs = 10_000,
371
+ timeoutMs = CDP_READY_TIMEOUT_MS,
116
372
  ) {
117
373
  const deadline = Date.now() + timeoutMs
118
374
  while(Date.now() < deadline) {
119
- try {
120
- const res = await fetch(`http://127.0.0.1:${port}/json/version`)
121
- if(res.ok) {
122
- return
123
- }
124
- } catch{
125
- // retry
375
+ if(await cdpAnswers(port)) {
376
+ return
126
377
  }
127
378
 
128
- await new Promise<void>((r) => setTimeout(r, 150))
379
+ await sleep(150)
129
380
  }
130
381
 
131
- throw new Error(
382
+ throw gatewayTimeout(
132
383
  `Chrome CDP did not become ready on port ${port} within ${timeoutMs}ms`,
133
384
  )
134
385
  }
386
+
387
+ /** A listener that accepts but never answers (a stale non-CDP service on the
388
+ * port) fails the poll after 2s instead of outliving the launch deadline. */
389
+ async function cdpAnswers(port: number) {
390
+ try {
391
+ const res = await fetch(`http://127.0.0.1:${port}/json/version`, {
392
+ signal: AbortSignal.timeout(2_000),
393
+ })
394
+ return res.ok
395
+ } catch{
396
+ return false
397
+ }
398
+ }
399
+
400
+ /** SIGTERM, then SIGKILL if it lingers, and wait for the exit either way, so
401
+ * the profile lock is released before anything launches on it again. */
402
+ async function stopProcess(child: ChildProcess) {
403
+ if(child.exitCode !== null || child.signalCode !== null) {
404
+ return
405
+ }
406
+
407
+ const exited = new Promise<void>((r) => child.once('exit', () => r()))
408
+ child.kill('SIGTERM')
409
+ const timer = setTimeout(() => child.kill('SIGKILL'), 3_000)
410
+ await exited
411
+ clearTimeout(timer)
412
+ }
413
+
414
+ function readLink(path: string) {
415
+ try {
416
+ return lstatSync(path).isSymbolicLink() ? readlinkSync(path) : undefined
417
+ } catch{
418
+ return undefined
419
+ }
420
+ }
421
+
422
+ function processAlive(pid: number) {
423
+ try {
424
+ process.kill(pid, 0)
425
+ return true
426
+ } catch(err) {
427
+ // EPERM: it exists, it just belongs to someone else.
428
+ return err instanceof Error && 'code' in err && err.code === 'EPERM'
429
+ }
430
+ }
431
+
432
+ function errorMessage(err: unknown) {
433
+ return err instanceof Error ? err.message : String(err)
434
+ }
435
+
436
+ function sleep(ms: number) {
437
+ return new Promise<void>((r) => setTimeout(r, ms))
438
+ }
package/src/logger.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import pino, { stdTimeFunctions } from 'pino'
2
2
  import { LOG_LEVEL } from './consts.ts'
3
3
 
4
+ // stderr, not pino's default stdout: the MCP server speaks JSON-RPC over
5
+ // stdout, so a log line there corrupts the protocol stream.
4
6
  export const LOGGER = pino({
5
7
  name: '@reclaimprotocol/agent',
6
8
  timestamp: stdTimeFunctions.isoTime,
7
9
  base: { hostname: undefined, pid: undefined },
8
- })
10
+ }, pino.destination(2))
9
11
  LOGGER.level = LOG_LEVEL
@@ -62,9 +62,9 @@ const TOPIC_SUMMARIES: Record<string, string> = {
62
62
  'auth': 'Signing in to the backend, and which token authorizes what.',
63
63
  'devtools-migration': 'Importing a public legacy provider with the same '
64
64
  + 'UUID.',
65
- 'browser': 'Choosing a browser source (local Chrome, the Reclaim runtime in '
66
- + 'Docker, a remote Builder browser, or a custom CDP endpoint), '
67
- + 'geolocation, live view, and launch troubleshooting.',
65
+ 'browser': 'The default local browser, the opt-in modes (attach, the '
66
+ + 'Reclaim runtime in Docker, a remote Builder browser, a custom CDP '
67
+ + 'endpoint), geolocation, live view, and launch troubleshooting.',
68
68
  'capture': 'Tabs, sign-in detection, capturing through the navigation '
69
69
  + 'script, and picking an authenticated request to prove.',
70
70
  'draft': 'The OPRF/privacy decision, propose_provider, and the canonical '
package/src/mcp/start.ts CHANGED
@@ -18,6 +18,7 @@ import { OLD_MODE_INSTRUCTIONS } from '../old/instructions.ts'
18
18
  import { ReclaimSdkClient } from '../old/sdk-client.ts'
19
19
  import { buildOldBackendTools } from '../old/tools/build.ts'
20
20
  import { NAME, VERSION } from '../pkg.ts'
21
+ import { startZkFilesDownload } from '../proof/zk-files.ts'
21
22
  import { registerTools } from './api/tools.ts'
22
23
  import {
23
24
  type AgentState,
@@ -32,6 +33,7 @@ import {
32
33
  disposeAgentState,
33
34
  readSession,
34
35
  releaseAllListeners,
36
+ sessionRefreshingFetch,
35
37
  stopAllForwarders,
36
38
  } from './tools/index.ts'
37
39
  import { BUILDER_MODE_INSTRUCTIONS } from './instructions.ts'
@@ -59,11 +61,21 @@ export async function startMcpServer() {
59
61
  const agentToolsEnabled = !agentDisabled()
60
62
  /** The agent state this server registered, for shutdown to release. */
61
63
  let agentState: AgentState | undefined
64
+ const envToken = apiToken()
62
65
  const builderClient = new ReclaimClient({
63
66
  baseUrl: apiUrl(),
64
67
  // Builder access is orthogonal to publishing mode. A legacy-devtools
65
68
  // session can authenticate here solely to allocate a remote browser.
66
- token: apiToken() || readSession()?.token,
69
+ token: envToken || readSession()?.token,
70
+ // The cached sign-in refreshes itself; an explicit RECLAIM_API_TOKEN
71
+ // takes precedence and is sent as is.
72
+ ...(envToken
73
+ ? {}
74
+ : {
75
+ fetch: sessionRefreshingFetch(
76
+ oldMode ? 'authenticate_builder' : 'reclaim_authenticate',
77
+ ),
78
+ }),
67
79
  headers: { 'User-Agent': `${NAME}/${VERSION}` },
68
80
  })
69
81
  // Backend-mode toggle — available in both modes and regardless of whether
@@ -141,6 +153,13 @@ export async function startMcpServer() {
141
153
 
142
154
  const transport = new StdioServerTransport()
143
155
  await server.connect(transport)
156
+
157
+ // Fetch the proof files in the background so the first proof doesn't wait
158
+ // for them. Progress goes to stderr; `check_attestor` reports the state,
159
+ // and every proof still awaits the same download.
160
+ if(agentToolsEnabled) {
161
+ startZkFilesDownload()
162
+ }
144
163
  }
145
164
 
146
165
  /** How long teardown gets before the process leaves anyway. */