@reclaimprotocol/agent 0.1.2 → 0.1.3

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.
@@ -75,6 +75,14 @@ export declare const MCP_TOOLS: {
75
75
  description: string;
76
76
  inputSchema: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
77
77
  };
78
+ DiscoverDataPoints: {
79
+ description: string;
80
+ inputSchema: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
81
+ };
82
+ DiscoverResolve: {
83
+ description: string;
84
+ inputSchema: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
85
+ };
78
86
  GetAttachSession: {
79
87
  description: string;
80
88
  inputSchema: import("zod").ZodType<unknown, unknown, import("zod/v4/core").$ZodTypeInternals<unknown, unknown>>;
@@ -1421,6 +1421,98 @@ export const MCP_TOOLS = {
1421
1421
  "additionalProperties": false
1422
1422
  }),
1423
1423
  },
1424
+ "DiscoverDataPoints": {
1425
+ 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`.",
1426
+ inputSchema: toZod({
1427
+ "type": "object",
1428
+ "properties": {
1429
+ "params": {
1430
+ "type": "object",
1431
+ "properties": {}
1432
+ },
1433
+ "query": {
1434
+ "type": "object",
1435
+ "properties": {
1436
+ "domain": {
1437
+ "type": "string",
1438
+ "format": "hostname",
1439
+ "minLength": 1,
1440
+ "maxLength": 200
1441
+ },
1442
+ "q": {
1443
+ "type": "string",
1444
+ "maxLength": 200
1445
+ }
1446
+ },
1447
+ "required": [
1448
+ "domain"
1449
+ ],
1450
+ "additionalProperties": false
1451
+ },
1452
+ "body": {
1453
+ "type": "object"
1454
+ }
1455
+ },
1456
+ "required": [
1457
+ "params",
1458
+ "query"
1459
+ ],
1460
+ "additionalProperties": false
1461
+ }),
1462
+ },
1463
+ "DiscoverResolve": {
1464
+ 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.",
1465
+ inputSchema: toZod({
1466
+ "type": "object",
1467
+ "properties": {
1468
+ "params": {
1469
+ "type": "object",
1470
+ "properties": {}
1471
+ },
1472
+ "query": {
1473
+ "type": "object",
1474
+ "properties": {
1475
+ "domain": {
1476
+ "type": "string",
1477
+ "format": "hostname",
1478
+ "minLength": 1,
1479
+ "maxLength": 200
1480
+ },
1481
+ "dataPoints": {
1482
+ "type": "array",
1483
+ "minItems": 1,
1484
+ "maxItems": 200,
1485
+ "items": {
1486
+ "type": "string",
1487
+ "minLength": 1,
1488
+ "maxLength": 200
1489
+ }
1490
+ },
1491
+ "mode": {
1492
+ "type": "string",
1493
+ "enum": [
1494
+ "combine",
1495
+ "ai"
1496
+ ]
1497
+ }
1498
+ },
1499
+ "required": [
1500
+ "domain",
1501
+ "dataPoints"
1502
+ ],
1503
+ "additionalProperties": false
1504
+ },
1505
+ "body": {
1506
+ "type": "object"
1507
+ }
1508
+ },
1509
+ "required": [
1510
+ "params",
1511
+ "query"
1512
+ ],
1513
+ "additionalProperties": false
1514
+ }),
1515
+ },
1424
1516
  "GetAttachSession": {
1425
1517
  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`.",
1426
1518
  inputSchema: toZod({
@@ -23,12 +23,14 @@ export interface OldProviderOpts {
23
23
  useProxy?: boolean;
24
24
  providerType?: 'PRIVATE' | 'PUBLIC';
25
25
  }
26
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
27
- export declare function providerToRegisterBody(provider: ReclaimProvider, opts: OldProviderOpts): RegisterProviderBody;
26
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
27
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
28
+ * come from the first draft. */
29
+ export declare function providerToRegisterBody(providers: ReclaimProvider[], opts: OldProviderOpts): RegisterProviderBody;
28
30
  /** Body for `POST /api/providers/:providerId/config` — adds a new version to
29
31
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
30
32
  * required and must be ≥10 chars (enforced by the backend). */
31
- export declare function providerToConfigBody(provider: ReclaimProvider, opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
33
+ export declare function providerToConfigBody(providers: ReclaimProvider[], opts: OldProviderOpts, version: string, versionInfo: string): UpdateProviderConfigBody;
32
34
  /** Minimal version triplet as the old backend stores it. */
33
35
  export interface SemverTriplet {
34
36
  major: number;
@@ -72,16 +72,24 @@ function requestDataFromProvider(provider) {
72
72
  // it ourselves would risk drifting from the server's own extraction.
73
73
  return requestData;
74
74
  }
75
- /** Body for `POST /api/providers/register` — new provider + v1.0.0. */
76
- export function providerToRegisterBody(provider, opts) {
75
+ /** One `requestData` entry per drafted provider. A multi-request provider
76
+ * becomes parallel entries that the old backend runs together at verification
77
+ * (the same way the builder version carries multiple `requests`). */
78
+ function requestDataFrom(providers) {
79
+ return providers.map(requestDataFromProvider);
80
+ }
81
+ /** Body for `POST /api/providers/register` — new provider + v1.0.0. Every
82
+ * drafted request becomes a `requestData` entry; provider-level fields (name)
83
+ * come from the first draft. */
84
+ export function providerToRegisterBody(providers, opts) {
77
85
  const body = {
78
- name: provider.name,
86
+ name: providers[0].name,
79
87
  loginUrl: opts.initialUrl,
80
88
  providerType: opts.providerType ?? 'PRIVATE',
81
89
  // Web-settings defaults mirror the builder openapi defaults.
82
90
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
83
91
  useProxy: opts.useProxy ?? DEFAULT_USE_PROXY,
84
- requestData: [requestDataFromProvider(provider)],
92
+ requestData: requestDataFrom(providers),
85
93
  };
86
94
  if (opts.description !== undefined) {
87
95
  body.description = opts.description;
@@ -91,14 +99,14 @@ export function providerToRegisterBody(provider, opts) {
91
99
  /** Body for `POST /api/providers/:providerId/config` — adds a new version to
92
100
  * an EXISTING provider. `version` must not already exist; `versionInfo` is
93
101
  * required and must be ≥10 chars (enforced by the backend). */
94
- export function providerToConfigBody(provider, opts, version, versionInfo) {
102
+ export function providerToConfigBody(providers, opts, version, versionInfo) {
95
103
  // `useProxy` is a Provider-level flag on the old backend, not part of a
96
104
  // version's providerConfig — so the config (new-version) path only carries
97
105
  // `geoLocation`. It defaults the same way as register.
98
106
  const providerConfig = {
99
107
  loginUrl: opts.initialUrl,
100
108
  geoLocation: opts.geoLocation ?? DEFAULT_GEO_LOCATION,
101
- requestData: [requestDataFromProvider(provider)],
109
+ requestData: requestDataFrom(providers),
102
110
  };
103
111
  return { providerConfig, version, versionInfo };
104
112
  }
@@ -43,10 +43,15 @@ function rethrowOwnershipError(err, providerId) {
43
43
  export function createVersionFromCaptureTool(client) {
44
44
  return defineTool({
45
45
  name: 'create_provider_version_from_capture',
46
- description: 'Publish a drafted ReclaimProvider to the OLD devtools backend. ' +
46
+ description: 'Publish drafted ReclaimProvider(s) to the OLD devtools backend. ' +
47
47
  'Pass the `provider` object from propose_provider VERBATIM and the ' +
48
48
  '`initialUrl` the user landed on (the login/landing page, NOT the ' +
49
49
  'API url). \n\n' +
50
+ 'For a MULTI-REQUEST provider, pass `providers: [...]` (each entry a ' +
51
+ 'proven draft from propose_provider) — every entry becomes one ' +
52
+ '`requestData` entry on the SAME version, and all are verified ' +
53
+ 'together. Pass a single `provider` for the one-request case ' +
54
+ '(provide exactly one of `provider` / `providers`). \n\n' +
50
55
  'Omit `providerId` to REGISTER A NEW provider (creates it + version ' +
51
56
  '1.0.0; re-running creates a duplicate). Pass `providerId` (from ' +
52
57
  'get_me_providers) to add a NEW VERSION to that existing provider ' +
@@ -61,7 +66,8 @@ export function createVersionFromCaptureTool(client) {
61
66
  properties: {
62
67
  provider: {
63
68
  type: 'object',
64
- description: 'The `provider` object from propose_provider, verbatim.',
69
+ description: 'A single `provider` object from propose_provider, verbatim. ' +
70
+ 'For multiple requests use `providers` instead.',
65
71
  properties: {
66
72
  name: { type: 'string' },
67
73
  url: { type: 'string' },
@@ -74,6 +80,28 @@ export function createVersionFromCaptureTool(client) {
74
80
  },
75
81
  required: ['name', 'url', 'method', 'responseMatches'],
76
82
  },
83
+ providers: {
84
+ type: 'array',
85
+ description: 'Multiple proven drafts → one multi-request version (each ' +
86
+ 'becomes a parallel `requestData` entry, all verified ' +
87
+ 'together). Each item has the same shape as `provider`. Pass ' +
88
+ 'this OR `provider`, not both.',
89
+ minItems: 1,
90
+ items: {
91
+ type: 'object',
92
+ properties: {
93
+ name: { type: 'string' },
94
+ url: { type: 'string' },
95
+ method: { type: 'string' },
96
+ headers: { type: 'object' },
97
+ body: { type: 'string' },
98
+ responseMatches: { type: 'array' },
99
+ responseRedactions: { type: 'array' },
100
+ paramValues: { type: 'object' },
101
+ },
102
+ required: ['name', 'url', 'method', 'responseMatches'],
103
+ },
104
+ },
77
105
  initialUrl: {
78
106
  type: 'string',
79
107
  description: 'The website URL the user landed on to trigger the request ' +
@@ -105,9 +133,18 @@ export function createVersionFromCaptureTool(client) {
105
133
  'chars or a default is used).',
106
134
  },
107
135
  },
108
- required: ['provider', 'initialUrl'],
136
+ required: ['initialUrl'],
109
137
  },
110
138
  }, async (args) => {
139
+ // Exactly one of `provider` / `providers` — normalize to an array so a
140
+ // multi-request capture publishes every request, not just the first.
141
+ const hasMany = Array.isArray(args.providers) && args.providers.length > 0;
142
+ const hasOne = !!args.provider;
143
+ if (hasMany === hasOne) {
144
+ throw new Boom('Pass exactly one of `provider` (a single draft) or `providers` ' +
145
+ '(an array of 1+ drafts).', { statusCode: 400 });
146
+ }
147
+ const providers = hasMany ? args.providers : [args.provider];
111
148
  const opts = { initialUrl: args.initialUrl };
112
149
  if (args.description !== undefined) {
113
150
  opts.description = args.description;
@@ -121,11 +158,9 @@ export function createVersionFromCaptureTool(client) {
121
158
  if (args.providerType !== undefined) {
122
159
  opts.providerType = args.providerType;
123
160
  }
124
- // Non-blocking advisory — never alters or blocks the publish; the
125
- // author keeps full control. Attaches under `_notes` when relevant.
126
- const notes = matchRedactionWarnings([
127
- countsFor('Request', args.provider),
128
- ]);
161
+ // Non-blocking advisory per request — never alters or blocks the
162
+ // publish; the author keeps full control. Attaches under `_notes`.
163
+ const notes = matchRedactionWarnings(providers.map((p, i) => countsFor(`Request ${i + 1}`, p)));
129
164
  const withNotes = (result) => {
130
165
  if (notes.length && result && typeof result === 'object') {
131
166
  return { ...result, _notes: notes };
@@ -134,7 +169,7 @@ export function createVersionFromCaptureTool(client) {
134
169
  };
135
170
  // New provider.
136
171
  if (!args.providerId) {
137
- const body = providerToRegisterBody(args.provider, opts);
172
+ const body = providerToRegisterBody(providers, opts);
138
173
  return withNotes(await client.registerProvider(body));
139
174
  }
140
175
  // New version of an existing provider — pick the next patch above
@@ -148,7 +183,7 @@ export function createVersionFromCaptureTool(client) {
148
183
  const versionInfo = args.notes && args.notes.trim().length >= 10
149
184
  ? args.notes.trim()
150
185
  : DEFAULT_VERSION_INFO;
151
- const body = providerToConfigBody(args.provider, opts, version, versionInfo);
186
+ const body = providerToConfigBody(providers, opts, version, versionInfo);
152
187
  return withNotes(await client.updateProviderConfig(providerId, body));
153
188
  }
154
189
  catch (err) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "description": "Reclaim Protocol agent — SDK, CLI, and MCP server in one package. SDK + CLI talk to the builder backend; MCP server adds Chrome CDP capture, provider synthesis, replay, and zkTLS proof.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -10,4 +10,6 @@ This server targets the Reclaim BUILDER backend. It exposes three tool layers: g
10
10
 
11
11
  **Templating:** bare `{{name}}` placeholders are provider params (values extracted from the response by the attestor). Values the CONSUMER must supply at verification time (e.g. a username in the URL path) are templated `{{context.<name>}}` and declared on the version as `requiredContext` — a JSON Schema set via `update_provider_version` while the version is on draft. The Builder validates every session-create's `context` against it (400 on mismatch; extra context fields are always allowed). `requiredContext` is optional — skip it when the recipe has no `{{context.*}}` placeholders. At verification time, requests are matched by exact origin+path: `{{context.<name>}}` segments match the consumer-sent value only, bare `{{name}}` values are extracted from the live traffic (plain-text alignment — works for JSON, XML, HTML, form-encoded, any text). Extract selectors apply client-side in spec order xPath → jsonPath → regex, each to the prior selection, so only the first selector constrains the full response; responses that cannot satisfy it (or miss a required match) are skipped. `propose_provider` returns advisory `verificationWarnings` for author-only decisions — surface them to the dev before publishing.
12
12
 
13
+ **Discover & resolve (find providers for the user's app):** when a dev wants to add verification but doesn't know which providers to use, run the same flow the Discover UI does. Ask which website their users prove data from, then `discover_data_points({ domain })` to list what's verifiable there. Confirm the data points with the dev, then `discover_resolve({ domain, dataPoints })` → the smallest provider set (`providers`, greedy combine), the `requiredContext` inputs to collect from the dev, and any `uncovered` points (author those with the provider-authoring flow above, then re-resolve). Create the session with `create_verification_session({ providers: [{ providerId }, …], context, verificationClientUrl })`, passing EVERY resolved provider id and the collected context. `merge.mergeable` only signals the set could collapse into one provider — you still pass them all; the Builder runs them in one session.
14
+
13
15
  **Verification sessions:** consumers authenticate with a per-org token (`rorg_…`, issued in the dashboard via `POST /orgs/{orgId}/token`) and create sessions via `POST /verifications/sessions`. Results are ECIES-encrypted (secp256k1 + AES-256-GCM) to the org's eth public key when its `canEncryptResult` is enabled — set via `PUT /orgs/{orgId}/keypair` with an eth public key — and delivered to the org's callback subscriptions (plaintext otherwise). Use the org/session tools (`get_verification_session`, `list_verification_events`, `list_org_verification_sessions`) to troubleshoot.
package/skill/old-mode.md CHANGED
@@ -4,8 +4,8 @@ This server is running against the LEGACY Reclaim devtools backend (temporary, w
4
4
 
5
5
  1. `reclaim_authenticate` — by default opens the dashboard in Chrome, waits for you to sign in, captures the token automatically, and closes the tab (or paste a `token` / pass an eth `ethAddress` to skip the browser). Do this once before publishing or listing.
6
6
  2. `attach_chrome` → `start_capture` → drive the page (`navigate`, `wait_for_page`) → inspect (`list_requests`, `get_request`, `find_requests_containing`).
7
- 3. `propose_provider` to draft a provider, `replay_request` to pre-flight, `run_proof` to verify with the attestor. OPRF/`hash` redactions are optional and supported.
8
- 4. `create_provider_version_from_capture` to publish. With NO `providerId` it REGISTERS A NEW provider (re-running creates a duplicate); pass a `providerId` from `get_me_providers` to add a new version to an existing provider instead.
7
+ 3. `propose_provider` to draft a provider, `replay_request` to pre-flight, `run_proof` to verify with the attestor. OPRF/`hash` redactions are optional and supported. When the data spans MULTIPLE requests (e.g. a landing call + a details call), propose + `run_proof` EACH request to get one proven draft per request.
8
+ 4. `create_provider_version_from_capture` to publish. With NO `providerId` it REGISTERS A NEW provider (re-running creates a duplicate); pass a `providerId` from `get_me_providers` to add a new version to an existing provider instead. For a MULTI-REQUEST provider, pass `providers: [draftA, draftB, …]` (every proven draft) so EACH becomes a parallel `requestData` entry on the one version — passing a single `provider` saves only that one request.
9
9
  5. `get_me_providers` lists the providers you own (the only provider-listing tool available in this mode).
10
10
 
11
11
  **Editing / new versions (ownership):** you can only add a version to a provider **you own** — the `providerId` must come from `get_me_providers`. Trying to version a public provider you don't own fails with a permission error; that is expected. To publish your own version of such a site, re-run `create_provider_version_from_capture` **without** `providerId` to register your own copy (a separate provider you own). If versioning a provider you *do* own fails, confirm you authenticated as the account that created it.
@@ -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({
@@ -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 }
@@ -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,