@reclaimprotocol/agent 0.1.34 → 0.1.35

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 (49) hide show
  1. package/README.md +3 -3
  2. package/lib/mcp/api/operations.gen.js +340 -150
  3. package/lib/mcp/instructions.js +3 -3
  4. package/lib/mcp/start.js +8 -7
  5. package/lib/mcp/tools/agent/build.js +2 -0
  6. package/lib/mcp/tools/agent/check-attestor.d.ts +5 -0
  7. package/lib/mcp/tools/agent/check-attestor.js +37 -0
  8. package/lib/mcp/tools/agent/live-view.js +42 -18
  9. package/lib/mcp/tools/agent/proof.d.ts +12 -0
  10. package/lib/mcp/tools/agent/proof.js +32 -0
  11. package/lib/mcp/tools/agent/shared.d.ts +2 -1
  12. package/lib/mcp/tools/agent/shared.js +28 -24
  13. package/lib/mcp/tools/agent/start-integration.js +5 -4
  14. package/lib/mcp/tools/verification/build.d.ts +3 -1
  15. package/lib/mcp/tools/verification/build.js +10 -2
  16. package/lib/mcp/tools/verification/callback-listener.d.ts +51 -0
  17. package/lib/mcp/tools/verification/callback-listener.js +163 -0
  18. package/lib/mcp/tools/verification/index.d.ts +2 -0
  19. package/lib/mcp/tools/verification/index.js +2 -0
  20. package/lib/mcp/tools/verification/proxy-callback-events.d.ts +59 -0
  21. package/lib/mcp/tools/verification/proxy-callback-events.js +300 -0
  22. package/lib/mcp/tools/verification/read-callback-events.d.ts +3 -0
  23. package/lib/mcp/tools/verification/read-callback-events.js +135 -0
  24. package/lib/mcp/tools/verification/result.d.ts +6 -0
  25. package/lib/mcp/tools/verification/result.js +1 -1
  26. package/lib/proof/check-attestor.d.ts +67 -0
  27. package/lib/proof/check-attestor.js +156 -0
  28. package/package.json +2 -2
  29. package/skill/SKILL.builder.md +7 -1
  30. package/skill/SKILL.common.md +21 -9
  31. package/skill/builder-mode.md +17 -12
  32. package/skill/start-integration.md +36 -11
  33. package/skill/verification-client-integration.md +15 -9
  34. package/src/mcp/api/operations.gen.ts +344 -150
  35. package/src/mcp/instructions.ts +3 -3
  36. package/src/mcp/start.ts +9 -7
  37. package/src/mcp/tools/agent/build.ts +2 -0
  38. package/src/mcp/tools/agent/check-attestor.ts +49 -0
  39. package/src/mcp/tools/agent/live-view.ts +42 -18
  40. package/src/mcp/tools/agent/proof.ts +55 -0
  41. package/src/mcp/tools/agent/shared.ts +33 -23
  42. package/src/mcp/tools/agent/start-integration.ts +5 -4
  43. package/src/mcp/tools/verification/build.ts +10 -2
  44. package/src/mcp/tools/verification/callback-listener.ts +203 -0
  45. package/src/mcp/tools/verification/index.ts +2 -0
  46. package/src/mcp/tools/verification/proxy-callback-events.ts +374 -0
  47. package/src/mcp/tools/verification/read-callback-events.ts +160 -0
  48. package/src/mcp/tools/verification/result.ts +1 -1
  49. package/src/proof/check-attestor.ts +209 -0
package/lib/mcp/start.js CHANGED
@@ -11,7 +11,7 @@ import { ReclaimSdkClient } from "../old/sdk-client.js";
11
11
  import { buildOldBackendTools } from "../old/tools/build.js";
12
12
  import { NAME, VERSION } from "../pkg.js";
13
13
  import { registerTools } from "./api/tools.js";
