@reclaimprotocol/agent 0.1.20 → 0.1.22

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.
@@ -12,9 +12,10 @@ export const remapErrorAsResponse = (err) => {
12
12
  content: [{ type: 'text', text }],
13
13
  };
14
14
  }
15
+ const data = err.data;
15
16
  return {
16
17
  isError: true,
17
- structuredContent: err.data,
18
+ ...(data ? { structuredContent: data } : {}),
18
19
  content: [{ type: 'text', text }],
19
20
  };
20
21
  };
package/lib/mcp/start.js CHANGED
@@ -10,7 +10,7 @@ import { ReclaimSdkClient } from "../old/sdk-client.js";
10
10
  import { buildOldBackendTools } from "../old/tools/build.js";
11
11
  import { NAME, VERSION } from "../pkg.js";
12
12
  import { registerTools } from "./api/tools.js";
13
- import { buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, readSession, } from "./tools/index.js";
13
+ import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, readSession, } from "./tools/index.js";
14
14
  import { BUILDER_MODE_INSTRUCTIONS } from "./instructions.js";
15
15
  import { registerToolList } from "./server.js";
16
16
  export async function startMcpServer() {
@@ -49,13 +49,16 @@ export async function startMcpServer() {
49
49
  identity: resolveInitialIdentity(),
50
50
  });
51
51
  const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() });
52
+ // Shared with buildAgentTools below so the old-mode publish tool can see
53
+ // the live attach state (e.g. to flag a still-open browser session).
54
+ const agentState = buildAgentState();
52
55
  registerToolList(server, buildBuilderAuthenticationTools(builderClient));
53
56
  registerToolList(server, buildBuilderOrganizationTools(builderClient));
54
57
  registerToolList(server, buildLocalCredentialTools());
55
- registerToolList(server, buildOldBackendTools(oldClient, sdkClient));
58
+ registerToolList(server, buildOldBackendTools(oldClient, sdkClient, agentState.attach));
56
59
  if (agentToolsEnabled) {
57
60
  // No builder client → no builder publish tool (old mode has its own).
58
- registerToolList(server, buildAgentTools(undefined, builderClient));
61
+ registerToolList(server, buildAgentTools(undefined, builderClient, agentState));
59
62
  }
60
63
  // NOTE: registerTools() is intentionally NOT called — generated tools off.
61
64
  }
@@ -18,6 +18,11 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
18
18
  + 'port), builder (quota-accounted Popcorn browser with a '
19
19
  + 'shareable live view), or custom (caller-supplied CDP '
20
20
  + 'websocket). '
21
+ + 'For builder mode, orgId may be omitted: it auto-resolves to '
22
+ + 'the account\'s only organization, or — if there is more '
23
+ + 'than one — returns needsChoice with the full list so the '
24
+ + 'caller can ask the developer which to use and re-call with '
25
+ + 'that orgId. '
21
26
  + 'Builder and custom CDP credentials are held in memory and never '
22
27
  + 'returned.',
23
28
  inputSchema: {
@@ -52,7 +57,30 @@ export function attachTool(state, builderClient, name = 'attach_browser', before
52
57
  configPath: defaultConfigPath(),
53
58
  };
54
59
  }
55
- const next = await connect(mode, parsed, persisted, builderClient, dependencies);
60
+ let orgId = parsed.orgId;
61
+ if (mode === 'builder' && !orgId) {
62
+ assert(builderClient, badRequest('Builder browser service is unavailable'));
63
+ const { data } = await builderClient.call('ListOrgs', { query: {} });
64
+ const orgs = data.items ?? [];
65
+ assert(orgs.length, badRequest('No organizations available for this account — create one '
66
+ + 'first, or authenticate as a user who belongs to one.'));
67
+ if (orgs.length > 1) {
68
+ return {
69
+ needsChoice: true,
70
+ message: 'This account belongs to more than one organization. Ask '
71
+ + 'the developer which one to use, then re-call '
72
+ + 'attach_browser with mode "builder" and that orgId.',
73
+ organizations: orgs.map((o) => ({
74
+ id: o.id,
75
+ name: o.name,
76
+ kind: o.kind,
77
+ status: o.status,
78
+ })),
79
+ };
80
+ }
81
+ orgId = orgs[0].id;
82
+ }
83
+ const next = await connect(mode, { ...parsed, orgId }, persisted, builderClient, dependencies);
56
84
  try {
57
85
  await beforeReplace?.();
58
86
  await disposeAttached(state);
@@ -21,9 +21,16 @@ export interface AgentState {
21
21
  };
22
22
  captures: CaptureStore;
23
23
  }
