@reclaimprotocol/agent 0.1.3 → 0.1.5

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 (58) hide show
  1. package/README.md +11 -42
  2. package/lib/authoring/backend.d.ts +95 -0
  3. package/lib/authoring/backend.js +24 -0
  4. package/lib/authoring/index.d.ts +4 -0
  5. package/lib/authoring/index.js +5 -0
  6. package/lib/authoring/tools.d.ts +9 -0
  7. package/lib/authoring/tools.js +378 -0
  8. package/lib/cdp/capture-engine.d.ts +7 -0
  9. package/lib/cdp/capture-engine.js +78 -0
  10. package/lib/cdp/capture.d.ts +1 -1
  11. package/lib/cdp/capture.js +1 -1
  12. package/lib/consts.d.ts +4 -2
  13. package/lib/consts.js +6 -2
  14. package/lib/mcp/api/operations.gen.js +46 -18
  15. package/lib/mcp/instructions.d.ts +2 -0
  16. package/lib/mcp/instructions.js +16 -0
  17. package/lib/mcp/server.d.ts +18 -2
  18. package/lib/mcp/server.js +19 -4
  19. package/lib/mcp/start.js +2 -1
  20. package/lib/mcp/tools/agent/build.js +4 -0
  21. package/lib/mcp/tools/agent/how-it-works.d.ts +11 -0
  22. package/lib/mcp/tools/agent/how-it-works.js +24 -0
  23. package/lib/mcp/tools/agent/publish.js +2 -2
  24. package/lib/mcp/tools/agent/shared.d.ts +6 -0
  25. package/lib/mcp/tools/agent/shared.js +115 -0
  26. package/lib/old/client.d.ts +10 -0
  27. package/lib/old/client.js +27 -0
  28. package/lib/old/login-cdp.d.ts +5 -0
  29. package/lib/old/login-cdp.js +12 -1
  30. package/lib/old/tools/authenticate.js +7 -3
  31. package/lib/old/tools/build.d.ts +2 -2
  32. package/lib/old/tools/build.js +4 -2
  33. package/lib/old/tools/session-analytics-logs.d.ts +9 -0
  34. package/lib/old/tools/session-analytics-logs.js +25 -0
  35. package/lib/proof/proof-core.d.ts +1 -1
  36. package/lib/proof/proof-core.js +1 -1
  37. package/lib/tools/registry.d.ts +36 -0
  38. package/lib/tools/registry.js +17 -0
  39. package/package.json +2 -3
  40. package/skill/{SKILL.md → SKILL.builder.md} +16 -45
  41. package/skill/SKILL.old.md +225 -0
  42. package/skill/old-mode.md +2 -0
  43. package/src/cdp/capture-engine.ts +89 -0
  44. package/src/cdp/capture.ts +5 -1
  45. package/src/consts.ts +7 -2
  46. package/src/mcp/api/operations.gen.ts +46 -18
  47. package/src/mcp/instructions.ts +18 -0
  48. package/src/mcp/server.ts +43 -6
  49. package/src/mcp/start.ts +2 -0
  50. package/src/mcp/tools/agent/build.ts +4 -0
  51. package/src/mcp/tools/agent/how-it-works.ts +32 -0
  52. package/src/mcp/tools/agent/publish.ts +2 -2
  53. package/src/old/client.ts +32 -0
  54. package/src/old/login-cdp.ts +20 -1
  55. package/src/old/tools/authenticate.ts +11 -3
  56. package/src/old/tools/build.ts +4 -2
  57. package/src/old/tools/session-analytics-logs.ts +33 -0
  58. package/src/proof/proof-core.ts +1 -1
