@reclaimprotocol/agent 0.1.2 → 0.1.4

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 (42) hide show
  1. package/README.md +7 -38
  2. package/lib/cdp/capture-engine.d.ts +7 -0
  3. package/lib/cdp/capture-engine.js +78 -0
  4. package/lib/cdp/capture.d.ts +1 -1
  5. package/lib/cdp/capture.js +1 -1
  6. package/lib/consts.d.ts +4 -2
  7. package/lib/consts.js +6 -2
  8. package/lib/mcp/api/operations.gen.d.ts +8 -0
  9. package/lib/mcp/api/operations.gen.js +92 -0
  10. package/lib/mcp/instructions.d.ts +2 -0
  11. package/lib/mcp/instructions.js +16 -0
  12. package/lib/mcp/start.js +2 -1
  13. package/lib/mcp/tools/agent/build.js +4 -0
  14. package/lib/mcp/tools/agent/how-it-works.d.ts +11 -0
  15. package/lib/mcp/tools/agent/how-it-works.js +24 -0
  16. package/lib/old/client.d.ts +10 -0
  17. package/lib/old/client.js +27 -0
  18. package/lib/old/to-register.d.ts +5 -3
  19. package/lib/old/to-register.js +14 -6
  20. package/lib/old/tools/build.d.ts +2 -2
  21. package/lib/old/tools/build.js +4 -2
  22. package/lib/old/tools/publish.js +45 -10
  23. package/lib/old/tools/session-analytics-logs.d.ts +9 -0
  24. package/lib/old/tools/session-analytics-logs.js +25 -0
  25. package/package.json +2 -3
  26. package/skill/{SKILL.md → SKILL.builder.md} +3 -36
  27. package/skill/SKILL.old.md +221 -0
  28. package/skill/builder-mode.md +2 -0
  29. package/skill/old-mode.md +4 -2
  30. package/src/cdp/capture-engine.ts +89 -0
  31. package/src/cdp/capture.ts +5 -1
  32. package/src/consts.ts +7 -2
  33. package/src/mcp/api/operations.gen.ts +96 -0
  34. package/src/mcp/instructions.ts +18 -0
  35. package/src/mcp/start.ts +2 -0
  36. package/src/mcp/tools/agent/build.ts +4 -0
  37. package/src/mcp/tools/agent/how-it-works.ts +32 -0
  38. package/src/old/client.ts +32 -0
  39. package/src/old/to-register.ts +15 -6
  40. package/src/old/tools/build.ts +4 -2
  41. package/src/old/tools/publish.ts +55 -11
  42. package/src/old/tools/session-analytics-logs.ts +33 -0
@@ -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
@@ -1479,6 +1479,102 @@ export const MCP_TOOLS = {
1479
1479
  "additionalProperties": false
1480
1480
  }),
1481
1481
  },