24
+ /** Exported so a mode (e.g. old-devtools) that supplies its own publish tool
25
+ * can build the attach/capture state up front and share the SAME instance
26
+ * with `buildAgentTools` — letting that publish tool see the live attach
27
+ * state (e.g. to remind the caller about a still-open browser session). */
28
+ export declare function buildAgentState(): AgentState;
24
29
  /** Build the flat list of `RegisteredTool` entries, ready for
25
30
  * `registerToolList`.
26
31
  * Pass `client` to include the builder `create_provider_version_from_capture`;
27
32
  * omit it (e.g. OLD-devtools mode) to register only the backend-independent
28
- * capture/proof tools — old mode supplies its own publish tool. */
29
- export declare function buildAgentTools(client?: ReclaimClient, builderClient?: ReclaimClient | undefined): RegisteredTool[];
33
+ * capture/proof tools — old mode supplies its own publish tool.
34
+ * Pass `externalState` to share attach/capture state with tools registered
35
+ * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
36
+ export declare function buildAgentTools(client?: ReclaimClient, builderClient?: ReclaimClient | undefined, externalState?: AgentState): RegisteredTool[];
@@ -19,7 +19,11 @@ import { howItWorksTool } from "./how-it-works.js";
19
19
  import { createVersionFromCaptureTool } from "./publish.js";
20
20
  import { sessionTools } from "./session.js";
21
21
  import { sharedAuthoringTools } from "./shared.js";