@@ -0,0 +1,225 @@
1
+ # Authoring a Reclaim Provider
2
+
3
+ > **Terminology note:** A **provider** is the recipe you author here — the
4
+ > instructions for extracting a data point from a website (the
5
+ > `responseMatches` / `responseRedactions` spec, identified by a
6
+ > `providerId`). Always call it a **provider**; never call a provider a
7
+ > "verification." A **verification** (or **verification session**) is the
8
+ > separate runtime act in which an end-user proves a data point *using* a
9
+ > provider — reserve that word for the session/result, never for the recipe.
10
+ > If a developer calls the recipe a "verification," gently use "provider"
11
+ > yourself rather than correcting them.
12
+
13
+ You drive the browser through the capture → draft → replay → prove loop using the `reclaim-agent` MCP server. **Your default stance is driver, not orchestrator.** Only pause to ask the dev for things that genuinely require a human: typing credentials, completing 2FA, solving a captcha, confirming an ambiguous value. Navigation, reloads, DOM inspection, request capture — all yours.
14
+
15
+ ## Getting authenticated
16
+
17
+ Call `reclaim_authenticate({})` with no arguments. It opens the Reclaim dashboard in Chrome, waits for the dev to sign in, reads the token from the page automatically, and closes the tab — a still-valid cached identity is reused first, so it's often instant. Tell the dev once: _"A browser window is opening the Reclaim dashboard — please sign in and I'll continue automatically."_
18
+
19
+ - Force a fresh login (ignore the cached identity): `reclaim_authenticate({ useBrowser: true })`.
20
+ - Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or `reclaim_authenticate({ ethAddress: "0x..." })` for an already-provisioned eth user.
21
+ - Tokens are short-lived (~1h). If a later call returns 401, call `reclaim_authenticate({})` again.
22
+
23
+ ## The loop
24
+
25
+ 1. **Attach.** Call `attach_chrome`. If it returns `needsChoice: true`, ask the dev:
26
+ - `dedicated` — agent auto-launches Chrome with a dedicated profile under `~/.reclaim/chrome-profile`. Easiest path; works out of the box.
27
+ - `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`.
28
+
29
+ Re-call with the chosen `attachMode`.
30
+
31
+ 2. **Pick a tab and open the site yourself.** Call `list_tabs`. If there's already a tab on the right origin, use it. Otherwise pick any tab (the first one is usually fine) and call `navigate(tabId, url)` to send it to the site you've inferred from the dev's request (e.g. "GitHub username" → `https://github.com`). Don't ask the dev to navigate.
32
+
33
+ 3. **Check login state, wait if needed.** Use `eval_in_page(tabId, ...)` to evaluate a site-specific check.
34
+
35
+ **Combine a positive signal (logged-in-only element present) with a negative signal (login button absent), AND them together.** Single signals are too prone to false positives — most notably, `<meta>` tags often exist on every page regardless of login state; only their `.content` attribute is populated. The check `!!document.querySelector('meta[name="user-login"]')` returns true even when logged out — DO NOT use existence-only checks on meta tags or other persistent-but-empty elements.
36
+
37
+ Examples (always verify against the live page if these break — sites change selectors):
38
+ - GitHub: `(document.querySelector('meta[name="user-login"]')?.content?.length || 0) > 0 && !document.querySelector('a.HeaderMenu-link[href="/login"]')`
39
+ - X / Twitter: `!!document.querySelector('[data-testid="AppTabBar_Profile_Link"]') && !document.querySelector('[data-testid="loginButton"]')`
40
+ - Generic shape: `<element-present-only-when-logged-in> && !<login-button-present-only-when-logged-out>`
41
+
42
+ If the result is truthy, skip to step 4.
43
+
44
+ If falsy: **navigate to the site's login URL yourself first**, so the dev lands directly on the login form instead of having to find it. Common patterns: `https://<host>/login`, `https://<host>/signin`, `https://<host>/i/flow/login` (Twitter/X). Pick the one you know works for the site. Call `navigate(tabId, "<login-url>")`. Then tell the dev once: "I've opened the login page on `<site>` — please log in (with 2FA if needed). I'll detect when you're done." Then call `wait_for_page(tabId, expression="<same composite expression>", timeoutMs=180000)`. The MCP polls every second and returns when the expression goes truthy. **Do not** instruct the dev to "tell me when you're done" — just wait via the tool.
45
+
46
+ **Self-check before trusting the result:** if your detector says "already logged in" on a freshly-launched Chrome profile (cookies-clean), the detector is wrong. Brand-new profile means brand-new cookie jar — there's no way to be already authenticated. If you see that contradiction, your check is matching something that's present regardless of login (a meta tag, a generic class). Rewrite the expression and re-evaluate.
47
+
48
+ 4. **Navigate to the page that displays the value.** Use what you know about the site to construct the URL. If you need the dev's username, eval it (e.g. on GitHub: `document.querySelector('meta[name="user-login"]')?.content`). Then `navigate(tabId, url)`. Don't ask the dev to navigate.
49
+
50
+ 5. **Start capture, then trigger the request.** Call `start_capture(tabId)` first. Then issue a reload so the relevant API call fires while capture is live: `eval_in_page(tabId, "location.reload()")`. For some pages you may need to click a specific element or call a SPA route — use `eval_in_page` to do that programmatically rather than asking the dev to click.
51
+
52
+ 6. **Identify the value from the DOM.** `eval_in_page` to read the displayed value (e.g. the logged-in username from `meta[name="user-login"]`'s `content` attribute). If the value is unambiguous, proceed without asking. Only confirm with the dev when there are multiple plausible interpretations (e.g. the page shows both a display name "John Doe" and a handle "@jdoe" and you need to pick which to prove).
53
+
54
+ 7. **Find the request.** Call `find_requests_containing(captureId, value)`. For each top candidate (limit ≤ 5), call `analyze_request_constraints` and surface its `concerns` to the dev verbatim. Skip candidates flagged `signedRequest` or `botChallenge` unless none others are available. Concerns flagged `csrfDependency`, `shortLivedToken`, or `graphql` are informational — surface them and proceed.
55
+
56
+ **Strongly prefer authenticated, user-specific endpoints over public pages.** A proof against a public endpoint (e.g. `https://github.com/<username>`) attests only that "this URL returned this value at this time" — anyone in the world could fetch the same URL and prove the same thing. The proof says nothing about who the prover _is_. To prove a fact about the dev, the captured request needs to be one whose **response varies with authentication** — typically an API endpoint that returns the logged-in user's data (e.g. `GET https://api.github.com/user`, `https://twitter.com/i/api/.../UserByScreenName?...&forCurrentUser=true`).
57
+
58
+ When ranking candidates from `find_requests_containing`, prefer ones that:
59
+ - Have `Cookie`, `Authorization`, or other secret headers (analyze_request_constraints will surface these via `splitSecrets`).
60
+ - Have URL paths suggesting authenticated data: `/me`, `/user`, `/settings`, `/api/...`, `/graphql` with viewer queries.
61
+ - Return JSON keyed to the dev's identity (login/username/id fields next to the target value).
62
+
63
+ If the only candidates are public-page HTML responses, tell the dev: "The value appears on a public page, so a proof against this endpoint won't bind to your identity — anyone could prove the same fact. I can still build it if you just want to attest the value, but for an identity-bound proof we need to capture an authenticated request. Want to keep digging, or proceed anyway?" Then wait for their call.
64
+
65
+ 8. **Decide whether to OPRF the value** (privacy from the attestor). Before calling `propose_provider`, decide if the captured value should be hashed end-to-end so the attestor never sees it in cleartext. Four options:
66
+ - **No hash** (default). Value travels via `paramValues` + `{{name}}` template; attestor sees the cleartext substring. Simplest, fewest moving parts. Right call for non-sensitive values (public IPs from `ifconfig.me`, public usernames the dev is happy to expose, public repo names).
67
+ - **`oprf-raw`**. Server-side OPRF. **The only variant that runs on Windows / without external deps.** Use this when the dev wants any privacy and you're in a normal dev loop. Privacy is the weakest of the three (attestor still participates in hashing) — surface that honestly.
68
+ - **`oprf`**. Client-side ZK OPRF via gnark. Strongest privacy. Requires gnark install (Linux/macOS in practice). Don't suggest this on Windows.
69
+ - **`oprf-mpc`**. TEE-based MPC OPRF. Requires a TEE attestor deployment. Niche; only suggest if the dev brings it up.
70
+
71
+ **Default skip-the-question heuristic.** If the value is obviously non-sensitive (public IP, public username, public repo name, public follower/star counts on sites where the captured endpoint is auth-bound), default to **no hash** and proceed without asking. If the value is or could be sensitive (email, real name, private ID, anything the dev wouldn't post publicly), surface the choice verbatim:
72
+
73
+ > "Do you want this value kept private from the attestor via OPRF?
74
+ >
75
+ > - **No** (default) — value visible to attestor
76
+ > - **`oprf-raw`** — works on Windows / without extra deps; weakest of the OPRF variants
77
+ > - **`oprf`** — gnark/ZK; strongest privacy but requires Linux/macOS + gnark install
78
+ > - **`oprf-mpc`** — TEE-based; requires a TEE attestor"
79
+
80
+ **Hard constraint:** OPRF-hashed values are capped at **62 bytes**. If the value is longer and the dev picks any hash, surface that and propose either a tighter `target` (e.g. just the email local-part) or no hash. `propose_provider` will return a clean error if you try anyway.
81
+
82
+ 9. **Draft.** Call `propose_provider(captureId, requestId, target, name, hash?)`. Pass `hash` only if the dev opted in. Inspect the returned provider:
83
+ - Is the right header marked secret? (cookies, Authorization, X-\* tokens)
84
+ - Is `responseMatches[0].value` a `contains` with a `{{name}}` template (matching the captured value's structural context)?
85
+ - Is `paramValues[name]` set to the original captured value?
86
+ - Does `responseRedactions[0]` carry an `xPath` or `jsonPath` anchor? (And if `hash` was requested, also a `regex` with a named capture and a `hash: oprf-…` field?)
87
+
88
+ **Canonical drafted shape** (no hash):
89
+
90
+ ```json
91
+ {
92
+ "responseMatches": [
93
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
94
+ ],
95
+ "responseRedactions": [{ "xPath": "//script[@id=\"client-env\"]" }],
96
+ "paramValues": { "username": "Scratch-net" }
97
+ }
98
+ ```
99
+
100
+ **Canonical drafted shape** (with `hash: 'oprf-raw'`):
101
+
102
+ ```json
103
+ {
104
+ "responseMatches": [
105
+ { "type": "contains", "value": "\"login\":\"{{username}}\"" }
106
+ ],
107
+ "responseRedactions": [
108
+ {
109
+ "xPath": "//script[@id=\"client-env\"]",
110
+ "regex": "\"login\":\"(?<username>[A-Za-z0-9_-]+)\"",
111
+ "hash": "oprf-raw"
112
+ }
113
+ ],
114
+ "paramValues": { "username": "Scratch-net" }
115
+ }
116
+ ```
117
+
118
+ **Do NOT rewrite the shape.** The synthesizer's output is the canonical Reclaim shape — `contains` template + paramValues + xPath/jsonPath redaction (optionally + regex+hash). Specifically:
119
+ - Do not replace the `contains` template with a hand-crafted regex.
120
+ - Do not hand-write a named-capture regex unless `hash` was requested AND the synthesizer's output doesn't already have one. (It will, for any digit/slug/email-shaped target.)
121
+ - Do not remove `paramValues` — that's how the template resolves at proof time.
122
+ - The redaction's xPath/jsonPath is what the prover reveals; the match's template is what the attestor verifies post-substitution. They serve different purposes — don't conflate them.
123
+ - Do not prefix the match-template's param with `context.`/`context_` — that namespace is reserved for consumer-supplied URL/body params (step 10). Match templates are extraction params and stay bare.
124
+
125
+ Permitted edits: narrowing the `contains` template, picking a more distinctive xPath class/id if the synthesizer's pick is too generic, fixing the `name` field, adding/removing secret headers. If you're tempted to rewrite the provider from scratch, call `propose_provider` again with different inputs instead.
126
+
127
+ 10. **Parameterize user-specific URL segments** so the provider isn't user-specific at the URL level. The matcher already populates `paramValues` with the _captured value_ (e.g. `{username: "Scratch-net"}` for a username target). If the URL ALSO contains user-identifying segments, those are **consumer-supplied context params** — at verification time their values come from the Consumer's session `context`, not from extraction. They use a namespaced placeholder, and the namespace differs by mode:
128
+
129
+ - Replace user-specific segments with `{{context_<name>}}` and `paramValues: { ..., "context_username": "Scratch-net" }`.
130
+ - Query parameters with user-specific values → same treatment.
131
+ - Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
132
+ - If the URL has no user-identifying segments (e.g. `/settings/profile`), leave it untouched — the matcher already handled the value via paramValues.
133
+
134
+ **Do not rename the match-template param.** The synthesizer's `{{name}}` in `responseMatches[].value` is an _extraction_ template (the attestor pulls the value out of the response) — it stays bare and never gets the `context.` prefix. Only URL/body placeholders whose values the Consumer must supply are context params.
135
+
136
+ `propose_provider` returns `verificationWarnings` for the calls only an author can make (currently: a captured value baked literally into the URL, needing the `{{context_<name>}}`-vs-bare-vs-re-capture decision). They are advisory, never blocking — **surface each warning to the dev verbatim** and resolve it before publishing.
137
+
138
+ 11. **Replay.** Call `replay_request(provider, secretParamsRef)`. Three outcomes:
139
+
140
+ - `matchesOriginal: true` → proceed.
141
+ - `matchesOriginal: false` with `hints` mentioning auth/csrf → re-trigger the action yourself: navigate or reload via the tools, then loop to step 5. Don't ask the dev to redo it manually.
142
+ - `matchesOriginal: false` with stable diff (timestamps etc.) → tighten the matcher and retry.
143
+
144
+ **After replay succeeds, run an authentication-bound check.** Call `replay_request(provider)` _without_ the `secretParamsRef` (omit it). This sends the request stripped of all secret headers — the same shape an unauthenticated visitor would send.
145
+
146
+ - If `matchesOriginal: false` (e.g. 401/403, or the value differs) → the endpoint is auth-bound. Good. The proof will actually mean "the holder of these secrets saw this value."
147
+ - If `matchesOriginal: true` with the same value → the endpoint is fully public. Tell the dev verbatim: "I verified this endpoint returns the same value with no authentication — the proof won't prove your identity, only that this URL returned the value. Anyone could generate the same proof. Want to keep digging for an auth-bound endpoint, or proceed anyway?" Wait for their decision before continuing.
148
+
149
+ 12. **Prove.** Call `run_proof(provider, secretParamsRef)`. On `verified: true`, the full claim has been written to `~/.reclaim/claims/<timestamp>.json` and the tool returns `{verified, extractedValue, extractedParameters, identifier, owner, claimPath, claim}`. The `claim` field is a display-ready version of the proof — its huge `request` payload has been stripped and all byte arrays converted to base64, so it renders cleanly. **Show it to the dev as a fenced JSON code block at the end** along with the provider JSON. That's the final result. On `verified: false`, diagnose using the returned error and iterate.
150
+
151
+ **Missing signing credential (`errorKind: "missing-owner-key"`)** — the dev hasn't set up an eth proof-owner key yet. Tell them what's happening, then drive the bootstrap yourself: call `resolve_owner_key`; if it returns no key, ask once _"shall I mint a fresh eth key via `issue_credentials` (or `import_credentials` if you already hold one)? It's written to the project `.env` (mode 0600, gitignored) for this and future sessions."_; on yes, call `issue_credentials` (or `import_credentials`) and retry the same `run_proof` call. **No restart needed** — the key is read live from `.env` on each `run_proof`, and the existing `secretParamsRef` from `propose_provider` is still valid.
152
+
153
+ **On any `verified: false` response, the result includes (inline):**
154
+ - `error` — the SDK's thrown error message verbatim
155
+ - `sentParams` — the exact `params` object the SDK saw, post-template substitution (lets you spot mutation / substitution surprises)
156
+
157
+ **Deeper tracing (opt-in).** If the dev launches Claude Code with `RECLAIM_AGENT_TRACE=1` in the MCP server's env, `run_proof` ALSO captures the SDK's full pino log stream into `result.attestorLogs` and persists a per-call dump to `~/.reclaim/proof-runs/<timestamp>.json` (surfaced as `runLogPath`). Off by default because the trace stream is verbose. Suggest turning it on when investigating a recurring proof failure.
158
+
159
+ **The attestor is reliable. If it errors, the cause is in the provider, the network, or the prover-side code — not "the attestor can't handle this site".** Do not invent failure modes like "HTML is too large", "TLS records are chunked", "the attestor matches against gzipped bytes" (it doesn't if the provider has `Accept-Encoding: identity`, which `propose_provider` always sets), or "xPath doesn't work on this DOM". These are wrong and they waste the dev's time. Instead:
160
+ 1. Show the `error` field to the dev verbatim.
161
+ 2. If `sentParams` differs from what you expected — e.g. `{{name}}` substituted to the wrong value, headers got mutated, redactions look different from `propose_provider`'s output — surface that.
162
+ 3. Only then propose a hypothesis, and tie it to that evidence. If the diagnosis is unclear, suggest the dev set `RECLAIM_AGENT_TRACE=1` and rerun for full attestor logs.
163
+
164
+ **Do not auto-retry with a different `attestorUrl`.** There is one live public attestor right now; backup hostnames (`attestor-2.*`, etc.) **do not exist**. `attestorUrl` is only for when the dev explicitly supplies a private/staging URL.
165
+
166
+ **Missing ZK circuit files:** if the error says "ZK circuit resources are missing" and points at a `circuit.wasm` path, the npm postinstall didn't run (commonly because the dev used `npm install --ignore-scripts` or a corp policy disabled it). Tell the dev to run `npm run download:zk-files` from the project root and then retry `run_proof` — no need to redo capture or propose. The MCP server doesn't need a restart either; the next `run_proof` call will see the files.
167
+
168
+ 13. **Publish the proved provider.** `provider` is the exact JSON from step 9 with the step-10 URL parameterisation applied, and `initialUrl` is the page where the user logs in / the value is displayed (e.g. `https://github.com/Syed`) — **not** the API endpoint URL inside the provider.
169
+
170
+ Call `create_provider_version_from_capture`:
171
+ - **New provider:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
172
+ - **New version of an existing provider:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
173
+ - `get_me_providers({ searchQuery? })` lists the providers you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
174
+ - There's no separate "create-then-version" step and no org / public-review workflow here.
175
+ - Consumer-supplied params (step 10) keep the `{{context_<name>}}` form in this mode; there is no `requiredContext` declaration on the legacy backend.
176
+
177
+ When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
178
+
179
+ ## Concerns the developer should see verbatim
180
+
181
+ When `analyze_request_constraints` returns concerns, repeat each `message` to the dev exactly. Don't paraphrase, don't omit. These are the dev's signal that a provider is or isn't worth pursuing.
182
+
183
+ ## Things to never do
184
+
185
+ - Never echo a raw `Authorization`, `Cookie`, or `X-*-Token` value back to the dev or write one to any file. The MCP guarantees these don't appear in tool responses — keep that guarantee.
186
+ - Never call `run_proof` before `replay_request` succeeds. Attestor calls are expensive.
187
+ - Never pretend a proof succeeded when `verified: false` came back. Show the error and iterate.
188
+ - **Never hand-craft `responseMatches` or `responseRedactions`.** If you find yourself typing a regex by hand, OR writing `(?<name>...)`-style patterns by hand, OR copying the captured value literally into the match — stop. The synthesizer in `propose_provider` already produces the canonical shape: `contains` template + paramValues for the match, xPath/jsonPath (optionally + regex+hash when `hash` is passed) for the redaction. The named-capture regex in the redaction is the synthesizer's job, not yours.
189
+ - Never write a literal value into `responseMatches.value` — the synthesizer always emits `{{name}}` templates with paramValues. A literal value bakes the original user into the provider.
190
+ - Never pass `hash` to `propose_provider` for a free-form (unicode / spaces / punctuation) target — the synthesizer will error, and the right move is either to use a tighter `target` or accept cleartext (no `hash`).
191
+ - Never accept "the synthesizer's xPath looks too narrow, let me write a regex" — the xPath is fine, the attestor will reveal the whole matched element which is naturally ≥16 bytes.
192
+
193
+ ## Common first-time issues — surface these proactively
194
+
195
+ If `attach_chrome` errors, times out, or `waitForCdpReady` reports the port isn't answering, walk through these in order before retrying:
196
+
197
+ - **Browser binary not found.** If the error lists candidate paths it tried (Program Files Chrome, LOCALAPPDATA Chrome, Edge), every one missed. Either the dev has Chrome installed somewhere unusual, or doesn't have Chrome/Edge at all. Walk them through setting `RECLAIM_AGENT_CHROME_PATH` to the actual binary path:
198
+ ```powershell
199
+ setx RECLAIM_AGENT_CHROME_PATH "C:\path\to\chrome.exe"
200
+ ```
201
+ `setx` (not `$env:`) writes to the user-level Windows env, so future shells and future Claude Code launches inherit it. **Then fully restart Claude Code** — MCP servers snapshot env vars at launch, so the running reclaim-agent has the old empty value. After restart, the new MCP process picks up the env. On macOS/Linux: `export RECLAIM_AGENT_CHROME_PATH=/path/to/chrome` in the shell, then restart Claude Code from that same shell so the var propagates.
202
+ - **Pre-existing `chrome.exe` processes hijack the new launch (Windows).** If any Chrome is already running, a new launch with `--remote-debugging-port` gets delegated to the existing process group and the debug port silently doesn't open. Tell the dev to kill all `chrome.exe` first (`taskkill /F /IM chrome.exe` in cmd/PowerShell, or via Task Manager), then retry. Affects both `attach` and `dedicated` modes when the existing instance was launched with the same profile dir.
203
+ - **Chrome 136+ silently disables remote debugging on the default user profile.** Already noted in step 1, but worth raising again here: the only fix is a non-default `--user-data-dir`. `dedicated` mode is always safe; `attach` mode needs the dev to have launched with both flags.
204
+
205
+ **Credential for signing the proof.** `run_proof` requires an `ownerAddress` — the eth proof-owner key whose private half signs the claim. It does **no** resolution itself; get the address from `resolve_owner_key` first. The private key is resolved locally (`RECLAIM_PRIVATE_KEY` env → a `RECLAIM_PRIVATE_KEY=0x…` line in the project `.env` → a file at `RECLAIM_PRIVATE_KEY_FILE`) and never enters the conversation.
206
+
207
+ 1. **Call `resolve_owner_key`.** It checks `RECLAIM_PRIVATE_KEY` (MCP env) → the project `.env` → `RECLAIM_PRIVATE_KEY_FILE`, and returns only the derived address.
208
+ - `{ ready: true, address }` → call `run_proof({ provider, ownerAddress: address, … })`. No prompts.
209
+ - `{ ready: false }` → no key exists yet. Ask the dev once how to set one up:
210
+ - **Provide their own key** → `import_credentials({ privateKey: "0x…" })` (also accepts a `keyFile` path).
211
+ - **Create a new one** → `issue_credentials({})`. (Fine default for test proofs — owner identity doesn't matter.)
212
+ - Either way the key is written to the project `.env` (mode 0600; `.env` is added to `.gitignore`) — never to `~/.reclaim`.
213
+ - Then call `resolve_owner_key` again and `run_proof` with the returned address.
214
+ 2. All fully local — no network, no server registration. The key is read live from `.env` on each `run_proof`, so no restart is needed after issuing/importing.
215
+ - **Advanced:** setting `RECLAIM_PRIVATE_KEY` directly in the MCP server's env also works, but env vars are snapshotted at Claude Code startup, so that route needs a full restart (which invalidates every `secretParamsRef`). The `.env` flow above avoids that — prefer it.
216
+ - **Multiple MCP config files exist on Windows.** `~/.claude.json` takes precedence over `~/.claude/mcp.json`. When advising config edits, point them at the right file or tell them to use `claude mcp list` / `claude mcp add --scope user` rather than editing JSON by hand.
217
+
218
+ If `replay_request` or `run_proof` errors with `unknown secretParamsRef`:
219
+
220
+ - **The ref is bound to the MCP server process.** Anything that restarts the server (Claude Code restart, `reset_session`, machine reboot) invalidates every previously-issued ref. The fix is always: re-run `start_capture` → interact → `propose_provider` to mint a fresh ref. Don't ask the dev to "re-enter the value" — walk them back through capture.
221
+ - **Exception:** if the drafted provider has empty `secretRefs: []` (no secret headers were captured), you can call `replay_request` and `run_proof` without `secretParamsRef` at all — both tools accept it as optional in that case.
222
+
223
+ ## Out of scope (v0)
224
+
225
+ If the dev needs a provider for a flow that requires multiple sequential requests (CSRF preflight, refresh-token chains), tell them: "multi-request providers are v1. For now, we can produce a single-request provider for the target value — let's see if one exists in this capture."
package/skill/old-mode.md CHANGED
@@ -12,4 +12,6 @@ This server is running against the LEGACY Reclaim devtools backend (temporary, w
12
12
 
13
13
  Only GET/POST endpoints can be captured into providers here. There is no organization, credential-linking, or public-review workflow in this mode.
14
14
 
15
+ **Diagnosing a session:** `session_analytics_logs` fetches the analytics logs for a verification session by id (from the logs service) — use it to see what happened in a session (matches, claims, failures).
16
+
15
17
  **Templating:** bare `{{name}}` placeholders are provider params (values extracted from the response). Values the CONSUMER must supply at verification time (e.g. a username in the URL path) use the `{{context_<name>}}` form — this backend does not support the dotted `{{context.<name>}}` interpolation the new builder uses.
@@ -55,6 +55,73 @@ export function stripPseudoHeaders(
55
55
  return out
56
56
  }
57
57
 
58
+ /** Extensions for static assets we never prove over — scripts, styles, fonts,
59
+ * images, media, source maps. A real page pulls hundreds of these; tracking
60
+ * them (and fetching their bodies) just bloats the request map and body store
61
+ * with traffic no recipe ever matches. Note: `.json` is deliberately absent —
62
+ * that's the API shape recipes care about. */
63
+ const STATIC_ASSET_EXTENSIONS = new Set([
64
+ 'js', 'mjs', 'cjs', 'css', 'map',
65
+ 'png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif', 'ico', 'bmp',
66
+ 'woff', 'woff2', 'ttf', 'otf', 'eot',
67
+ 'mp4', 'webm', 'mp3', 'ogg', 'wav',
68
+ ])
69
+
70
+ /** True when the URL's path names a static asset (by file extension; query and
71
+ * hash are ignored, so `chunk.js?v=1` still counts). The capture engine drops
72
+ * these up front — they never carry JSON/HTML a recipe matches. */
73
+ export function isStaticAsset(url: string): boolean {
74
+ let path: string
75
+ try {
76
+ path = new URL(url).pathname
77
+ } catch{
78
+ path = url.split(/[?#]/)[0] || url
79
+ }
80
+
81
+ // Extension of the last path segment only, so `/v1.2/data` isn't misread.
82
+ const base = path.slice(path.lastIndexOf('/') + 1)
83
+ const dot = base.lastIndexOf('.')
84
+ if(dot <= 0) {
85
+ return false
86
+ }
87
+
88
+ return STATIC_ASSET_EXTENSIONS.has(base.slice(dot + 1).toLowerCase())
89
+ }
90
+
91
+ /** Response Content-Types for static assets. The URL-extension filter misses
92
+ * extension-less bundler/CDN URLs (e.g. `/assets/3b8f1c`), so we also drop by
93
+ * MIME at response time. `text/html`, `application/json`, and `text/plain`
94
+ * are NOT here — those are the document/API shapes recipes match. */
95
+ const STATIC_ASSET_MIME_EXACT = new Set([
96
+ 'text/css',
97
+ 'application/javascript', 'text/javascript', 'application/x-javascript',
98
+ 'application/ecmascript', 'text/ecmascript',
99
+ 'application/wasm',
100
+ 'application/font-woff', 'application/font-woff2',
101
+ 'application/vnd.ms-fontobject', 'application/x-font-ttf',
102
+ ])
103
+
104
+ const STATIC_ASSET_MIME_PREFIXES = ['image/', 'font/', 'audio/', 'video/']
105
+
106
+ /** True when a response Content-Type is a static asset (scripts/styles/fonts/
107
+ * images/media). Parameters (`; charset=…`) and case are ignored. */
108
+ export function isStaticAssetMime(mimeType: string | undefined): boolean {
109
+ if(!mimeType) {
110
+ return false
111
+ }
112
+
113
+ const m = mimeType.toLowerCase().split(';')[0].trim()
114
+ if(!m) {
115
+ return false
116
+ }
117
+
118
+ if(STATIC_ASSET_MIME_PREFIXES.some((p) => m.startsWith(p))) {
119
+ return true
120
+ }
121
+
122
+ return STATIC_ASSET_MIME_EXACT.has(m)
123
+ }
124
+
58
125
  export interface CaptureSession {
59
126
  captureId: string
60
127
  requests: Map<string, CapturedRequest>
@@ -173,6 +240,14 @@ export function createCaptureEngine(opts: CaptureEngineOptions): CaptureEngine {
173
240
  }
174
241
 
175
242
  function onRequestWillBeSent(p: RequestWillBeSentLike) {
243
+ // Static assets never match a recipe — never track them (so we don't
244
+ // fetch their bodies or grow the store). Drop any extra-info that raced
245
+ // ahead of this event so it can't linger.
246
+ if(isStaticAsset(p.request.url)) {
247
+ pendingRequestExtra.delete(p.requestId)
248
+ return
249
+ }
250
+
176
251
  const f: InFlight = {
177
252
  requestId: p.requestId,
178
253
  method: p.request.method,
@@ -197,6 +272,20 @@ export function createCaptureEngine(opts: CaptureEngineOptions): CaptureEngine {
197
272
  function onResponseReceived(p: ResponseReceivedLike) {
198
273
  const f = inFlight.get(p.requestId)
199
274
  if(!f) {
275
+ // No in-flight entry → the request was skipped (e.g. a static asset)
276
+ // or never seen. Discard any buffered response extra-info so a skipped
277
+ // asset can't leave a dangling entry.
278
+ pendingResponseExtra.delete(p.requestId)
279
+ return
280
+ }
281
+
282
+ // Second-line asset filter by Content-Type: catches extension-less
283
+ // bundler/CDN URLs the request-time URL filter can't. Forget the
284
+ // in-flight entry so it never reaches `requests` and a later
285
+ // loadingFinished/extra-info for it becomes a no-op.
286
+ if(isStaticAssetMime(p.response.mimeType)) {
287
+ inFlight.delete(p.requestId)
288
+ pendingResponseExtra.delete(p.requestId)
200
289
  return
201
290
  }
202
291
 
@@ -11,7 +11,11 @@ import {
11
11
  type StartCaptureOptions,
12
12
  } from './capture-engine.ts'
13
13
 
14
- export { stripPseudoHeaders } from './capture-engine.ts'
14
+ export {
15
+ isStaticAsset,
16
+ isStaticAssetMime,
17
+ stripPseudoHeaders,
18
+ } from './capture-engine.ts'
15
19
  export type { CaptureSession, StartCaptureOptions } from './capture-engine.ts'
16
20
 
17
21
  export async function startCapture(
package/src/consts.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  import { DEFAULT_BASE_URL } from '@reclaimprotocol/client'
19
19
  import { homedir } from 'node:os'
20
20
  import { join } from 'node:path'
21
- import { DEFAULT_OLD_API_URL } from './old/client.ts'
21
+ import { DEFAULT_OLD_API_URL, DEFAULT_OLD_LOGS_URL } from './old/client.ts'
22
22
 
23
23
  // ---------------------------------------------------------------------------
24
24
  // Default / fallback literals (used only as env fallbacks).
@@ -32,7 +32,7 @@ export const DEFAULT_LOGIN_URL = 'https://dev.reclaimprotocol.org'
32
32
 
33
33
  // Re-export the two non-env-fallback defaults so callers can keep importing
34
34
  // their URL defaults from a single place if they wish.
35
- export { DEFAULT_BASE_URL, DEFAULT_OLD_API_URL }
35
+ export { DEFAULT_BASE_URL, DEFAULT_OLD_API_URL, DEFAULT_OLD_LOGS_URL }
36
36
 
37
37
  // ---------------------------------------------------------------------------
38
38
  // Logging.
@@ -96,6 +96,11 @@ export function oldApiUrl(): string {
96
96
  return process.env.RECLAIM_OLD_API_URL || DEFAULT_OLD_API_URL
97
97
  }
98
98
 
99
+ /** Old-devtools analytics-logs service base URL (separate host). */
100
+ export function oldLogsUrl(): string {
101
+ return process.env.RECLAIM_OLD_LOGS_URL || DEFAULT_OLD_LOGS_URL
102
+ }
103
+
99
104
  /** Builder backend base URL. Shared by mcp/start and the verification tool. */
100
105
  export function apiUrl(): string {
101
106
  return process.env.RECLAIM_API_URL || DEFAULT_BASE_URL
@@ -98,7 +98,7 @@ export const MCP_TOOLS = {
98
98
  }),
99
99
  },
100
100
  "AdminProviders": {
101
- description: "All verifications (admin-only)\n\nAdmin only. Lists all verifications across all organizations. Supports filtering by visibility, status, and free-text search on title/domain/slug.",
101
+ description: "All providers (admin-only)\n\nAdmin only. Lists all providers across all organizations. Supports filtering by visibility, status, and free-text search on title/domain/slug.",
102
102
  inputSchema: toZod({
103
103
  "type": "object",
104
104
  "properties": {
@@ -485,7 +485,7 @@ export const MCP_TOOLS = {
485
485
  }),
486
486
  },
487
487
  "CreateProvider": {
488
- description: "Create a verification\n\nCreate a new Reclaim verification. Use this only after `providers` returned nothing useful for the domain. The caller must be an active member of the target organization.\nCreates a new verification in the specified organization. `visibility` is coerced to `PRIVATE` at creation regardless of what's sent; the author may toggle to `PUBLIC` later via `PATCH /providers/{id}`. While `PRIVATE`, versions go to the go live freely; once `PUBLIC`, non-admin versions need admin review. See `Provider.visibility`.\nThe server also seeds an initial `0.1.0` version on the `draft` branch in the same transaction so the verification is immediately editable. Its `initialUrl` defaults to `https://<domain>`; all other version fields fall back to their schema-declared defaults (structured verification, empty requests, empty web settings). `Provider.latestVersion` stays `null` until the draft is promoted to the live channel.",
488
+ description: "Create a provider\n\nCreate a new Reclaim provider. Use this only after `providers` returned nothing useful for the domain. The caller must be an active member of the target organization.\nCreates a new provider in the specified organization. `visibility` is coerced to `PRIVATE` at creation regardless of what's sent; the author may toggle to `PUBLIC` later via `PATCH /providers/{id}`. While `PRIVATE`, versions go to the go live freely; once `PUBLIC`, non-admin versions need admin review. See `Provider.visibility`.\nThe server also seeds an initial `0.1.0` version on the `draft` branch in the same transaction so the provider is immediately editable. Its `initialUrl` defaults to `https://<domain>`; all other version fields fall back to their schema-declared defaults (a `structured` verificationType, empty requests, empty web settings). `Provider.latestVersion` stays `null` until the draft is promoted to the live channel.",
489
489
  inputSchema: toZod({
490
490
  "type": "object",
491
491
  "properties": {
@@ -618,7 +618,7 @@ export const MCP_TOOLS = {
618
618
  }),
619
619
  },
620
620
  "CreateProviderVersion": {
621
- description: "Create a new verification version\n\nCreates a new version of the verification. The starting branch\ndepends on the verification's `visibility` and the caller's role:\n\n- `PRIVATE` verification (any caller): the live channel (no review).\n\n- `PUBLIC` verification, admin caller (Reclaim staff): the live channel\n (no review).\n\n- `PUBLIC` verification, non-admin caller: `draft` (or `ai` if\n authored by the AI assistant). Must pass review (see\n `POST /providers/{id}/versions/{version}/review`) before\n reaching the live channel.\n",
621
+ description: "Create a new provider version\n\nCreates a new version of the provider. The starting branch\ndepends on the provider's `visibility` and the caller's role:\n\n- `PRIVATE` provider (any caller): the live channel (no review).\n\n- `PUBLIC` provider, admin caller (Reclaim staff): the live channel\n (no review).\n\n- `PUBLIC` provider, non-admin caller: `draft` (or `ai` if\n authored by the AI assistant). Must pass review (see\n `POST /providers/{id}/versions/{version}/review`) before\n reaching the live channel.\n",
622
622
  inputSchema: toZod({
623
623
  "type": "object",
624
624
  "properties": {
@@ -644,7 +644,7 @@ export const MCP_TOOLS = {
644
644
  "body": {
645
645
  "type": "object",
646
646
  "additionalProperties": false,
647
- "description": "Body for creating a new version of a provider. Every new version starts on the `draft` branch — see `CreateProviderVersion`. The request body never sets `branch` directly; going live is a separate, explicit step (publish for PRIVATE, submit-for-review → approve for PUBLIC). The version's `verificationType` is always `structured` and is not settable here; a `prompt` field is reserved for a future AI authoring flow and is also not accepted on this surface.",
647
+ "description": "Body for creating a new version of a provider. Every new version starts on the `draft` branch — see `CreateProviderVersion`. The request body never sets `branch` directly; going live is a separate, explicit step (publish for PRIVATE, submit-for-review → approve for PUBLIC). `verificationType` defaults to `structured`; pass `llm` with a `prompt` to author an AI-verification provider.",
648
648
  "required": [
649
649
  "initialUrl",
650
650
  "requests",
@@ -652,6 +652,20 @@ export const MCP_TOOLS = {
652
652
  "webSettings"
653
653
  ],
654
654
  "properties": {
655
+ "verificationType": {
656
+ "type": "string",
657
+ "description": "How a provider version verifies a claim. `structured` uses the explicit match rules on this version (the `matches` / `allowedJsMatches` and related settings). `llm` defers the verification decision to a language model and requires `prompt` to be set.",
658
+ "enum": [
659
+ "llm",
660
+ "structured"
661
+ ]
662
+ },
663
+ "prompt": {
664
+ "type": "string",
665
+ "description": "Extraction prompt, used (and required) when `verificationType` is `llm`.",
666
+ "minLength": 0,
667
+ "maxLength": 1000000
668
+ },
655
669
  "version": {
656
670
  "type": "object",
657
671
  "additionalProperties": false,
@@ -1211,7 +1225,7 @@ export const MCP_TOOLS = {
1211
1225
  }),
1212
1226
  },
1213
1227
  "DecideProviderVersionReview": {
1214
- description: "Record a decision on a verification version review\n\nAdmin only (Reclaim staff). Approves or rejects a verification version currently on the `in_review` branch. `APPROVED` clears the branch tag and promotes the version to the live channel; `REJECTED` moves it to the `rejected` branch where the author can address feedback and resubmit. A rejected version stays rejected — the author either fixes and resubmits, or abandons it.\nUntil RBAC is designed, \"admin\" is detected via the user's email domain (`@reclaimprotocol.org`). Non-admin callers receive a 403.",
1228
+ description: "Record a decision on a provider version review\n\nAdmin only (Reclaim staff). Approves or rejects a provider version currently on the `in_review` branch. `APPROVED` clears the branch tag and promotes the version to the live channel; `REJECTED` moves it to the `rejected` branch where the author can address feedback and resubmit. A rejected version stays rejected — the author either fixes and resubmits, or abandons it.\nUntil RBAC is designed, \"admin\" is detected via the user's email domain (`@reclaimprotocol.org`). Non-admin callers receive a 403.",
1215
1229
  inputSchema: toZod({
1216
1230
  "type": "object",
1217
1231
  "properties": {
@@ -1384,7 +1398,7 @@ export const MCP_TOOLS = {
1384
1398
  }),
1385
1399
  },
1386
1400
  "DeleteProvider": {
1387
- description: "Delete a verification\n\nOwner of the verification's organization only. Soft-archives the verification (sets `status` to `ARCHIVED`) so historical references remain resolvable. Supports optimistic concurrency via `If-Match`.",
1401
+ description: "Delete a provider\n\nOwner of the provider's organization only. Soft-archives the provider (sets `status` to `ARCHIVED`) so historical references remain resolvable. Supports optimistic concurrency via `If-Match`.",
1388
1402
  inputSchema: toZod({
1389
1403
  "type": "object",
1390
1404
  "properties": {
@@ -1429,7 +1443,7 @@ export const MCP_TOOLS = {
1429
1443
  }),
1430
1444
  },
1431
1445
  "DeleteProviderVersion": {
1432
- description: "Delete a verification version\n\nSoft-archives a version on the `draft`, `ai`, or `rejected` branch — sets `status` to `ARCHIVED` so history is preserved. Trunk (live) and `in_review` versions cannot be archived this way; archive the parent verification instead. Supports optimistic concurrency via `If-Match`.",
1446
+ description: "Delete a provider version\n\nSoft-archives a version on the `draft`, `ai`, or `rejected` branch — sets `status` to `ARCHIVED` so history is preserved. Trunk (live) and `in_review` versions cannot be archived this way; archive the parent provider instead. Supports optimistic concurrency via `If-Match`.",
1433
1447
  inputSchema: toZod({
1434
1448
  "type": "object",
1435
1449
  "properties": {
@@ -1521,7 +1535,7 @@ export const MCP_TOOLS = {
1521
1535
  }),
1522
1536
  },
1523
1537
  "DiscoverResolve": {
1524
- description: "Resolve a website + data points to verification providers\n\nProvider resolution: given a website and the data points the user wants their users to prove, return the smallest set of existing providers whose data points COVER the selection (greedy combine), the `requiredContext` inputs the consumer must supply, and any `uncovered` points no provider exposes yet (`needsCreation` is true then — author a provider with the agent tools). When the chosen providers can be fused into one (same start page, no clashing page scripts) `merge.mergeable` is true.\n\nAgent flow: pick a website (`GET /discover/data-points` lists what's verifiable there), confirm the data points with the user, call this to get the `providers` + `requiredContext`, collect those context values from the user, then `POST /verifications/sessions` with `providers` (pass every id) and `context` to mint a verification.",
1538
+ description: "Resolve a website + data points to providers\n\nProvider resolution: given a website and the data points the user wants their users to prove, return the smallest set of existing providers whose data points COVER the selection (greedy combine), the `requiredContext` inputs the consumer must supply, and any `uncovered` points no provider exposes yet (`needsCreation` is true then — author a provider with the agent tools). When the chosen providers can be fused into one (same start page, no clashing page scripts) `merge.mergeable` is true.\n\nAgent flow: pick a website (`GET /discover/data-points` lists what's verifiable there), confirm the data points with the user, call this to get the `providers` + `requiredContext`, collect those context values from the user, then `POST /verifications/sessions` with `providers` (pass every id) and `context` to mint a verification.",
1525
1539
  inputSchema: toZod({
1526
1540
  "type": "object",
1527
1541
  "properties": {
@@ -1792,7 +1806,7 @@ export const MCP_TOOLS = {
1792
1806
  }),
1793
1807
  },
1794
1808
  "GetProvider": {
1795
- description: "Get a verification\n\nFetch a single Reclaim verification by id. Returns every field needed\nto integrate it into a flow: domain, the live version (initialUrl + requests),\nand the data points it can prove. Use `providers` first if you only\nhave a query string.\n\nReturns the verification record.\n\n**Canonical URL.** For SEO and stable sharing, the canonical\nHTML URL for a verification is `/providers/{providerId}/{slug}`\n(see `GetProviderBySlug`). HTML callers (`Accept: text/html`)\nthat hit this route — which omits the slug — receive a `301`\nredirect to the slugged URL. JSON callers\n(`Accept: application/json`) receive the body directly without\nredirect; the slug is on the response.\n",
1809
+ description: "Get a provider\n\nFetch a single Reclaim provider by id. Returns every field needed\nto integrate it into a flow: domain, the live version (initialUrl + requests),\nand the data points it can prove. Use `providers` first if you only\nhave a query string.\n\nReturns the provider record.\n\n**Canonical URL.** For SEO and stable sharing, the canonical\nHTML URL for a provider is `/providers/{providerId}/{slug}`\n(see `GetProviderBySlug`). HTML callers (`Accept: text/html`)\nthat hit this route — which omits the slug — receive a `301`\nredirect to the slugged URL. JSON callers\n(`Accept: application/json`) receive the body directly without\nredirect; the slug is on the response.\n",
1796
1810
  inputSchema: toZod({
1797
1811
  "type": "object",
1798
1812
  "properties": {
@@ -1827,7 +1841,7 @@ export const MCP_TOOLS = {
1827
1841
  }),
1828
1842
  },
1829
1843
  "GetProviderVersion": {
1830
- description: "Get a specific verification version",
1844
+ description: "Get a specific provider version",
1831
1845
  inputSchema: toZod({
1832
1846
  "type": "object",
1833
1847
  "properties": {
@@ -2165,7 +2179,7 @@ export const MCP_TOOLS = {
2165
2179
  }),
2166
2180
  },
2167
2181
  "ListProviderVersions": {
2168
- description: "List a verification's versions\n\nLists versions visible to the caller. Non-public branches (`ai`, `draft`, `in_review`, `rejected`) are only visible to members of the verification's owning organization.",
2182
+ description: "List a provider's versions\n\nLists versions visible to the caller. Non-public branches (`ai`, `draft`, `in_review`, `rejected`) are only visible to members of the provider's owning organization.",
2169
2183
  inputSchema: toZod({
2170
2184
  "type": "object",
2171
2185
  "properties": {
@@ -2320,7 +2334,7 @@ export const MCP_TOOLS = {
2320
2334
  }),
2321
2335
  },
2322
2336
  "Providers": {
2323
- description: "Search verifications\n\nSearch published Reclaim verifications by domain or data point. A verification is the recipe an end-user follows to prove something about themselves (e.g. \"I have N followers on Twitter\"). Returns a list of matches. Call this BEFORE `create_provider` — most popular domains already have a public verification you can reuse.\nCatalog page listing all verifications. Returns a paginated list of verifications the caller can see. Public verifications are visible to everyone; private verifications are only visible to org members.",
2337
+ description: "Search providers\n\nSearch published Reclaim providers by domain or data point. A provider is the recipe an end-user follows to prove something about themselves (e.g. \"I have N followers on Twitter\"). Returns a list of matches. Call this BEFORE `create_provider` — most popular domains already have a public provider you can reuse.\nCatalog page listing all providers. Returns a paginated list of providers the caller can see. Public providers are visible to everyone; private providers are only visible to org members.",
2324
2338
  inputSchema: toZod({
2325
2339
  "type": "object",
2326
2340
  "properties": {
@@ -2366,7 +2380,7 @@ export const MCP_TOOLS = {
2366
2380
  }),
2367
2381
  },
2368
2382
  "PublishProviderVersion": {
2369
- description: "Publish a draft version directly (PRIVATE providers only)\n\nPublishes a verification version on the `draft` branch directly to the live channel (trunk). Only allowed for PRIVATE providers — PUBLIC providers must use the review flow (`POST .../review` → admin decision). On success the version's branch transitions from `draft` to `null` (trunk/live).",
2383
+ description: "Publish a draft version directly (PRIVATE providers only)\n\nPublishes a provider version on the `draft` branch directly to the live channel (trunk). Only allowed for PRIVATE providers — PUBLIC providers must use the review flow (`POST .../review` → admin decision). On success the version's branch transitions from `draft` to `null` (trunk/live).",
2370
2384
  inputSchema: toZod({
2371
2385
  "type": "object",
2372
2386
  "properties": {
@@ -2615,7 +2629,7 @@ export const MCP_TOOLS = {
2615
2629
  }),
2616
2630
  },
2617
2631
  "SubmitProviderVersionForReview": {
2618
- description: "Submit a verification version for review\n\nSubmits a verification version (currently on the `draft` or `rejected` branch) for admin review. On success the version's branch transitions to `in_review`. Callable by any authenticated member of the verification's owning org. Meaningful only on `PUBLIC` verifications — on `PRIVATE` verifications new versions go to the live channel directly and never need review.",
2632
+ description: "Submit a provider version for review\n\nSubmits a provider version (currently on the `draft` or `rejected` branch) for admin review. On success the version's branch transitions to `in_review`. Callable by any authenticated member of the provider's owning org. Meaningful only on `PUBLIC` providers — on `PRIVATE` providers new versions go to the live channel directly and never need review.",
2619
2633
  inputSchema: toZod({
2620
2634
  "type": "object",
2621
2635
  "properties": {
@@ -2666,7 +2680,7 @@ export const MCP_TOOLS = {
2666
2680
  }),
2667
2681
  },
2668
2682
  "TransferProvider": {
2669
- description: "Transfer a verification to a different workspace\n\nAdmin-only. Moves the verification's `orgId` to the supplied workspace; preserves slug, versions, members. SQL function `builder.transfer_provider` enforces the admin gate and target existence atomically.",
2683
+ description: "Transfer a provider to a different workspace\n\nAdmin-only. Moves the provider's `orgId` to the supplied workspace; preserves slug, versions, members. SQL function `builder.transfer_provider` enforces the admin gate and target existence atomically.",
2670
2684
  inputSchema: toZod({
2671
2685
  "type": "object",
2672
2686
  "properties": {
@@ -2825,7 +2839,7 @@ export const MCP_TOOLS = {
2825
2839
  }),
2826
2840
  },
2827
2841
  "UpdateProvider": {
2828
- description: "Update a verification\n\nUpdate a Reclaim verification's metadata. Partial PATCH — pass only fields you want to change. The caller must be an active member of the owning organization. To change the verification's recipe (initialUrl, requests, …) cut a new version via the dashboard. Use `seo` to control how the verification appears in search results and social shares; omit a subfield to keep the server default.\nCaller must be a member of the verification's owning organization. `id`, `orgId`, and `slug` are immutable. Supports optimistic concurrency via `If-Match`.",
2842
+ description: "Update a provider\n\nUpdate a Reclaim provider's metadata. Partial PATCH — pass only fields you want to change. The caller must be an active member of the owning organization. To change the provider's recipe (initialUrl, requests, …) cut a new version via the dashboard. Use `seo` to control how the provider appears in search results and social shares; omit a subfield to keep the server default.\nCaller must be a member of the provider's owning organization. `id`, `orgId`, and `slug` are immutable. Supports optimistic concurrency via `If-Match`.",
2829
2843
  inputSchema: toZod({
2830
2844
  "type": "object",
2831
2845
  "properties": {
@@ -2962,7 +2976,7 @@ export const MCP_TOOLS = {
2962
2976
  }),
2963
2977
  },
2964
2978
  "UpdateProviderVersion": {
2965
- description: "Update a verification version\n\nMutable only while the version is on the `draft` or `rejected` branch. Versions on `in_review` or the live channel reject all writes here. Caller must be a member of the verification's owning organization. Supports optimistic concurrency via `If-Match`.",
2979
+ description: "Update a provider version\n\nMutable only while the version is on the `draft` or `rejected` branch. Versions on `in_review` or the live channel reject all writes here. Caller must be a member of the provider's owning organization. Supports optimistic concurrency via `If-Match`.",
2966
2980
  inputSchema: toZod({
2967
2981
  "type": "object",
2968
2982
  "properties": {
@@ -3004,13 +3018,27 @@ export const MCP_TOOLS = {
3004
3018
  "body": {
3005
3019
  "type": "object",
3006
3020
  "additionalProperties": false,
3007
- "description": "Partial update of a provider version. Mutable only while the version is on the `draft` or `rejected` branch; versions on `in_review` or the live channel reject all writes here. The semver does not change on update — `build_number` increments instead, giving callers a monotonic revision counter without touching the (major, minor, patch) PK. `verificationType` / `prompt` are not accepted; see `ProviderVersion` for why.",
3021
+ "description": "Partial update of a provider version. Mutable only while the version is on the `draft` or `rejected` branch; versions on `in_review` or the live channel reject all writes here. The semver does not change on update — `build_number` increments instead, giving callers a monotonic revision counter without touching the (major, minor, patch) PK. `verificationType` and `prompt` may be updated; omit either to carry the prior build's value forward.",
3008
3022
  "properties": {
3009
3023
  "initialUrl": {
3010
3024
  "type": "string",
3011
3025
  "format": "uri",
3012
3026
  "maxLength": 300
3013
3027
  },
3028
+ "verificationType": {
3029
+ "type": "string",
3030
+ "description": "How a provider version verifies a claim. `structured` uses the explicit match rules on this version (the `matches` / `allowedJsMatches` and related settings). `llm` defers the verification decision to a language model and requires `prompt` to be set.",
3031
+ "enum": [
3032
+ "llm",
3033
+ "structured"
3034
+ ]
3035
+ },
3036
+ "prompt": {
3037
+ "type": "string",
3038
+ "description": "Extraction prompt, used when `verificationType` is `llm`.",
3039
+ "minLength": 0,
3040
+ "maxLength": 1000000
3041
+ },
3014
3042
  "requests": {
3015
3043
  "type": "array",
3016
3044
  "minItems": 0,