14
- import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, disposeAgentState, readSession, } from "./tools/index.js";
14
+ import { buildAgentState, buildAgentTools, buildBuilderAuthenticationTools, buildBuilderOrganizationTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, disposeAgentState, readSession, releaseAllListeners, stopAllForwarders, } from "./tools/index.js";
15
15
  import { BUILDER_MODE_INSTRUCTIONS } from "./instructions.js";
16
16
  import { registerToolList } from "./server.js";
17
17
  export async function startMcpServer() {
@@ -94,7 +94,7 @@ export async function startMcpServer() {
94
94
  // the session can end without a single tool call: the developer closes their
95
95
  // agent, the client kills the server, stdin drops. Nothing ran here before,
96
96
  // which left a container up after the session that made it was gone.
97
- if (agentToolsEnabled) {
97
+ if (agentToolsEnabled || !oldMode) {
98
98
  installShutdownHooks(() => agentState);
99
99
  }
100
100
  const transport = new StdioServerTransport();
@@ -117,10 +117,7 @@ function installShutdownHooks(state) {
117
117
  }
118
118
  shuttingDown = true;
119
119
  const current = state();
120
- if (!current) {
121
- process.exit(0);
122
- }
123
- LOGGER.info({ reason }, 'releasing the browser before exit');
120
+ LOGGER.info({ reason }, 'releasing resources before exit');
124
121
  // Bounded: a wedged `docker stop` must not keep the process alive, but
125
122
  // nor should we skip cleanup that would have finished in a second.
126
123
  const timer = setTimeout(() => {
@@ -128,7 +125,11 @@ function installShutdownHooks(state) {
128
125
  process.exit(0);
129
126
  }, SHUTDOWN_GRACE_MS);
130
127
  timer.unref();
131
- void disposeAgentState(current)
128
+ void Promise.all([
129
+ stopAllForwarders(),
130
+ releaseAllListeners(),
131
+ current && disposeAgentState(current),
132
+ ])
132
133
  .catch((err) => {
133
134
  LOGGER.warn({ err }, 'teardown failed');
134
135
  })
@@ -14,6 +14,7 @@ import { ensureAttestorCrypto } from "../../../proof/proof-core.js";
14
14
  import { analyzeTool } from "./analyze.js";
15
15
  import { attachTool } from "./attach.js";
16
16
  import { captureTools, resetCaptures } from "./capture.js";
17
+ import { checkAttestorTool } from "./check-attestor.js";
17
18
  import { listTabsTool } from "./eval.js";
18
19
  import { howItWorksTool } from "./how-it-works.js";
19
20
  import { liveViewTools } from "./live-view.js";
@@ -65,6 +66,7 @@ export function buildAgentTools(client, builderClient = client, externalState) {
65
66
  tools.push(t);
66
67
  }
67
68
  tools.push(analyzeTool(state.captures));
69
+ tools.push(checkAttestorTool());
68
70
  // navigate / eval_in_page / wait_for_page / get_cookies / list_requests /
69
71
  // find_requests_containing / propose_provider / replay_request / run_proof —
70
72
  // shared with the cloud loop, bound to each capture's local backend.
@@ -0,0 +1,5 @@
1
+ import { checkAttestor, type CheckAttestorOptions } from '../../../proof/check-attestor.ts';
2
+ import { type RegisteredTool } from '../../server.ts';
3
+ /** `check_attestor`: proves a public request to show whether the attestor
4
+ * works from this machine. `check` is injectable so tests need no network. */
5
+ export declare function checkAttestorTool(check?: (opts: CheckAttestorOptions) => ReturnType<typeof checkAttestor>): RegisteredTool;
@@ -0,0 +1,37 @@
1
+ import { checkAttestor, } from "../../../proof/check-attestor.js";
2
+ import { defineTool } from "../../server.js";
3
+ /** `check_attestor`: proves a public request to show whether the attestor
4
+ * works from this machine. `check` is injectable so tests need no network. */
5
+ export function checkAttestorTool(check = checkAttestor) {
6
+ return defineTool({
7
+ name: 'check_attestor',
8
+ description: 'Checks that the Reclaim attestor works from this machine by proving '
9
+ + 'a public CoinGecko price request with a throwaway key. It uses no '
10
+ + 'credential, provider, or logged-in site, and takes up to 45 '
11
+ + 'seconds (about 8 when healthy). Call it once when you set up a '
12
+ + 'new provider build, and after `run_proof` fails with a network '
13
+ + 'error. Returns `{ ok, attestorUrl, durationMs, '
14
+ + 'extractedParameters, error, errorKind }`. If `ok` is true, the '
15
+ + 'attestor is fine and a failing proof points to the target site. '
16
+ + 'If `errorKind` is `attestor-unreachable`, this machine cannot '
17
+ + 'reach the attestor (network, VPN, firewall, or proxy): tell the '
18
+ + 'developer. If it is `setup-failed`, the ZK file download failed '
19
+ + 'before the attestor was contacted: tell the developer to check '
20
+ + 'network access to the circuit download. If it is '
21
+ + '`attestor-error`, the attestor rejected the claim: report '
22
+ + '`error`.',
23
+ inputSchema: {
24
+ type: 'object',
25
+ properties: {
26
+ attestorUrl: {
27
+ type: 'string',
28
+ description: 'Attestor websocket URL. Set it only if the developer '
29
+ + 'supplied one; the default is the public attestor.',
30
+ },
31
+ },
32
+ },
33
+ annotations: { openWorldHint: true, readOnlyHint: true },
34
+ }, async ({ attestorUrl }) => {
35
+ return check(attestorUrl ? { attestorUrl } : {});
36
+ });
37
+ }
@@ -3,9 +3,10 @@
3
3
  * attached container browser's built-in viewer behind a public URL, so someone
4
4
  * who is not at this machine can watch it and optionally drive it.
5
5
  *
6
- * The server never runs, downloads or resolves cloudflared: the agent runs it
7
- * itself and passes the resulting `trycloudflare.com` URL back to get the link
8
- * to hand out.
6
+ * The server never runs, downloads or resolves cloudflared. The developer
7
+ * chooses who runs it: they run it themselves (recommended) or approve the
8
+ * agent running it. Either way the `trycloudflare.com` URL it prints comes
9
+ * back as `publicUrl` to get the link to hand out.
9
10
  *
10
11
  * Scoped to `container` mode on purpose. A `builder`-mode browser already has
11
12
  * a hosted live view at `attach_browser`'s `liveViewUrl`.
@@ -24,9 +25,13 @@ export function liveViewTools(attachRef) {
24
25
  description: 'Returns a link that lets someone who is not at this machine watch '
25
26
  + 'and drive the attached `container` browser, for example to sign '
26
27
  + 'in. This server opens no tunnel itself. Step 1: call with no '
27
- + 'arguments to get the local viewer URL and the `cloudflared` '
28
- + 'command for the developer to run. Step 2: after cloudflared '
29
- + 'prints its `https://*.trycloudflare.com` URL, call again with it '
28
+ + 'arguments. It returns the local viewer URL, the `cloudflared` '
29
+ + 'command, and two `choices`. Ask the developer to pick one before '
30
+ + 'any tunnel runs: (a, recommended) they run `! cloudflared tunnel '
31
+ + '--url http://localhost:<port>` themselves and paste the printed '
32
+ + '`https://*.trycloudflare.com` URL; (b) they explicitly approve '
33
+ + 'you running the command in the background. Never install '
34
+ + '`cloudflared` without approval. Step 2: call again with the URL '
30
35
  + 'as `publicUrl` to get the link to relay. The link grants access '
31
36
  + 'to that browser session only, so share it only with the person '
32
37
  + 'who needs it. For `builder`, use the `liveViewUrl` from '
@@ -75,27 +80,46 @@ export function liveViewTools(attachRef) {
75
80
  + 'separate window is visible too.',
76
81
  'Opening it on a phone lays the page out for that phone, with '
77
82
  + 'touch, pinch-zoom and the soft keyboard.',
78
- 'When they are done, stop the cloudflared process you started. '
79
- + 'The browser keeps running until `dispose_browser`.',
83
+ 'When they are done, stop the cloudflared process: yourself if '
84
+ + 'you started it, otherwise ask the developer. The browser '
85
+ + 'keeps running until `dispose_browser`.',
80
86
  ],
81
87
  };
82
88
  }
83
89
  const localUrl = `http://localhost:${viewerPort}`;
90
+ const command = `cloudflared tunnel --url ${localUrl}`;
84
91
  return {
85
92
  localViewerUrl: ensureLiveViewUrl(localUrl),
86
93
  port: viewerPort,
87
- command: `cloudflared tunnel --url ${localUrl}`,
94
+ command,
95
+ choices: [
96
+ {
97
+ id: 'developer-runs',
98
+ recommended: true,
99
+ action: `Run \`! ${command}\` in the prompt, then paste the `
100
+ + 'https://*.trycloudflare.com URL it prints.',
101
+ },
102
+ {
103
+ id: 'agent-runs',
104
+ recommended: false,
105
+ action: 'Approve the agent to run the command in the '
106
+ + 'background and read the URL from its output.',
107
+ },
108
+ ],
88
109
  _notes: [
89
- 'Run `command` yourself in a background process. If `cloudflared` '
90
- + 'is not installed, tell the developer and ask them to install '
91
- + 'it (https://developers.cloudflare.com/cloudflare-one/'
92
- + 'connections/connect-networks/downloads/) — do not install it '
110
+ 'Ask the developer to choose before any tunnel runs. Do not run '
111
+ + '`command` until they pick `agent-runs`.',
112
+ 'If they pick `developer-runs`, wait for the pasted URL. If they '
113
+ + 'pick `agent-runs`, run `command` in a background process and '
114
+ + 'read its output for a line containing '
115
+ + '`https://<words>.trycloudflare.com`.',
116
+ 'If `cloudflared` is not installed, tell the developer and ask '
117
+ + 'them to install it '
118
+ + '(https://developers.cloudflare.com/cloudflare-one/'
119
+ + 'connections/connect-networks/downloads/). Never install it '
93
120
  + 'without their approval.',
94
- 'Read its output for a line containing '
95
- + '`https://<words>.trycloudflare.com`. That is the public URL.',
96
- 'Call share_browser_view again with that value as `publicUrl` '
97
- + 'to get the link to relay. Never guess or reconstruct the '
98
- + 'hostname.',
121
+ 'Call share_browser_view again with the URL as `publicUrl` to get '
122
+ + 'the link to relay. Never guess or reconstruct the hostname.',
99
123
  'Expose only this port. Never tunnel the browser\'s CDP port.',
100
124
  ],
101
125
  };
@@ -1,4 +1,5 @@
1
1
  import { type ProofResult } from '../../../proof/attestor.ts';
2
+ import { type AttestorCheckResult, type CheckAttestorOptions } from '../../../proof/check-attestor.ts';
2
3
  /**
3
4
  * The MCP's rich `run_proof` view: persist the full claim to disk, return
4
5
  * identifier + extractedParameters + a prettified claim (request dropped, byte
@@ -11,3 +12,14 @@ export declare function shapeRichProofResult(result: ProofResult, opts: {
11
12
  hasSecrets: boolean;
12
13
  attestorUrl?: string;
13
14
  }): Record<string, unknown>;
15
+ export interface ProofDiagnosis {
16
+ attestor: AttestorCheckResult;
17
+ likelyCause: string;
18
+ next: string;
19
+ }
20
+ /**
21
+ * When an unverified proof failed with a network-like error, runs the
22
+ * attestor check once and says whether the attestor or the target site is the
23
+ * likely cause. Returns `undefined` for verified proofs and other errors.
24
+ */
25
+ export declare function diagnoseNetworkFailure(result: Pick<ProofResult, 'verified' | 'error' | 'errorKind'>, check: (opts: CheckAttestorOptions) => Promise<AttestorCheckResult>, attestorUrl?: string): Promise<ProofDiagnosis | undefined>;
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { agentTraceEnabled } from "../../../consts.js";
4
4
  import { reclaimHomeDirPath } from "../../../paths.js";
5
5
  import { tailLogs, warnsAndErrors, } from "../../../proof/attestor.js";
6
+ import { isNetworkError, } from "../../../proof/check-attestor.js";
6
7
  function claimsDir() {
7
8
  return join(reclaimHomeDirPath(), 'claims');
8
9
  }
@@ -157,3 +158,34 @@ export function shapeRichProofResult(result, opts) {
157
158
  }
158
159
  return out;
159
160
  }
161
+ /**
162
+ * When an unverified proof failed with a network-like error, runs the
163
+ * attestor check once and says whether the attestor or the target site is the
164
+ * likely cause. Returns `undefined` for verified proofs and other errors.
165
+ */
166
+ export async function diagnoseNetworkFailure(result, check, attestorUrl) {
167
+ if (result.verified
168
+ || result.errorKind !== undefined
169
+ || !isNetworkError(result.error)) {
170
+ return undefined;
171
+ }
172
+ const attestor = await check(attestorUrl ? { attestorUrl } : {});
173
+ if (attestor.ok) {
174
+ return {
175
+ attestor,
176
+ likelyCause: 'The attestor works from this machine, so the target site likely '
177
+ + 'blocks or drops the attestor\'s network (geo or IP restriction).',
178
+ next: 'If the browser attach mode is "builder", call `run_proof_via_pod`. '
179
+ + 'Otherwise tell the developer and suggest asking the Reclaim team. '
180
+ + 'Do not retry the same proof.',
181
+ };
182
+ }
183
+ return {
184
+ attestor,
185
+ likelyCause: 'The attestor check also failed, so this machine cannot reach the '
186
+ + 'attestor (network, VPN, firewall, or proxy).',
187
+ next: 'Tell the developer. To see the connection details, suggest '
188
+ + 'restarting the MCP server with `RECLAIM_AGENT_TRACE=1`. Do not '
189
+ + 'retry the same proof.',
190
+ };
191
+ }
@@ -1,3 +1,4 @@
1
+ import { checkAttestor } from '../../../proof/check-attestor.ts';
1
2
  import type { RegisteredTool } from '../../server.ts';
2
3
  import type { CaptureStore } from './capture.ts';
3
4
  /** The shared authoring tools mapped to the MCP's shape: resolve each call's
@@ -5,4 +6,4 @@ import type { CaptureStore } from './capture.ts';
5
6
  * takes `captureId`; `run_proof` also gets the credential-aware contract. The
6
7
  * tools are authored plain in `../../../authoring`; this only wraps them with
7
8
  * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
8
- export declare function sharedAuthoringTools(captures: CaptureStore): RegisteredTool[];
9
+ export declare function sharedAuthoringTools(captures: CaptureStore, check?: typeof checkAttestor): RegisteredTool[];
@@ -1,7 +1,8 @@
1
1
  import { authoringTools, extendTool, } from "../../../authoring/index.js";
2
2
  import { runAttestorProof } from "../../../proof/attestor.js";
3
+ import { checkAttestor } from "../../../proof/check-attestor.js";
3
4
  import { replayProviderInPage } from "../../../proof/page-replay.js";
4
- import { shapeRichProofResult } from "./proof.js";
5
+ import { diagnoseNetworkFailure, shapeRichProofResult } from "./proof.js";
5
6
  /**
6
7
  * OPRF mode this LOCAL (non-TEE) attestor can actually run. Recipes carry
7
8
  * `oprf-mpc` — the canonical/publish mode a TEE expects — but the local dev
@@ -32,7 +33,7 @@ function withLocalOprfMode(provider) {
32
33
  /** The local backend for one capture: drive its tab over CDP, read its
33
34
  * captured requests, prove via the in-process attestor, replay by re-fetching
34
35
  * FROM INSIDE the tab (real cookies/session — see proof/page-replay.ts). */
35
- function localBackend(entry, kept) {
36
+ function localBackend(entry, kept, check) {
36
37
  const { tabClient, session, drafts } = entry;
37
38
  const sendCdp = (method, params) => {
38
39
  return tabClient.send(method, params ?? {});
@@ -76,11 +77,13 @@ function localBackend(entry, kept) {
76
77
  ...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
77
78
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
78
79
  });
79
- return shapeRichProofResult(result, {
80
+ const shaped = shapeRichProofResult(result, {
80
81
  provider: draft.provider,
81
82
  hasSecrets: Object.keys(draft.secrets).length > 0,
82
83
  ...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
83
84
  });
85
+ const diagnosis = await diagnoseNetworkFailure(result, check, opts?.attestorUrl);
86
+ return diagnosis ? { ...shaped, diagnosis } : shaped;
84
87
  },
85
88
  };
86
89
  }
@@ -88,25 +91,26 @@ function localBackend(entry, kept) {
88
91
  * tool by the host below (the cloud's TEE signs in-enclave, so it needs none
89
92
  * of this). */
90
93
  const RUN_PROOF_EXTRAS = {
91
- description: 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
92
- + 'attestor SDK. Use only after replay_request looks right — attestor '
93
- + 'calls are expensive. Writes the full claim to '
94
- + '~/.reclaim/claims/<timestamp>.json and returns identifier + '
95
- + 'extractedParameters + claimPath + a prettified `claim` (request '
96
- + 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
97
- + 'writes a per-call diagnostic dump (input provider, sentParams, full '
98
- + 'attestor log stream, result) and returns it as `runLogPath`. '
99
- + '`attestorUrl` overrides the default only when the dev supplies a '
100
- + 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
101
- + '(the credential to sign with) is REQUIRED — call resolve_owner_key '
102
- + 'first. errorKind="missing-owner-key" means that address has no known '
103
- + 'private key; re-run resolve_owner_key or issue/import a credential. '
104
- + 'NOTE: unlike replay_request (which re-issues from inside the live '
105
- + 'attached browser tab), the ATTESTOR calls the target from its OWN '
106
- + 'network/IP, separate from the browser\'s. A request that replayed '
107
- + 'fine can still fail here for geo/IP-bound content (country-locked '
108
- + "pages, IP-allowlisted APIs) — if that happens, it's a property of "
109
- + 'the site, not a bug in the draft.',
94
+ description: 'Proves the drafted recipe with a full zkTLS proof through the attestor '
95
+ + 'SDK. Call it after `replay_request` looks right; attestor calls are '
96
+ + 'expensive. Call `resolve_owner_key` first and pass its address as '
97
+ + '`ownerAddress`. Writes the full claim to '
98
+ + '`~/.reclaim/claims/<timestamp>.json`. Returns `identifier`, '
99
+ + '`extractedParameters`, `claimPath`, and a readable `claim` (request '
100
+ + 'removed, byte arrays as base64). Set `RECLAIM_AGENT_TRACE=1` to also '
101
+ + 'get `runLogPath`, a diagnostic dump with the attestor log stream. '
102
+ + 'Set `attestorUrl` only if the developer supplied one; never invent '
103
+ + 'backup URLs. If `errorKind` is `missing-owner-key`, the address has '
104
+ + 'no known private key: run `resolve_owner_key` again, or issue or '
105
+ + 'import a credential. Unlike `replay_request`, which re-sends from '
106
+ + 'inside the attached browser tab, the attestor calls the target from '
107
+ + 'its own network and IP. A request that replayed can still fail here '
108
+ + 'for geo- or IP-bound content, such as country-locked pages or '
109
+ + 'IP-allowlisted APIs. That is a property of the site, not a draft '
110
+ + 'bug. After a network-like failure ("connection closed", '
111
+ + '`ERROR_NETWORK_ERROR`, timeout), the result includes `diagnosis` '
112
+ + 'with `attestor` (a `check_attestor` result), `likelyCause`, and '
113
+ + '`next`. Follow `next`; do not retry the same proof.',
110
114
  properties: {
111
115
  ownerAddress: {
112
116
  type: 'string',
@@ -124,7 +128,7 @@ const RUN_PROOF_EXTRAS = {
124
128
  * takes `captureId`; `run_proof` also gets the credential-aware contract. The
125
129
  * tools are authored plain in `../../../authoring`; this only wraps them with
126
130
  * `extendTool` (handlers untouched — loose arg parse keeps `captureId`). */
127
- export function sharedAuthoringTools(captures) {
131
+ export function sharedAuthoringTools(captures, check = checkAttestor) {
128
132
  const resolveBackend = (args) => {
129
133
  const captureId = String(args.captureId);
130
134
  const entry = captures.sessions.get(captureId);
@@ -132,7 +136,7 @@ export function sharedAuthoringTools(captures) {
132
136
  throw new Error(`no capture ${captureId} — call start_capture first `
133
137
  + '(it has been reset or never started).');
134
138
  }
135
- return localBackend(entry, captures);
139
+ return localBackend(entry, captures, check);
136
140
  };
137
141
  return authoringTools(resolveBackend).map((tool) => {
138
142
  if (tool.definition.name === 'run_proof') {
@@ -10,10 +10,11 @@ export function startIntegrationTool() {
10
10
  return defineTool({
11
11
  name: 'start_integration',
12
12
  description: 'Returns a step-by-step plan for adding Reclaim verification to the '
13
- + 'current codebase: sign in, build or choose a provider, then add the '
14
- + 'session route, callback, and Verify button. Call it first when a '
15
- + 'developer wants their users to prove something, and follow the '
16
- + 'plan to the end. Changes nothing itself.',
13
+ + 'current codebase: sign in, set up the app (session route, callback, '
14
+ + 'Verify button, local event proxy) in parallel with building or '
15
+ + 'choosing the provider, then wire the provider and test. Call it '
16
+ + 'first when a developer wants their users to prove something, and '
17
+ + 'follow the plan to the end. Changes nothing itself.',
17
18
  inputSchema: {
18
19
  type: 'object',
19
20
  required: ['goal'],
@@ -4,7 +4,9 @@ import type { RegisteredTool } from '../../server.ts';
4
4
  * generated API surface. `get_verification_result` authenticates with the org
5
5
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
6
  * the optional eth key — a flow the generated raw-API tools can't express
7
- * (the decryption key never leaves the consumer). Diagnosis is left to the
7
+ * (the decryption key never leaves the consumer). `read_callback_events` and
8
+ * `proxy_callback_events` read and forward the org's pull-listener events.
9
+ * Diagnosis is left to the
8
10
  * agent over the generated tools (`get_verification_session`,
9
11
  * `list_verification_events`, `list_verification_deliveries`), whose
10
12
  * descriptions carry the lifecycle and delivery semantics.
@@ -1,14 +1,22 @@
1
+ import { proxyCallbackEventsTool } from "./proxy-callback-events.js";
2
+ import { readCallbackEventsTool } from "./read-callback-events.js";
1
3
  import { verificationResultTool } from "./result.js";
2
4
  /**
3
5
  * Builder-mode verification tools: higher-level orchestrations over the
4
6
  * generated API surface. `get_verification_result` authenticates with the org
5
7
  * token, checks status, pulls the stored payload, and decrypts it locally with
6
8
  * the optional eth key — a flow the generated raw-API tools can't express
7
- * (the decryption key never leaves the consumer). Diagnosis is left to the
9
+ * (the decryption key never leaves the consumer). `read_callback_events` and
10
+ * `proxy_callback_events` read and forward the org's pull-listener events.
11
+ * Diagnosis is left to the
8
12
  * agent over the generated tools (`get_verification_session`,
9
13
  * `list_verification_events`, `list_verification_deliveries`), whose
10
14
  * descriptions carry the lifecycle and delivery semantics.
11
15
  */
12
16
  export function buildVerificationTools() {
13
- return [verificationResultTool()];
17
+ return [
18
+ verificationResultTool(),
19
+ readCallbackEventsTool(),
20
+ proxyCallbackEventsTool(),
21
+ ];
14
22
  }
@@ -0,0 +1,51 @@
1
+ import { ReclaimClient } from '@reclaimprotocol/client/api';
2
+ import type { IOperationResponse } from '@reclaimprotocol/client/openapi';
3
+ /** Runtime seams. Production passes none; tests pass a stub `fetch`. */
4
+ export interface CallbackDeps {
5
+ fetch?: typeof globalThis.fetch;
6
+ /** Waits between heartbeats and polls. Resolves, without error, on abort. */
7
+ sleep?: (ms: number, signal: AbortSignal) => Promise<void>;
8
+ keeper?: ListenerKeeper;
9
+ }
10
+ /**
11
+ * Events the org's pull listener subscribes to: the five session-ending
12
+ * events (the first four carry the signed result; `session_expired` has no
13
+ * result) plus the lifecycle milestones an app developer checks while testing.
14
+ * High-volume diagnostics (`network_request_observed`, `provider_script_log`,
15
+ * `user_interaction_*`) are left out, so a forwarded session sends the app at
16
+ * most 11 requests instead of dozens.
17
+ */
18
+ export declare const LISTENER_EVENTS: readonly ["verification_success", "verification_rejected", "verification_error", "verification_cancelled", "session_expired", "session_pending", "verification_client_opened", "authenticated", "verification_proofs_completed", "verification_result_submitting", "verification_result_submission_failed"];
19
+ export type ListenerInfo = {
20
+ subscriptionId: string;
21
+ created: boolean;
22
+ /** When the lease lapses unless the heartbeat renews it. */
23
+ expiresAt: string;
24
+ };
25
+ export type CallbackEvent = IOperationResponse<'ListCallbackEvents'>['items'][number];
26
+ /**
27
+ * A client that presents the organization's API key. With a `signal`, every
28
+ * request aborts when it does.
29
+ */
30
+ export declare function orgClient(secret: string, deps?: CallbackDeps, signal?: AbortSignal): ReclaimClient;
31
+ /**
32
+ * Keeps each org's pull listener alive while this MCP process runs. Creating
33
+ * the subscription is the heartbeat: it renews a lease, and the listener
34
+ * stops receiving events when the lease lapses. {@link release} ends the
35
+ * lease at shutdown.
36
+ */
37
+ export declare class ListenerKeeper {
38
+ #private;
39
+ /** Creates or renews the listener and starts its heartbeat (once). */
40
+ ensure(secret: string, deps?: CallbackDeps): Promise<ListenerInfo>;
41
+ /** Ends every lease and stops the heartbeats, within a short timeout. */
42
+ release(): Promise<void>;
43
+ }
44
+ /** Releases every listener this process keeps alive. Runs at MCP shutdown. */
45
+ export declare function releaseAllListeners(): Promise<void>;
46
+ /**
47
+ * Create the org's pull listener and keep it alive until shutdown. An org has
48
+ * one, so a second call returns the existing subscription (HTTP 200).
49
+ */
50
+ export declare function ensureListener(secret: string, deps?: CallbackDeps): Promise<ListenerInfo>;
51
+ export declare function sleepFor(ms: number, signal: AbortSignal): Promise<void>;