1482
+ "DiscoverDataPoints": {
1483
+ description: "List the verifiable data points on a website\n\nThe data points existing providers can prove on a website — the menu to pick from before `GET /discover/resolve`. Optionally filter by `q`.",
1484
+ inputSchema: toZod({
1485
+ "type": "object",
1486
+ "properties": {
1487
+ "params": {
1488
+ "type": "object",
1489
+ "properties": {
1490
+
1491
+ }
1492
+ },
1493
+ "query": {
1494
+ "type": "object",
1495
+ "properties": {
1496
+ "domain": {
1497
+ "type": "string",
1498
+ "format": "hostname",
1499
+ "minLength": 1,
1500
+ "maxLength": 200
1501
+ },
1502
+ "q": {
1503
+ "type": "string",
1504
+ "maxLength": 200
1505
+ }
1506
+ },
1507
+ "required": [
1508
+ "domain"
1509
+ ],
1510
+ "additionalProperties": false
1511
+ },
1512
+ "body": {
1513
+ "type": "object"
1514
+ }
1515
+ },
1516
+ "required": [
1517
+ "params",
1518
+ "query"
1519
+ ],
1520
+ "additionalProperties": false
1521
+ }),
1522
+ },
1523
+ "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.",
1525
+ inputSchema: toZod({
1526
+ "type": "object",
1527
+ "properties": {
1528
+ "params": {
1529
+ "type": "object",
1530
+ "properties": {
1531
+
1532
+ }
1533
+ },
1534
+ "query": {
1535
+ "type": "object",
1536
+ "properties": {
1537
+ "domain": {
1538
+ "type": "string",
1539
+ "format": "hostname",
1540
+ "minLength": 1,
1541
+ "maxLength": 200
1542
+ },
1543
+ "dataPoints": {
1544
+ "type": "array",
1545
+ "minItems": 1,
1546
+ "maxItems": 200,
1547
+ "items": {
1548
+ "type": "string",
1549
+ "minLength": 1,
1550
+ "maxLength": 200
1551
+ }
1552
+ },
1553
+ "mode": {
1554
+ "type": "string",
1555
+ "enum": [
1556
+ "combine",
1557
+ "ai"
1558
+ ]
1559
+ }
1560
+ },
1561
+ "required": [
1562
+ "domain",
1563
+ "dataPoints"
1564
+ ],
1565
+ "additionalProperties": false
1566
+ },
1567
+ "body": {
1568
+ "type": "object"
1569
+ }
1570
+ },
1571
+ "required": [
1572
+ "params",
1573
+ "query"
1574
+ ],
1575
+ "additionalProperties": false
1576
+ }),
1577
+ },
1482
1578
  "GetAttachSession": {
1483
1579
  description: "Poll a pairing session\n\nAnonymous poll endpoint used by the CLI / MCP while it waits for the human to complete the dashboard step. Once `status` is `LINKED`, the response includes a `token` (2-day bearer token) the CLI / MCP can use for authenticated API calls. Expired or unknown codes return `404`.",
1484
1580
  inputSchema: toZod({
@@ -13,3 +13,21 @@ export const BUILDER_MODE_INSTRUCTIONS = readFileSync(
13
13
  fileURLToPath(new URL('../../skill/builder-mode.md', import.meta.url)),
14
14
  'utf8',
15
15
  ).trim()
16
+
17
+ /**
18
+ * The full provider-authoring guide — the deep capture→prove→publish workflow,
19
+ * decision frameworks, and troubleshooting. Served on demand by the
20
+ * `how_it_works` MCP tool so every client (not just Claude Code, which
21
+ * auto-loads skills) can pull it. There's a guide PER MODE so an agent is never
22
+ * shown auth/publish tools that don't exist in its backend: the server picks
23
+ * one by `oldMode` (see `mcp/start.ts`). Same `../../skill/` path resolution as
24
+ * the instructions above; any leading frontmatter is stripped defensively.
25
+ */
26
+ function readGuide(file: string): string {
27
+ return readFileSync(fileURLToPath(new URL(file, import.meta.url)), 'utf8')
28
+ .replace(/^---\n[\s\S]*?\n---\n/, '')
29
+ .trim()
30
+ }
31
+
32
+ export const PROVIDER_GUIDE_BUILDER = readGuide('../../skill/SKILL.builder.md')
33
+ export const PROVIDER_GUIDE_OLD = readGuide('../../skill/SKILL.old.md')
package/src/mcp/start.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  apiToken,
7
7
  apiUrl,
8
8
  oldApiUrl,
9
+ oldLogsUrl,
9
10
  useOldDevtools,
10
11
  } from '../consts.ts'
11
12
  import { ReclaimOldClient } from '../old/client.ts'
@@ -49,6 +50,7 @@ export async function startMcpServer() {
49
50
  // get_me_providers instead.
50
51
  const oldClient = new ReclaimOldClient({
51
52
  baseUrl: oldApiUrl(),
53
+ logsBaseUrl: oldLogsUrl(),
52
54
  identity: resolveInitialIdentity(),
53
55
  })
54
56
 
@@ -17,6 +17,7 @@ import { type AttachState, attachTool } from './attach.ts'
17
17
  import { type CaptureStore, captureTools } from './capture.ts'
18
18
  import { browserTools } from './eval.ts'
19
19
  import { findTool } from './find.ts'
20
+ import { howItWorksTool } from './how-it-works.ts'
20
21
  import { proofTool } from './proof.ts'
21
22
  import {
22
23
  makeSecretStore,
@@ -52,6 +53,9 @@ export function buildAgentTools(
52
53
  const state = buildAgentState()
53
54
  ensureAttestorCrypto()
54
55
  const tools: RegisteredTool[] = []
56
+ // Stateless docs tool — serve the mode-specific authoring guide to any
57
+ // client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
58
+ tools.push(howItWorksTool(!client))
55
59
  tools.push(attachTool(state.attach))
56
60
  for(const t of captureTools(state.attach, state.captures)) {
57
61
  tools.push(t)
@@ -0,0 +1,32 @@
1
+ import {
2
+ PROVIDER_GUIDE_BUILDER,
3
+ PROVIDER_GUIDE_OLD,
4
+ } from '../../instructions.ts'
5
+ import { defineTool, type RegisteredTool } from '../../server.ts'
6
+
7
+ /**
8
+ * Serves the full provider-authoring guide on demand. The MCP server's
9
+ * always-on `instructions` are a terse tool-flow reference; this tool returns
10
+ * the deep version (workflow, decision frameworks, gotchas, "never do" rules)
11
+ * so any MCP client — not just Claude Code, which alone auto-loads skills — can
12
+ * pull it. `oldMode` selects the backend-specific guide so the agent is never
13
+ * told to call auth/publish tools that aren't registered in its mode.
14
+ * Stateless: callable before `attach_chrome`.
15
+ */
16
+ export function howItWorksTool(oldMode: boolean): RegisteredTool {
17
+ const guide = oldMode ? PROVIDER_GUIDE_OLD : PROVIDER_GUIDE_BUILDER
18
+ return defineTool<Record<string, never>>(
19
+ {
20
+ name: 'how_it_works',
21
+ description:
22
+ 'Return the full Reclaim provider-authoring guide for THIS server: '
23
+ + 'the capture → draft → replay → prove → publish workflow, decision '
24
+ + 'frameworks (login detection, OPRF privacy, context vs bare params), '
25
+ + 'multi-request handling, troubleshooting, and what never to do. '
26
+ + 'Call this BEFORE building a provider unless you are already '
27
+ + 'fluent in the flow.',
28
+ inputSchema: { type: 'object', properties: {} },
29
+ },
30
+ async() => ({ guide }),
31
+ )
32
+ }
package/src/old/client.ts CHANGED
@@ -6,6 +6,10 @@ import { NAME, VERSION } from '../pkg.ts'
6
6
  * `RECLAIM_OLD_API_URL`. */
7
7
  export const DEFAULT_OLD_API_URL = 'https://devapi.reclaimprotocol.org'
8
8
 
9
+ /** Default origin of the OLD-devtools analytics-logs service — a SEPARATE host
10
+ * from the devtools API. Override with `RECLAIM_OLD_LOGS_URL`. */
11
+ export const DEFAULT_OLD_LOGS_URL = 'https://logs.reclaimprotocol.org'
12
+
9
13
  /**
10
14
  * Externally-supplied caller identity. We do NOT mint these — the dev
11
15
  * provides one:
@@ -63,15 +67,19 @@ interface RequestOpts {
63
67
  */
64
68
  export class ReclaimOldClient {
65
69
  readonly baseUrl: string
70
+ readonly logsBaseUrl: string
66
71
  #identity?: OldIdentity
67
72
  #fetch: typeof fetch
68
73
 
69
74
  constructor(opts: {
70
75
  baseUrl?: string
76
+ logsBaseUrl?: string
71
77
  identity?: OldIdentity
72
78
  fetch?: typeof fetch
73
79
  } = {}) {
74
80
  this.baseUrl = (opts.baseUrl || DEFAULT_OLD_API_URL).replace(/\/+$/, '')
81
+ this.logsBaseUrl = (opts.logsBaseUrl || DEFAULT_OLD_LOGS_URL)
82
+ .replace(/\/+$/, '')
75
83
  this.#identity = opts.identity
76
84
  this.#fetch = opts.fetch ?? fetch
77
85
  }
@@ -113,6 +121,30 @@ export class ReclaimOldClient {
113
121
  )
114
122
  }
115
123
 
124
+ /** Analytics logs for a verification session, from the logs service (a
125
+ * DIFFERENT host than the devtools API — hence its own fetch, not
126
+ * `#request`). Attaches the caller identity when one is set; queried
127
+ * anonymously otherwise. */
128
+ async getSessionAnalyticsLogs(sessionId: string): Promise<unknown> {
129
+ const url = `${this.logsBaseUrl}/api/analytics-logs/session/`
130
+ + encodeURIComponent(sessionId)
131
+ const headers: Record<string, string> = {
132
+ Accept: 'application/json',
133
+ 'User-Agent': `${NAME}/${VERSION}`,
134
+ }
135
+ if(this.#identity) {
136
+ headers[this.#authHeaderName()] = this.#authHeaderValue()
137
+ }
138
+
139
+ const res = await this.#fetch(url, { method: 'GET', headers })
140
+ const json = await this.#parseBody(res)
141
+ if(!res.ok) {
142
+ throw this.#asBoom(res, json)
143
+ }
144
+
145
+ return json
146
+ }
147
+
116
148
  getMyProviders(query: MyProvidersQuery = {}): Promise<unknown> {
117
149
  // Every one of these query params is REQUIRED by the backend
118
150
  // validator (missing/invalid → 400), so default them all here.
@@ -150,19 +150,28 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
150
150
  return requestData
151
151
  }
152
152
 
153
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
153
+ /** One `requestData` entry per drafted provider. A multi-request provider
154
+ * becomes parallel entries that the old backend runs together at verification
155
+ * (the same way the builder version carries multiple `requests`). */
156
+ function requestDataFrom(providers: ReclaimProvider[]): OldRequestData[] {
157
+ return providers.map(requestDataFromProvider)
158
+ }
159
+
160
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
161
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
162
+ * come from the first draft. */
154
163
  export function providerToRegisterBody(
155
- provider: ReclaimProvider,
164
+ providers: ReclaimProvider[],
156
165
  opts: OldProviderOpts,
157
166
  ): RegisterProviderBody {
158
167
  const body: RegisterProviderBody = {
159
- name: provider.name,
168
+ name: providers[0].name,
160
169
  loginUrl: opts.initialUrl,
161
170
  providerType: opts.providerType ?? 'PRIVATE',
162
171
  // Web-settings defaults mirror the builder openapi defaults.
163
172
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
164
173
  useProxy: opts.useProxy ?? DEFAULT_USE_PROXY,
165
- requestData: [requestDataFromProvider(provider)],
174
+ requestData: requestDataFrom(providers),
166
175
  }
167
176
  if(opts.description !== undefined) {
168
177
  body.description = opts.description
@@ -175,7 +184,7 @@ export function providerToRegisterBody(
175
184
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
176
185
  * required and must be ≥10 chars (enforced by the backend). */
177
186
  export function providerToConfigBody(
178
- provider: ReclaimProvider,
187
+ providers: ReclaimProvider[],
179
188
  opts: OldProviderOpts,
180
189
  version: string,
181
190
  versionInfo: string,
@@ -186,7 +195,7 @@ export function providerToConfigBody(
186
195
  const providerConfig: Record<string, unknown> = {
187
196
  loginUrl: opts.initialUrl,
188
197
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
189
- requestData: [requestDataFromProvider(provider)],
198
+ requestData: requestDataFrom(providers),
190
199
  }
191
200
 
192
201
  return { providerConfig, version, versionInfo }
@@ -3,12 +3,13 @@ import type { ReclaimOldClient } from '../client.ts'
3
3
  import { authenticateTool } from './authenticate.ts'
4
4
  import { getMeProvidersTool } from './me-providers.ts'
5
5
  import { createVersionFromCaptureTool } from './publish.ts'
6
+ import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
6
7
 
7
8
  /**
8
9
  * The backend-facing tools for OLD-devtools mode. Mirrors
9
10
  * `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
10
- * list for `registerToolList`. These three replace the builder credential +
11
- * publish tools and the (disabled) generated provider tools.
11
+ * list for `registerToolList`. These replace the builder credential + publish
12
+ * tools and the (disabled) generated provider tools.
12
13
  */
13
14
  export function buildOldBackendTools(
14
15
  client: ReclaimOldClient,
@@ -17,5 +18,6 @@ export function buildOldBackendTools(
17
18
  authenticateTool(client),
18
19
  createVersionFromCaptureTool(client),
19
20
  getMeProvidersTool(client),
21
+ sessionAnalyticsLogsTool(client),
20
22
  ]
21
23
  }
@@ -62,7 +62,8 @@ export function createVersionFromCaptureTool(
62
62
  client: ReclaimOldClient,
63
63
  ): RegisteredTool {
64
64
  return defineTool<{
65
- provider: ReclaimProvider
65
+ provider?: ReclaimProvider
66
+ providers?: ReclaimProvider[]
66
67
  initialUrl: string
67
68
  providerId?: string
68
69
  description?: string
@@ -74,10 +75,15 @@ export function createVersionFromCaptureTool(
74
75
  {
75
76
  name: 'create_provider_version_from_capture',
76
77
  description:
77
- 'Publish a drafted ReclaimProvider to the OLD devtools backend. ' +
78
+ 'Publish drafted ReclaimProvider(s) to the OLD devtools backend. ' +
78
79
  'Pass the `provider` object from propose_provider VERBATIM and the ' +
79
80
  '`initialUrl` the user landed on (the login/landing page, NOT the ' +
80
81
  'API url). \n\n' +
82
+ 'For a MULTI-REQUEST provider, pass `providers: [...]` (each entry a ' +
83
+ 'proven draft from propose_provider) — every entry becomes one ' +
84
+ '`requestData` entry on the SAME version, and all are verified ' +
85
+ 'together. Pass a single `provider` for the one-request case ' +
86
+ '(provide exactly one of `provider` / `providers`). \n\n' +
81
87
  'Omit `providerId` to REGISTER A NEW provider (creates it + version ' +
82
88
  '1.0.0; re-running creates a duplicate). Pass `providerId` (from ' +
83
89
  'get_me_providers) to add a NEW VERSION to that existing provider ' +
@@ -93,7 +99,8 @@ export function createVersionFromCaptureTool(
93
99
  provider: {
94
100
  type: 'object',
95
101
  description:
96
- 'The `provider` object from propose_provider, verbatim.',
102
+ 'A single `provider` object from propose_provider, verbatim. ' +
103
+ 'For multiple requests use `providers` instead.',
97
104
  properties: {
98
105
  name: { type: 'string' },
99
106
  url: { type: 'string' },
@@ -106,6 +113,29 @@ export function createVersionFromCaptureTool(
106
113
  },
107
114
  required: ['name', 'url', 'method', 'responseMatches'],
108
115
  },
116
+ providers: {
117
+ type: 'array',
118
+ description:
119
+ 'Multiple proven drafts → one multi-request version (each ' +
120
+ 'becomes a parallel `requestData` entry, all verified ' +
121
+ 'together). Each item has the same shape as `provider`. Pass ' +
122
+ 'this OR `provider`, not both.',
123
+ minItems: 1,
124
+ items: {
125
+ type: 'object',
126
+ properties: {
127
+ name: { type: 'string' },
128
+ url: { type: 'string' },
129
+ method: { type: 'string' },
130
+ headers: { type: 'object' },
131
+ body: { type: 'string' },
132
+ responseMatches: { type: 'array' },
133
+ responseRedactions: { type: 'array' },
134
+ paramValues: { type: 'object' },
135
+ },
136
+ required: ['name', 'url', 'method', 'responseMatches'],
137
+ },
138
+ },
109
139
  initialUrl: {
110
140
  type: 'string',
111
141
  description:
@@ -142,10 +172,24 @@ export function createVersionFromCaptureTool(
142
172
  'chars or a default is used).',
143
173
  },
144
174
  },
145
- required: ['provider', 'initialUrl'],
175
+ required: ['initialUrl'],
146
176
  },
147
177
  },
148
178
  async(args) => {
179
+ // Exactly one of `provider` / `providers` — normalize to an array so a
180
+ // multi-request capture publishes every request, not just the first.
181
+ const hasMany = Array.isArray(args.providers) && args.providers.length > 0
182
+ const hasOne = !!args.provider
183
+ if(hasMany === hasOne) {
184
+ throw new Boom(
185
+ 'Pass exactly one of `provider` (a single draft) or `providers` ' +
186
+ '(an array of 1+ drafts).',
187
+ { statusCode: 400 },
188
+ )
189
+ }
190
+
191
+ const providers = hasMany ? args.providers! : [args.provider!]
192
+
149
193
  const opts: OldProviderOpts = { initialUrl: args.initialUrl }
150
194
  if(args.description !== undefined) {
151
195
  opts.description = args.description
@@ -163,11 +207,11 @@ export function createVersionFromCaptureTool(
163
207
  opts.providerType = args.providerType
164
208
  }
165
209
 
166
- // Non-blocking advisory — never alters or blocks the publish; the
167
- // author keeps full control. Attaches under `_notes` when relevant.
168
- const notes = matchRedactionWarnings([
169
- countsFor('Request', args.provider),
170
- ])
210
+ // Non-blocking advisory per request — never alters or blocks the
211
+ // publish; the author keeps full control. Attaches under `_notes`.
212
+ const notes = matchRedactionWarnings(
213
+ providers.map((p, i) => countsFor(`Request ${i + 1}`, p)),
214
+ )
171
215
  const withNotes = (result: unknown) => {
172
216
  if(notes.length && result && typeof result === 'object') {
173
217
  return { ...result, _notes: notes }
@@ -178,7 +222,7 @@ export function createVersionFromCaptureTool(
178
222
 
179
223
  // New provider.
180
224
  if(!args.providerId) {
181
- const body = providerToRegisterBody(args.provider, opts)
225
+ const body = providerToRegisterBody(providers, opts)
182
226
  return withNotes(await client.registerProvider(body))
183
227
  }
184
228
 
@@ -195,7 +239,7 @@ export function createVersionFromCaptureTool(
195
239
  ? args.notes.trim()
196
240
  : DEFAULT_VERSION_INFO
197
241
  const body = providerToConfigBody(
198
- args.provider,
242
+ providers,
199
243
  opts,
200
244
  version,
201
245
  versionInfo,
@@ -0,0 +1,33 @@
1
+ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
2
+ import type { ReclaimOldClient } from '../client.ts'
3
+
4
+ /**
5
+ * Fetch analytics logs for a verification session from the OLD-devtools logs
6
+ * service (`GET https://logs.reclaimprotocol.org/api/analytics-logs/session/
7
+ * :sessionId`). Old mode only — useful for diagnosing why a session failed or
8
+ * stalled. The logs service is a separate host from the devtools API.
9
+ */
10
+ export function sessionAnalyticsLogsTool(
11
+ client: ReclaimOldClient,
12
+ ): RegisteredTool {
13
+ return defineTool<{ sessionId: string }>(
14
+ {
15
+ name: 'session_analytics_logs',
16
+ description:
17
+ 'Fetch analytics logs for a verification session by id from the '
18
+ + 'old-devtools logs service — use to diagnose what happened in a '
19
+ + 'session (matches, claims, failures). Pass the `sessionId`.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ sessionId: {
24
+ type: 'string',
25
+ description: 'The verification session id to fetch logs for.',
26
+ },
27
+ },
28
+ required: ['sessionId'],
29
+ },
30
+ },
31
+ async(args) => client.getSessionAnalyticsLogs(args.sessionId),
32
+ )
33
+ }