22
- function buildAgentState() {
22
+ /** Exported so a mode (e.g. old-devtools) that supplies its own publish tool
23
+ * can build the attach/capture state up front and share the SAME instance
24
+ * with `buildAgentTools` — letting that publish tool see the live attach
25
+ * state (e.g. to remind the caller about a still-open browser session). */
26
+ export function buildAgentState() {
23
27
  return {
24
28
  attach: { config: new ConfigStore() },
25
29
  captures: { sessions: new Map() },
@@ -29,9 +33,11 @@ function buildAgentState() {
29
33
  * `registerToolList`.
30
34
  * Pass `client` to include the builder `create_provider_version_from_capture`;
31
35
  * omit it (e.g. OLD-devtools mode) to register only the backend-independent
32
- * capture/proof tools — old mode supplies its own publish tool. */
33
- export function buildAgentTools(client, builderClient = client) {
34
- const state = buildAgentState();
36
+ * capture/proof tools — old mode supplies its own publish tool.
37
+ * Pass `externalState` to share attach/capture state with tools registered
38
+ * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
39
+ export function buildAgentTools(client, builderClient = client, externalState) {
40
+ const state = externalState ?? buildAgentState();
35
41
  ensureAttestorCrypto();
36
42
  const tools = [];
37
43
  // Stateless docs tool — serve the mode-specific authoring guide to any
@@ -55,7 +61,7 @@ export function buildAgentTools(client, builderClient = client) {
55
61
  tools.push(t);
56
62
  }
57
63
  if (client) {
58
- tools.push(createVersionFromCaptureTool(client));
64
+ tools.push(createVersionFromCaptureTool(client, state.attach));
59
65
  }
60
66
  return tools;
61
67
  }
@@ -1,2 +1,2 @@
1
- export { buildAgentTools } from './build.ts';
1
+ export { buildAgentState, buildAgentTools } from './build.ts';
2
2
  export type { AgentState } from './build.ts';
@@ -1,3 +1,3 @@
1
1
  // Barrel for the agent tool group (Chrome CDP capture, provider synthesis,
2
2
  // replay, zkTLS proof). Import from here, not the individual files.
3
- export { buildAgentTools } from "./build.js";
3
+ export { buildAgentState, buildAgentTools } from "./build.js";
@@ -1,3 +1,6 @@
1
1
  import type { ReclaimClient } from '@reclaimprotocol/client';
2
2
  import { type RegisteredTool } from '../../server.ts';
3
- export declare function createVersionFromCaptureTool(client: ReclaimClient): RegisteredTool;
3
+ import type { AttachState } from './attach.ts';
4
+ export declare function createVersionFromCaptureTool(client: ReclaimClient, attachRef?: {
5
+ current?: AttachState;
6
+ }): RegisteredTool;
@@ -97,7 +97,7 @@ const ALLOWED_JS_REQUEST_SCHEMA = {
97
97
  },
98
98
  required: ['url', 'method'],
99
99
  };
100
- export function createVersionFromCaptureTool(client) {
100
+ export function createVersionFromCaptureTool(client, attachRef) {
101
101
  return defineTool({
102
102
  name: 'create_provider_version_from_capture',
103
103
  description: 'Persist drafted ReclaimProvider(s) as a provider version. ' +
@@ -125,7 +125,11 @@ export function createVersionFromCaptureTool(client) {
125
125
  'sessions (verification matches those values exactly; a missing ' +
126
126
  'field is a 400 at session create). ' +
127
127
  'Refine types/descriptions via `update_provider_version` while ' +
128
- 'still on draft.',
128
+ 'still on draft. ' +
129
+ 'On success, if a browser is still attached, the response\'s ' +
130
+ '`_notes` reminds you to confirm with the developer before ' +
131
+ 'calling `dispose_browser` — never close it unprompted, they ' +
132
+ 'may want it for further edits.',
129
133
  inputSchema: {
130
134
  type: 'object',
131
135
  properties: {
@@ -261,6 +265,20 @@ export function createVersionFromCaptureTool(client) {
261
265
  ...(args.allowedJsRequests ?? []).map((r, i) => countsFor(`Injected request ${i + 1}`, r)),
262
266
  ];
263
267
  notes.push(...matchRedactionWarnings(requestCounts));
268
+ // A browser session left attached after a successful publish is easy
269
+ // to forget about — surface it so the caller asks the developer
270
+ // before closing it (a builder-mode session is quota-accounted and
271
+ // otherwise just sits until its TTL expires).
272
+ const attached = attachRef?.current;
273
+ if (attached) {
274
+ notes.push(`A "${attached.mode}" browser session is still attached` +
275
+ (attached.browserSessionId
276
+ ? ` (browserSessionId: ${attached.browserSessionId})`
277
+ : '') +
278
+ '. Ask the developer whether they want to keep it open for ' +
279
+ 'further edits — only call dispose_browser once they confirm ' +
280
+ 'they\'re done with it.');
281
+ }
264
282
  // Let the dev know how/when the script runs (and the window.Reclaim gap).
265
283
  if (args.jsUserScripts?.trim()) {
266
284
  notes.push('jsUserScripts will run in the Builder verification client before ' +
@@ -170,8 +170,9 @@ async function exchangeCode(authBase, opts) {
170
170
  client_id: opts.clientId,
171
171
  }).toString(),
172
172
  });
173
- assert(res.ok, new Error(`Token exchange failed (${res.status}): ${await res.text()}`));
174
- const json = (await res.json());
173
+ const body = await res.text();
174
+ assert(res.ok, new Error(`Token exchange failed (${res.status}): ${body}`));
175
+ const json = JSON.parse(body);
175
176
  assert(json.access_token, new Error('Token exchange returned no access_token.'));
176
177
  return {
177
178
  access_token: json.access_token,
@@ -1,4 +1,5 @@
1
1
  import type { RegisteredTool } from '../../mcp/server.ts';
2
+ import type { AttachState } from '../../mcp/tools/agent/attach.ts';
2
3
  import type { ReclaimOldClient } from '../client.ts';
3
4
  import type { ReclaimSdkClient } from '../sdk-client.ts';
4
5
  /**
@@ -10,5 +11,11 @@ import type { ReclaimSdkClient } from '../sdk-client.ts';
10
11
  * `sdkClient` is a SEPARATE, unauthenticated client for the public
11
12
  * production SDK backend (`api.reclaimprotocol.org`) — a different host from
12
13
  * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
14
+ *
15
+ * `attachRef` is the SAME attach state `buildAgentTools` was given (see
16
+ * `mcp/start.ts`) — passing it lets the publish tool remind the caller about
17
+ * a still-open browser session after a successful publish.
13
18
  */
14
- export declare function buildOldBackendTools(client: ReclaimOldClient, sdkClient: ReclaimSdkClient): RegisteredTool[];
19
+ export declare function buildOldBackendTools(client: ReclaimOldClient, sdkClient: ReclaimSdkClient, attachRef?: {
20
+ current?: AttachState;
21
+ }): RegisteredTool[];
@@ -19,11 +19,15 @@ import { verifySdkSetupTool } from "./verify-sdk-setup.js";
19
19
  * `sdkClient` is a SEPARATE, unauthenticated client for the public
20
20
  * production SDK backend (`api.reclaimprotocol.org`) — a different host from
21
21
  * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
22
+ *
23
+ * `attachRef` is the SAME attach state `buildAgentTools` was given (see
24
+ * `mcp/start.ts`) — passing it lets the publish tool remind the caller about
25
+ * a still-open browser session after a successful publish.
22
26
  */
23
- export function buildOldBackendTools(client, sdkClient) {
27
+ export function buildOldBackendTools(client, sdkClient, attachRef) {
24
28
  return [
25
29
  authenticateTool(client),
26
- createVersionFromCaptureTool(client),
30
+ createVersionFromCaptureTool(client, attachRef),
27
31
  getMeProvidersTool(client),
28
32
  getProviderInfoTool(sdkClient),
29
33
  updateProviderMetadataTool(client),
@@ -1,4 +1,5 @@
1
1
  import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { AttachState } from '../../mcp/tools/agent/attach.ts';
2
3
  import type { ReclaimOldClient } from '../client.ts';
3
4
  /**
4
5
  * Publish a drafted provider to the OLD devtools backend. Keeps the builder
@@ -12,5 +13,13 @@ import type { ReclaimOldClient } from '../client.ts';
12
13
  * highest existing version).
13
14
  *
14
15
  * Get a `providerId` from `get_me_providers`.
16
+ *
17
+ * `attachRef`, when given, is checked after a successful publish: if a
18
+ * browser is still attached, a `_notes` entry reminds the caller to confirm
19
+ * with the developer before closing it (see `dispose_browser`) rather than
20
+ * closing it unprompted — the developer may still want the session for
21
+ * further edits.
15
22
  */
16
- export declare function createVersionFromCaptureTool(client: ReclaimOldClient): RegisteredTool;
23
+ export declare function createVersionFromCaptureTool(client: ReclaimOldClient, attachRef?: {
24
+ current?: AttachState;
25
+ }): RegisteredTool;
@@ -46,8 +46,14 @@ function rethrowOwnershipError(err, providerId) {
46
46
  * highest existing version).
47
47
  *
48
48
  * Get a `providerId` from `get_me_providers`.
49
+ *
50
+ * `attachRef`, when given, is checked after a successful publish: if a
51
+ * browser is still attached, a `_notes` entry reminds the caller to confirm
52
+ * with the developer before closing it (see `dispose_browser`) rather than
53
+ * closing it unprompted — the developer may still want the session for
54
+ * further edits.
49
55
  */
50
- export function createVersionFromCaptureTool(client) {
56
+ export function createVersionFromCaptureTool(client, attachRef) {
51
57
  return defineTool({
52
58
  name: 'create_provider_version_from_capture',
53
59
  description: 'Publish drafted ReclaimProvider(s) to the devtools backend. ' +
@@ -93,7 +99,11 @@ export function createVersionFromCaptureTool(client) {
93
99
  'default include), `urlType` (REGEX/CONSTANT/TEMPLATE, default ' +
94
100
  'derived from {{placeholders}}), and `isOptional` on each ' +
95
101
  'responseMatch (default false). \n\n' +
96
- 'Captured requests must be GET/POST. OPRF redactions are supported.',
102
+ 'Captured requests must be GET/POST. OPRF redactions are supported. ' +
103
+ 'On success, if a browser is still attached, the response\'s ' +
104
+ '`_notes` reminds you to confirm with the developer before ' +
105
+ 'calling `dispose_browser` — never close it unprompted, they ' +
106
+ 'may want it for further edits.',
97
107
  inputSchema: {
98
108
  type: 'object',
99
109
  properties: {
@@ -427,6 +437,20 @@ export function createVersionFromCaptureTool(client) {
427
437
  // Non-blocking advisory per request — never alters or blocks the
428
438
  // publish; the author keeps full control. Attaches under `_notes`.
429
439
  const notes = matchRedactionWarnings(providers.map((p, i) => countsFor(`Request ${i + 1}`, p)));
440
+ // A browser session left attached after a successful publish is
441
+ // easy to forget about — surface it so the caller asks the
442
+ // developer before closing it (a builder-mode session is
443
+ // quota-accounted and otherwise just sits until its TTL expires).
444
+ const attached = attachRef?.current;
445
+ if (attached) {
446
+ notes.push(`A "${attached.mode}" browser session is still attached` +
447
+ (attached.browserSessionId
448
+ ? ` (browserSessionId: ${attached.browserSessionId})`
449
+ : '') +
450
+ '. Ask the developer whether they want to keep it open for ' +
451
+ 'further edits — only call dispose_browser once they confirm ' +
452
+ 'they\'re done with it.');
453
+ }
430
454
  const withNotes = (result) => {
431
455
  if (notes.length && result && typeof result === 'object') {
432
456
  return { ...result, _notes: notes };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.20",
4
+ "version": "0.1.22",
5
5
  "description": "Reclaim Protocol's tools for agents — Reclaim Protocol lets your app verify **data points** about its users — a follower count, an account balance, an order history. This package gives your AI coding agent the tools to **find, build, test and fix the _data verification providers_** (the recipes that verify a data point) by inspecting and replaying captured requests, checking sessions, evaluate proofs, publishing updates, and much more.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -83,7 +83,7 @@
83
83
  "@modelcontextprotocol/sdk": "^1.29.0",
84
84
  "@noble/hashes": "^2.2.0",
85
85
  "@reclaimprotocol/attestor-core": "^5.0.8",
86
- "@reclaimprotocol/client": "0.1.20",
86
+ "@reclaimprotocol/client": "0.1.22",
87
87
  "@reclaimprotocol/tls": "^0.1.3",
88
88
  "chrome-remote-interface": "^0.33.0",
89
89
  "domhandler": "^6.0.0",
@@ -173,6 +173,10 @@ The value of any parameter marked secret is not included in the final proof, omi
173
173
 
174
174
  When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
175
175
 
176
+ - **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow. Closing remote browsers
177
+ soon is also important to avoid wasting resources. So as best
178
+ practise, close remote browser session when not needed anymore.
179
+
176
180
  <!-- common: concerns-section -->
177
181
 
178
182
  ## Things to never do
@@ -17,8 +17,8 @@ You drive the browser through the capture → draft → replay → prove loop us
17
17
  1. **Attach.** Call `attach_browser`. If it returns `needsChoice: true`, ask the dev which browser source they want:
18
18
  - `dedicated` — agent auto-launches Chrome with a dedicated profile under `~/.reclaim/chrome-profile`. Easiest local path; works out of the box.
19
19
  - `attach` — connect to a Chrome the dev is already running. **Important Chrome 136+ gotcha:** the dev's Chrome must be launched with BOTH `--remote-debugging-port=9222` AND `--user-data-dir=<some-non-default-path>`. Since Chrome 136 (May 2025), the debug-port flag is silently ignored when the default user profile is in use. If `attach` fails, tell them this verbatim and offer to switch to `dedicated`.
20
- - `builder` — allocate a quota-accounted remote Popcorn browser. Obtain an `orgId` with `list_builder_organizations` in old-devtools mode or the generated `list_orgs` tool in Builder mode. If Builder answers 401, authenticate with `authenticate_builder` in old-devtools mode or the existing `authenticate` flow in Builder mode, then retry. Return the `liveViewUrl` to the dev so they can share it with the volunteer operating the browser. This works in both publishing modes and consumes one VERIFICATION-pool use under the chosen organization's normal plan restrictions.
21
- - `custom` — connect to a `ws://` or `wss://` CDP URL supplied by the dev. The URL is a credential: never repeat it, log it, or persist it. An optional `liveViewUrl` may be returned and shared.
20
+ - `builder` — allocate a quota-accounted remote Popcorn browser. If Builder answers 401, authenticate with `authenticate_builder` in old-devtools mode or the existing `authenticate` flow in Builder mode, then retry. You can omit `orgId` and call `attach_browser` directly: it auto-resolves to the account's only organization, or — if the account belongs to more than one — comes back with `needsChoice: true` and an `organizations` list; **ask the dev which one to use** (don't guess or default to the first) and re-call `attach_browser` with that `orgId`. (`list_builder_organizations` in old-devtools mode / `list_orgs` in Builder mode still work directly if you want the list up front instead.) Return the FULL `liveViewUrl` to the dev verbatim — on its own line, never abbreviated, elided, or wrapped in prose that truncates it — so it stays one clean, copy-pasteable string; they'll share it with the volunteer operating the browser. This works in both publishing modes and consumes one VERIFICATION-pool use under the chosen organization's normal plan restrictions.
21
+ - `custom` — connect to a `ws://` or `wss://` CDP URL supplied by the dev. The URL is a credential: never repeat it, log it, or persist it. An optional `liveViewUrl` may be returned — if so, share it FULL and verbatim, same as `builder` above.
22
22
 
23
23
  Re-call with the chosen `mode` and its required fields.
24
24
 
@@ -141,6 +141,8 @@ A secret value CAN be used in the URL, geoLocation, request body, response match
141
141
 
142
142
  When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
143
143
 
144
+ - **If a browser is still attached**, `create_provider_version_from_capture`'s response carries a `_notes` reminder about it. Surface that to the dev and ask whether they want to keep the session open for further edits — only call `dispose_browser` once they confirm they're done. Never close it unprompted; a `builder`-mode session is quota-accounted, but the dev may still be mid-workflow.
145
+
144
146
  <!-- common: concerns-section -->
145
147
 
146
148
  ## Things to never do
@@ -20,9 +20,10 @@ export const remapErrorAsResponse = (
20
20
  }
21
21
  }
22
22
 
23
+ const data = (err as Boom<ProblemDetails>).data
23
24
  return {
24
25
  isError: true as const,
25
- structuredContent: (err as Boom<ProblemDetails>).data,
26
+ ...(data ? { structuredContent: data } : {}),
26
27
  content: [{ type: 'text' as const, text }],
27
28
  }
28
29
  }
package/src/mcp/start.ts CHANGED
@@ -19,6 +19,7 @@ import { buildOldBackendTools } from '../old/tools/build.ts'
19
19
  import { NAME, VERSION } from '../pkg.ts'
20
20
  import { registerTools } from './api/tools.ts'
21
21
  import {
22
+ buildAgentState,
22
23
  buildAgentTools,
23
24
  buildBuilderAuthenticationTools,
24
25
  buildBuilderOrganizationTools,
@@ -73,14 +74,23 @@ export async function startMcpServer() {
73
74
  identity: resolveInitialIdentity(),
74
75
  })
75
76
  const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() })
77
+ // Shared with buildAgentTools below so the old-mode publish tool can see
78
+ // the live attach state (e.g. to flag a still-open browser session).
79
+ const agentState = buildAgentState()
76
80
 
77
81
  registerToolList(server, buildBuilderAuthenticationTools(builderClient))
78
82
  registerToolList(server, buildBuilderOrganizationTools(builderClient))
79
83
  registerToolList(server, buildLocalCredentialTools())
80
- registerToolList(server, buildOldBackendTools(oldClient, sdkClient))
84
+ registerToolList(
85
+ server,
86
+ buildOldBackendTools(oldClient, sdkClient, agentState.attach),
87
+ )
81
88
  if(agentToolsEnabled) {
82
89
  // No builder client → no builder publish tool (old mode has its own).
83
- registerToolList(server, buildAgentTools(undefined, builderClient))
90
+ registerToolList(
91
+ server,
92
+ buildAgentTools(undefined, builderClient, agentState),
93
+ )
84
94
  }
85
95
  // NOTE: registerTools() is intentionally NOT called — generated tools off.
86
96
  } else {
@@ -70,6 +70,11 @@ export function attachTool(
70
70
  + 'port), builder (quota-accounted Popcorn browser with a '
71
71
  + 'shareable live view), or custom (caller-supplied CDP '
72
72
  + 'websocket). '
73
+ + 'For builder mode, orgId may be omitted: it auto-resolves to '
74
+ + 'the account\'s only organization, or — if there is more '
75
+ + 'than one — returns needsChoice with the full list so the '
76
+ + 'caller can ask the developer which to use and re-call with '
77
+ + 'that orgId. '
73
78
  + 'Builder and custom CDP credentials are held in memory and never '
74
79
  + 'returned.',
75
80
  inputSchema: {
@@ -107,8 +112,42 @@ export function attachTool(
107
112
  }
108
113
  }
109
114
 
115
+ let orgId = parsed.orgId
116
+ if(mode === 'builder' && !orgId) {
117
+ assert(
118
+ builderClient,
119
+ badRequest('Builder browser service is unavailable'),
120
+ )
121
+ const { data } = await builderClient.call('ListOrgs', { query: {} })
122
+ const orgs = data.items ?? []
123
+ assert(
124
+ orgs.length,
125
+ badRequest(
126
+ 'No organizations available for this account — create one '
127
+ + 'first, or authenticate as a user who belongs to one.',
128
+ ),
129
+ )
130
+ if(orgs.length > 1) {
131
+ return {
132
+ needsChoice: true,
133
+ message:
134
+ 'This account belongs to more than one organization. Ask '
135
+ + 'the developer which one to use, then re-call '
136
+ + 'attach_browser with mode "builder" and that orgId.',
137
+ organizations: orgs.map((o) => ({
138
+ id: o.id,
139
+ name: o.name,
140
+ kind: o.kind,
141
+ status: o.status,
142
+ })),
143
+ }
144
+ }
145
+
146
+ orgId = orgs[0].id
147
+ }
148
+
110
149
  const next = await connect(
111
- mode, parsed, persisted, builderClient, dependencies,
150
+ mode, { ...parsed, orgId }, persisted, builderClient, dependencies,
112
151
  )
113
152
  try {
114
153
  await beforeReplace?.()
@@ -28,7 +28,11 @@ export interface AgentState {
28
28
  captures: CaptureStore
29
29
  }
30
30
 
31
- function buildAgentState(): AgentState {
31
+ /** Exported so a mode (e.g. old-devtools) that supplies its own publish tool
32
+ * can build the attach/capture state up front and share the SAME instance
33
+ * with `buildAgentTools` — letting that publish tool see the live attach
34
+ * state (e.g. to remind the caller about a still-open browser session). */
35
+ export function buildAgentState(): AgentState {
32
36
  return {
33
37
  attach: { config: new ConfigStore() },
34
38
  captures: { sessions: new Map() },
@@ -39,12 +43,15 @@ function buildAgentState(): AgentState {
39
43
  * `registerToolList`.
40
44
  * Pass `client` to include the builder `create_provider_version_from_capture`;
41
45
  * omit it (e.g. OLD-devtools mode) to register only the backend-independent
42
- * capture/proof tools — old mode supplies its own publish tool. */
46
+ * capture/proof tools — old mode supplies its own publish tool.
47
+ * Pass `externalState` to share attach/capture state with tools registered
48
+ * elsewhere (e.g. old mode's publish tool) instead of building a fresh one. */
43
49
  export function buildAgentTools(
44
50
  client?: ReclaimClient,
45
51
  builderClient: ReclaimClient | undefined = client,
52
+ externalState?: AgentState,
46
53
  ): RegisteredTool[] {
47
- const state = buildAgentState()
54
+ const state = externalState ?? buildAgentState()
48
55
  ensureAttestorCrypto()
49
56
  const tools: RegisteredTool[] = []
50
57
  // Stateless docs tool — serve the mode-specific authoring guide to any
@@ -75,7 +82,7 @@ export function buildAgentTools(
75
82
  }
76
83
 
77
84
  if(client) {
78
- tools.push(createVersionFromCaptureTool(client))
85
+ tools.push(createVersionFromCaptureTool(client, state.attach))
79
86
  }
80
87
 
81
88
  return tools
@@ -1,4 +1,4 @@
1
1
  // Barrel for the agent tool group (Chrome CDP capture, provider synthesis,
2
2
  // replay, zkTLS proof). Import from here, not the individual files.
3
- export { buildAgentTools } from './build.ts'
3
+ export { buildAgentState, buildAgentTools } from './build.ts'
4
4
  export type { AgentState } from './build.ts'
@@ -12,6 +12,7 @@ import {
12
12
  providerToCreateVersionRequest,
13
13
  } from '../../../provider/to-version.ts'
14
14
  import { defineTool, type RegisteredTool } from '../../server.ts'
15
+ import type { AttachState } from './attach.ts'
15
16
 
16
17
  /** Schemas shared by the top-level `provider`/`providers` and the lighter
17
18
  * `allowedJsRequests` drafts — keeps the request shape from drifting between
@@ -142,6 +143,7 @@ interface PublishArgs {
142
143
 
143
144
  export function createVersionFromCaptureTool(
144
145
  client: ReclaimClient,
146
+ attachRef?: { current?: AttachState },
145
147
  ): RegisteredTool {
146
148
  return defineTool<PublishArgs>(
147
149
  {
@@ -172,7 +174,11 @@ export function createVersionFromCaptureTool(
172
174
  'sessions (verification matches those values exactly; a missing ' +
173
175
  'field is a 400 at session create). ' +
174
176
  'Refine types/descriptions via `update_provider_version` while ' +
175
- 'still on draft.',
177
+ 'still on draft. ' +
178
+ 'On success, if a browser is still attached, the response\'s ' +
179
+ '`_notes` reminds you to confirm with the developer before ' +
180
+ 'calling `dispose_browser` — never close it unprompted, they ' +
181
+ 'may want it for further edits.',
176
182
  inputSchema: {
177
183
  type: 'object',
178
184
  properties: {
@@ -328,6 +334,23 @@ export function createVersionFromCaptureTool(
328
334
  ]
329
335
  notes.push(...matchRedactionWarnings(requestCounts))
330
336
 
337
+ // A browser session left attached after a successful publish is easy
338
+ // to forget about — surface it so the caller asks the developer
339
+ // before closing it (a builder-mode session is quota-accounted and
340
+ // otherwise just sits until its TTL expires).
341
+ const attached = attachRef?.current
342
+ if(attached) {
343
+ notes.push(
344
+ `A "${attached.mode}" browser session is still attached` +
345
+ (attached.browserSessionId
346
+ ? ` (browserSessionId: ${attached.browserSessionId})`
347
+ : '') +
348
+ '. Ask the developer whether they want to keep it open for ' +
349
+ 'further edits — only call dispose_browser once they confirm ' +
350
+ 'they\'re done with it.',
351
+ )
352
+ }
353
+
331
354
  // Let the dev know how/when the script runs (and the window.Reclaim gap).
332
355
  if(args.jsUserScripts?.trim()) {
333
356
  notes.push(
@@ -233,12 +233,10 @@ async function exchangeCode(
233
233
  client_id: opts.clientId,
234
234
  }).toString(),
235
235
  })
236
- assert(
237
- res.ok,
238
- new Error(`Token exchange failed (${res.status}): ${await res.text()}`),
239
- )
236
+ const body = await res.text()
237
+ assert(res.ok, new Error(`Token exchange failed (${res.status}): ${body}`))
240
238
 
241
- const json = (await res.json()) as {
239
+ const json = JSON.parse(body) as {
242
240
  access_token?: string
243
241
  expires_in?: number
244
242
  }
@@ -1,4 +1,5 @@
1
1
  import type { RegisteredTool } from '../../mcp/server.ts'
2
+ import type { AttachState } from '../../mcp/tools/agent/attach.ts'
2
3
  import type { ReclaimOldClient } from '../client.ts'
3
4
  import type { ReclaimSdkClient } from '../sdk-client.ts'
4
5
  import { authenticateTool } from './authenticate.ts'
@@ -23,14 +24,19 @@ import { verifySdkSetupTool } from './verify-sdk-setup.ts'
23
24
  * `sdkClient` is a SEPARATE, unauthenticated client for the public
24
25
  * production SDK backend (`api.reclaimprotocol.org`) — a different host from
25
26
  * `client`'s old-devtools dashboard backend (`devapi.reclaimprotocol.org`).
27
+ *
28
+ * `attachRef` is the SAME attach state `buildAgentTools` was given (see
29
+ * `mcp/start.ts`) — passing it lets the publish tool remind the caller about
30
+ * a still-open browser session after a successful publish.
26
31
  */
27
32
  export function buildOldBackendTools(
28
33
  client: ReclaimOldClient,
29
34
  sdkClient: ReclaimSdkClient,
35
+ attachRef?: { current?: AttachState },
30
36
  ): RegisteredTool[] {
31
37
  return [
32
38
  authenticateTool(client),
33
- createVersionFromCaptureTool(client),
39
+ createVersionFromCaptureTool(client, attachRef),
34
40
  getMeProvidersTool(client),
35
41
  getProviderInfoTool(sdkClient),
36
42
  updateProviderMetadataTool(client),
@@ -1,5 +1,6 @@
1
1
  import { Boom, isBoom } from '@hapi/boom'
2
2
  import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
3
+ import type { AttachState } from '../../mcp/tools/agent/attach.ts'
3
4
  import { INJECTION_GUIDE } from '../../provider/injection-doc.ts'
4
5
  import {
5
6
  countsFor,
@@ -70,9 +71,16 @@ function rethrowOwnershipError(err: unknown, providerId: string): never {
70
71
  * highest existing version).
71
72
  *
72
73
  * Get a `providerId` from `get_me_providers`.
74
+ *
75
+ * `attachRef`, when given, is checked after a successful publish: if a
76
+ * browser is still attached, a `_notes` entry reminds the caller to confirm
77
+ * with the developer before closing it (see `dispose_browser`) rather than
78
+ * closing it unprompted — the developer may still want the session for
79
+ * further edits.
73
80
  */
74
81
  export function createVersionFromCaptureTool(
75
82
  client: ReclaimOldClient,
83
+ attachRef?: { current?: AttachState },
76
84
  ): RegisteredTool {
77
85
  return defineTool<{
78
86
  provider?: ReclaimProvider
@@ -139,7 +147,11 @@ export function createVersionFromCaptureTool(
139
147
  'default include), `urlType` (REGEX/CONSTANT/TEMPLATE, default ' +
140
148
  'derived from {{placeholders}}), and `isOptional` on each ' +
141
149
  'responseMatch (default false). \n\n' +
142
- 'Captured requests must be GET/POST. OPRF redactions are supported.',
150
+ 'Captured requests must be GET/POST. OPRF redactions are supported. ' +
151
+ 'On success, if a browser is still attached, the response\'s ' +
152
+ '`_notes` reminds you to confirm with the developer before ' +
153
+ 'calling `dispose_browser` — never close it unprompted, they ' +
154
+ 'may want it for further edits.',
143
155
  inputSchema: {
144
156
  type: 'object',
145
157
  properties: {
@@ -507,6 +519,23 @@ export function createVersionFromCaptureTool(
507
519
  providers.map((p, i) => countsFor(`Request ${i + 1}`, p)),
508
520
  )
509
521
 
522
+ // A browser session left attached after a successful publish is
523
+ // easy to forget about — surface it so the caller asks the
524
+ // developer before closing it (a builder-mode session is
525
+ // quota-accounted and otherwise just sits until its TTL expires).
526
+ const attached = attachRef?.current
527
+ if(attached) {
528
+ notes.push(
529
+ `A "${attached.mode}" browser session is still attached` +
530
+ (attached.browserSessionId
531
+ ? ` (browserSessionId: ${attached.browserSessionId})`
532
+ : '') +
533
+ '. Ask the developer whether they want to keep it open for ' +
534
+ 'further edits — only call dispose_browser once they confirm ' +
535
+ 'they\'re done with it.',
536
+ )
537
+ }
538
+
510
539
  const withNotes = (result: unknown) => {
511
540
  if(notes.length && result && typeof result === 'object') {
512
541
  return { ...result, _notes: notes }