@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.
- package/README.md +7 -38
- package/lib/cdp/capture-engine.d.ts +7 -0
- package/lib/cdp/capture-engine.js +78 -0
- package/lib/cdp/capture.d.ts +1 -1
- package/lib/cdp/capture.js +1 -1
- package/lib/consts.d.ts +4 -2
- package/lib/consts.js +6 -2
- package/lib/mcp/api/operations.gen.d.ts +8 -0
- package/lib/mcp/api/operations.gen.js +92 -0
- package/lib/mcp/instructions.d.ts +2 -0
- package/lib/mcp/instructions.js +16 -0
- package/lib/mcp/start.js +2 -1
- package/lib/mcp/tools/agent/build.js +4 -0
- package/lib/mcp/tools/agent/how-it-works.d.ts +11 -0
- package/lib/mcp/tools/agent/how-it-works.js +24 -0
- package/lib/old/client.d.ts +10 -0
- package/lib/old/client.js +27 -0
- package/lib/old/to-register.d.ts +5 -3
- package/lib/old/to-register.js +14 -6
- package/lib/old/tools/build.d.ts +2 -2
- package/lib/old/tools/build.js +4 -2
- package/lib/old/tools/publish.js +45 -10
- package/lib/old/tools/session-analytics-logs.d.ts +9 -0
- package/lib/old/tools/session-analytics-logs.js +25 -0
- package/package.json +2 -3
- package/skill/{SKILL.md → SKILL.builder.md} +3 -36
- package/skill/SKILL.old.md +221 -0
- package/skill/builder-mode.md +2 -0
- package/skill/old-mode.md +4 -2
- package/src/cdp/capture-engine.ts +89 -0
- package/src/cdp/capture.ts +5 -1
- package/src/consts.ts +7 -2
- package/src/mcp/api/operations.gen.ts +96 -0
- package/src/mcp/instructions.ts +18 -0
- package/src/mcp/start.ts +2 -0
- package/src/mcp/tools/agent/build.ts +4 -0
- package/src/mcp/tools/agent/how-it-works.ts +32 -0
- package/src/old/client.ts +32 -0
- package/src/old/to-register.ts +15 -6
- package/src/old/tools/build.ts +4 -2
- package/src/old/tools/publish.ts +55 -11
- 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
|
|
package/src/cdp/capture.ts
CHANGED
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
type StartCaptureOptions,
|
|
12
12
|
} from './capture-engine.ts'
|
|
13
13
|
|
|
14
|
-
export {
|
|
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({
|
package/src/mcp/instructions.ts
CHANGED
|
@@ -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.
|
package/src/old/to-register.ts
CHANGED
|
@@ -150,19 +150,28 @@ function requestDataFromProvider(provider: ReclaimProvider): OldRequestData {
|
|
|
150
150
|
return requestData
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/**
|
|
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
|
-
|
|
164
|
+
providers: ReclaimProvider[],
|
|
156
165
|
opts: OldProviderOpts,
|
|
157
166
|
): RegisterProviderBody {
|
|
158
167
|
const body: RegisterProviderBody = {
|
|
159
|
-
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:
|
|
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
|
-
|
|
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:
|
|
198
|
+
requestData: requestDataFrom(providers),
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
return { providerConfig, version, versionInfo }
|
package/src/old/tools/build.ts
CHANGED
|
@@ -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
|
|
11
|
-
*
|
|
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
|
}
|
package/src/old/tools/publish.ts
CHANGED
|
@@ -62,7 +62,8 @@ export function createVersionFromCaptureTool(
|
|
|
62
62
|
client: ReclaimOldClient,
|
|
63
63
|
): RegisteredTool {
|
|
64
64
|
return defineTool<{
|
|
65
|
-
provider
|
|
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
|
|
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
|
-
'
|
|
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: ['
|
|
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
|
|
167
|
-
// author keeps full control. Attaches under `_notes
|
|
168
|
-
const notes = matchRedactionWarnings(
|
|
169
|
-
countsFor(
|
|
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(
|
|
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
|
-
|
|
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
|
+
}
|