@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.
- package/README.md +11 -42
- package/lib/authoring/backend.d.ts +95 -0
- package/lib/authoring/backend.js +24 -0
- package/lib/authoring/index.d.ts +4 -0
- package/lib/authoring/index.js +5 -0
- package/lib/authoring/tools.d.ts +9 -0
- package/lib/authoring/tools.js +378 -0
- 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.js +46 -18
- package/lib/mcp/instructions.d.ts +2 -0
- package/lib/mcp/instructions.js +16 -0
- package/lib/mcp/server.d.ts +18 -2
- package/lib/mcp/server.js +19 -4
- 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/mcp/tools/agent/publish.js +2 -2
- package/lib/mcp/tools/agent/shared.d.ts +6 -0
- package/lib/mcp/tools/agent/shared.js +115 -0
- package/lib/old/client.d.ts +10 -0
- package/lib/old/client.js +27 -0
- package/lib/old/login-cdp.d.ts +5 -0
- package/lib/old/login-cdp.js +12 -1
- package/lib/old/tools/authenticate.js +7 -3
- package/lib/old/tools/build.d.ts +2 -2
- package/lib/old/tools/build.js +4 -2
- package/lib/old/tools/session-analytics-logs.d.ts +9 -0
- package/lib/old/tools/session-analytics-logs.js +25 -0
- package/lib/proof/proof-core.d.ts +1 -1
- package/lib/proof/proof-core.js +1 -1
- package/lib/tools/registry.d.ts +36 -0
- package/lib/tools/registry.js +17 -0
- package/package.json +2 -3
- package/skill/{SKILL.md → SKILL.builder.md} +16 -45
- package/skill/SKILL.old.md +225 -0
- package/skill/old-mode.md +2 -0
- 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 +46 -18
- package/src/mcp/instructions.ts +18 -0
- package/src/mcp/server.ts +43 -6
- 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/mcp/tools/agent/publish.ts +2 -2
- package/src/old/client.ts +32 -0
- package/src/old/login-cdp.ts +20 -1
- package/src/old/tools/authenticate.ts +11 -3
- package/src/old/tools/build.ts +4 -2
- package/src/old/tools/session-analytics-logs.ts +33 -0
- package/src/proof/proof-core.ts +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the shared authoring tools for the MCP, bound to a per-capture LOCAL
|
|
3
|
+
* backend: a Chrome tab over CDP + the in-process attestor SDK. The tool logic
|
|
4
|
+
* (navigate/inspect/propose/replay/prove) is authored once in `../../../
|
|
5
|
+
* authoring` with the shared `defineTool` primitive; this only supplies the
|
|
6
|
+
* MCP host — resolve the backend from `captureId` (the session handle from
|
|
7
|
+
* `start_capture`, since one capture bundles the tab, its requests, and its
|
|
8
|
+
* drafts) and the rich, credential-aware `run_proof` schema.
|
|
9
|
+
*/
|
|
10
|
+
import { buildAuthoringTools, } from "../../../authoring/index.js";
|
|
11
|
+
import { runAttestorProof } from "../../../proof/attestor.js";
|
|
12
|
+
import { replayProvider } from "../../../proof/replay.js";
|
|
13
|
+
import { shapeRichProofResult } from "./proof.js";
|
|
14
|
+
/** The local backend for one capture: drive its tab over CDP, read its
|
|
15
|
+
* captured requests, prove via the in-process attestor, replay by re-fetch. */
|
|
16
|
+
function localBackend(entry) {
|
|
17
|
+
const { tabClient, session, drafts } = entry;
|
|
18
|
+
// The per-tab CDP client is strongly typed to `keyof Commands`; the shared
|
|
19
|
+
// tools drive it with dynamic method/event names, so view it as string-keyed.
|
|
20
|
+
const client = tabClient;
|
|
21
|
+
return {
|
|
22
|
+
capture: session,
|
|
23
|
+
drafts,
|
|
24
|
+
sendCdp: (method, params) => client.send(method, params ?? {}),
|
|
25
|
+
waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
|
|
26
|
+
const onEvent = () => {
|
|
27
|
+
cleanup();
|
|
28
|
+
resolve('fired');
|
|
29
|
+
};
|
|
30
|
+
const timer = setTimeout(() => {
|
|
31
|
+
cleanup();
|
|
32
|
+
resolve('timeout');
|
|
33
|
+
}, timeoutMs);
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
client.removeListener(method, onEvent);
|
|
37
|
+
};
|
|
38
|
+
client.once(method, onEvent);
|
|
39
|
+
}),
|
|
40
|
+
replay: async (draft, opts) => {
|
|
41
|
+
// Auth-bound check: re-fetch with the secrets dropped. replayProvider
|
|
42
|
+
// already does a live Node fetch, so empty secrets == anonymous.
|
|
43
|
+
const secrets = opts?.withoutSecrets ? {} : draft.secrets;
|
|
44
|
+
const result = await replayProvider(draft.provider, secrets);
|
|
45
|
+
return {
|
|
46
|
+
status: result.status,
|
|
47
|
+
matchesOriginal: result.matchesOriginal,
|
|
48
|
+
bodySnippet: result.body.slice(0, 256),
|
|
49
|
+
...(result.hints !== undefined ? { hints: result.hints } : {}),
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
prove: async (draft, opts) => {
|
|
53
|
+
const result = await runAttestorProof(draft.provider, draft.secrets, {
|
|
54
|
+
...(opts?.ownerAddress ? { ownerAddress: opts.ownerAddress } : {}),
|
|
55
|
+
...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
|
|
56
|
+
});
|
|
57
|
+
return shapeRichProofResult(result, {
|
|
58
|
+
provider: draft.provider,
|
|
59
|
+
hasSecrets: Object.keys(draft.secrets).length > 0,
|
|
60
|
+
...(opts?.attestorUrl ? { attestorUrl: opts.attestorUrl } : {}),
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The MCP's rich, credential-aware `run_proof` schema, merged onto the shared
|
|
66
|
+
* tool by the host below (the cloud's TEE signs in-enclave, so it needs none
|
|
67
|
+
* of this). */
|
|
68
|
+
const RUN_PROOF_EXTRAS = {
|
|
69
|
+
description: 'Run a full zkTLS proof of the drafted recipe through the Reclaim '
|
|
70
|
+
+ 'attestor SDK. Use only after replay_request looks right — attestor '
|
|
71
|
+
+ 'calls are expensive. Writes the full claim to '
|
|
72
|
+
+ '~/.reclaim/claims/<timestamp>.json and returns identifier + '
|
|
73
|
+
+ 'extractedParameters + claimPath + a prettified `claim` (request '
|
|
74
|
+
+ 'dropped, byte arrays as base64). With RECLAIM_AGENT_TRACE=1, also '
|
|
75
|
+
+ 'writes a per-call diagnostic dump (input provider, sentParams, full '
|
|
76
|
+
+ 'attestor log stream, result) and returns it as `runLogPath`. '
|
|
77
|
+
+ '`attestorUrl` overrides the default only when the dev supplies a '
|
|
78
|
+
+ 'specific alternative — do NOT invent backup URLs. `ownerAddress` '
|
|
79
|
+
+ '(the credential to sign with) is REQUIRED — call resolve_owner_key '
|
|
80
|
+
+ 'first. errorKind="missing-owner-key" means that address has no known '
|
|
81
|
+
+ 'private key; re-run resolve_owner_key or issue/import a credential.',
|
|
82
|
+
properties: {
|
|
83
|
+
ownerAddress: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
pattern: '^0x[0-9a-fA-F]{40}$',
|
|
86
|
+
description: 'ETH address of the credential to sign the claim with. Get it '
|
|
87
|
+
+ 'from resolve_owner_key first. The private key is resolved '
|
|
88
|
+
+ 'locally (cache / env / .env) — never passed here.',
|
|
89
|
+
},
|
|
90
|
+
attestorUrl: { type: 'string' },
|
|
91
|
+
},
|
|
92
|
+
required: ['ownerAddress'],
|
|
93
|
+
};
|
|
94
|
+
/** The shared authoring tools as MCP `RegisteredTool`s: each resolves its
|
|
95
|
+
* capture's local backend from `args.captureId`; the schema carries
|
|
96
|
+
* `captureId` plus the rich `run_proof` contract. */
|
|
97
|
+
export function sharedAuthoringTools(captures) {
|
|
98
|
+
const host = {
|
|
99
|
+
resolveBackend: (args) => {
|
|
100
|
+
const captureId = String(args.captureId);
|
|
101
|
+
const entry = captures.sessions.get(captureId);
|
|
102
|
+
if (!entry) {
|
|
103
|
+
throw new Error(`no capture ${captureId} — call start_capture first `
|
|
104
|
+
+ '(it has been reset or never started).');
|
|
105
|
+
}
|
|
106
|
+
return localBackend(entry);
|
|
107
|
+
},
|
|
108
|
+
sessionSchema: {
|
|
109
|
+
properties: { captureId: { type: 'string' } },
|
|
110
|
+
required: ['captureId'],
|
|
111
|
+
},
|
|
112
|
+
toolExtras: { run_proof: RUN_PROOF_EXTRAS },
|
|
113
|
+
};
|
|
114
|
+
return buildAuthoringTools(host);
|
|
115
|
+
}
|
package/lib/old/client.d.ts
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* `/api` route prefix itself, so this is the bare origin. Override with
|
|
3
3
|
* `RECLAIM_OLD_API_URL`. */
|
|
4
4
|
export declare const DEFAULT_OLD_API_URL = "https://devapi.reclaimprotocol.org";
|
|
5
|
+
/** Default origin of the OLD-devtools analytics-logs service — a SEPARATE host
|
|
6
|
+
* from the devtools API. Override with `RECLAIM_OLD_LOGS_URL`. */
|
|
7
|
+
export declare const DEFAULT_OLD_LOGS_URL = "https://logs.reclaimprotocol.org";
|
|
5
8
|
/**
|
|
6
9
|
* Externally-supplied caller identity. We do NOT mint these — the dev
|
|
7
10
|
* provides one:
|
|
@@ -53,8 +56,10 @@ export interface MyProvidersQuery {
|
|
|
53
56
|
export declare class ReclaimOldClient {
|
|
54
57
|
#private;
|
|
55
58
|
readonly baseUrl: string;
|
|
59
|
+
readonly logsBaseUrl: string;
|
|
56
60
|
constructor(opts?: {
|
|
57
61
|
baseUrl?: string;
|
|
62
|
+
logsBaseUrl?: string;
|
|
58
63
|
identity?: OldIdentity;
|
|
59
64
|
fetch?: typeof fetch;
|
|
60
65
|
});
|
|
@@ -66,5 +71,10 @@ export declare class ReclaimOldClient {
|
|
|
66
71
|
registerProvider(body: RegisterProviderBody): Promise<unknown>;
|
|
67
72
|
updateProviderConfig(providerId: string, body: UpdateProviderConfigBody): Promise<unknown>;
|
|
68
73
|
listVersions(providerId: string): Promise<unknown>;
|
|
74
|
+
/** Analytics logs for a verification session, from the logs service (a
|
|
75
|
+
* DIFFERENT host than the devtools API — hence its own fetch, not
|
|
76
|
+
* `#request`). Attaches the caller identity when one is set; queried
|
|
77
|
+
* anonymously otherwise. */
|
|
78
|
+
getSessionAnalyticsLogs(sessionId: string): Promise<unknown>;
|
|
69
79
|
getMyProviders(query?: MyProvidersQuery): Promise<unknown>;
|
|
70
80
|
}
|
package/lib/old/client.js
CHANGED
|
@@ -4,6 +4,9 @@ import { NAME, VERSION } from "../pkg.js";
|
|
|
4
4
|
* `/api` route prefix itself, so this is the bare origin. Override with
|
|
5
5
|
* `RECLAIM_OLD_API_URL`. */
|
|
6
6
|
export const DEFAULT_OLD_API_URL = 'https://devapi.reclaimprotocol.org';
|
|
7
|
+
/** Default origin of the OLD-devtools analytics-logs service — a SEPARATE host
|
|
8
|
+
* from the devtools API. Override with `RECLAIM_OLD_LOGS_URL`. */
|
|
9
|
+
export const DEFAULT_OLD_LOGS_URL = 'https://logs.reclaimprotocol.org';
|
|
7
10
|
/**
|
|
8
11
|
* Minimal REST client for the OLD devtools Express backend (base `/api`).
|
|
9
12
|
* Deliberately NOT a clone of `@reclaimprotocol/client`'s operationId
|
|
@@ -16,10 +19,13 @@ export const DEFAULT_OLD_API_URL = 'https://devapi.reclaimprotocol.org';
|
|
|
16
19
|
*/
|
|
17
20
|
export class ReclaimOldClient {
|
|
18
21
|
baseUrl;
|
|
22
|
+
logsBaseUrl;
|
|
19
23
|
#identity;
|
|
20
24
|
#fetch;
|
|
21
25
|
constructor(opts = {}) {
|
|
22
26
|
this.baseUrl = (opts.baseUrl || DEFAULT_OLD_API_URL).replace(/\/+$/, '');
|
|
27
|
+
this.logsBaseUrl = (opts.logsBaseUrl || DEFAULT_OLD_LOGS_URL)
|
|
28
|
+
.replace(/\/+$/, '');
|
|
23
29
|
this.#identity = opts.identity;
|
|
24
30
|
this.#fetch = opts.fetch ?? fetch;
|
|
25
31
|
}
|
|
@@ -43,6 +49,27 @@ export class ReclaimOldClient {
|
|
|
43
49
|
listVersions(providerId) {
|
|
44
50
|
return this.#request('GET', `/providers/${encodeURIComponent(providerId)}/versions`, { auth: true });
|
|
45
51
|
}
|
|
52
|
+
/** Analytics logs for a verification session, from the logs service (a
|
|
53
|
+
* DIFFERENT host than the devtools API — hence its own fetch, not
|
|
54
|
+
* `#request`). Attaches the caller identity when one is set; queried
|
|
55
|
+
* anonymously otherwise. */
|
|
56
|
+
async getSessionAnalyticsLogs(sessionId) {
|
|
57
|
+
const url = `${this.logsBaseUrl}/api/analytics-logs/session/`
|
|
58
|
+
+ encodeURIComponent(sessionId);
|
|
59
|
+
const headers = {
|
|
60
|
+
Accept: 'application/json',
|
|
61
|
+
'User-Agent': `${NAME}/${VERSION}`,
|
|
62
|
+
};
|
|
63
|
+
if (this.#identity) {
|
|
64
|
+
headers[this.#authHeaderName()] = this.#authHeaderValue();
|
|
65
|
+
}
|
|
66
|
+
const res = await this.#fetch(url, { method: 'GET', headers });
|
|
67
|
+
const json = await this.#parseBody(res);
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
throw this.#asBoom(res, json);
|
|
70
|
+
}
|
|
71
|
+
return json;
|
|
72
|
+
}
|
|
46
73
|
getMyProviders(query = {}) {
|
|
47
74
|
// Every one of these query params is REQUIRED by the backend
|
|
48
75
|
// validator (missing/invalid → 400), so default them all here.
|
package/lib/old/login-cdp.d.ts
CHANGED
|
@@ -17,4 +17,9 @@ export declare function captureFirebaseTokenViaCdp(opts: {
|
|
|
17
17
|
loginUrl?: string;
|
|
18
18
|
timeoutMs?: number;
|
|
19
19
|
pollMs?: number;
|
|
20
|
+
/** Called every ~10s while waiting for the human to finish signing in, so
|
|
21
|
+
* MCP clients that track progress on this call can reset their own
|
|
22
|
+
* request timeout instead of giving up on a login that's still in
|
|
23
|
+
* flight (see `ToolContext` in `mcp/server.ts`). */
|
|
24
|
+
onProgress?: (message: string) => Promise<void>;
|
|
20
25
|
}): Promise<CdpLoginResult>;
|
package/lib/old/login-cdp.js
CHANGED
|
@@ -65,6 +65,8 @@ export async function captureFirebaseTokenViaCdp(opts) {
|
|
|
65
65
|
const loginUrl = opts.loginUrl || oldLoginUrl() || DEFAULT_LOGIN_URL;
|
|
66
66
|
const timeoutMs = opts.timeoutMs ?? 180_000;
|
|
67
67
|
const pollMs = opts.pollMs ?? 1000;
|
|
68
|
+
const onProgress = opts.onProgress ?? (async () => { });
|
|
69
|
+
const progressIntervalMs = 10_000;
|
|
68
70
|
const cfg = new ConfigStore().read()?.browser_agent;
|
|
69
71
|
const mode = cfg?.attachMode ?? 'dedicated';
|
|
70
72
|
const port = cfg?.port ?? 9222;
|
|
@@ -89,7 +91,9 @@ export async function captureFirebaseTokenViaCdp(opts) {
|
|
|
89
91
|
try {
|
|
90
92
|
const session = await handles.attachToTab(targetId);
|
|
91
93
|
const { Runtime } = session;
|
|
92
|
-
const
|
|
94
|
+
const startedAt = Date.now();
|
|
95
|
+
const deadline = startedAt + timeoutMs;
|
|
96
|
+
let lastProgressAt = startedAt;
|
|
93
97
|
while (Date.now() < deadline) {
|
|
94
98
|
const r = await Runtime.evaluate({
|
|
95
99
|
expression: EXTRACT_TOKEN_JS,
|
|
@@ -100,6 +104,13 @@ export async function captureFirebaseTokenViaCdp(opts) {
|
|
|
100
104
|
if (typeof value === 'string' && value.length > 0) {
|
|
101
105
|
return { token: value, closedBrowser: launchedByUs };
|
|
102
106
|
}
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (now - lastProgressAt >= progressIntervalMs) {
|
|
109
|
+
lastProgressAt = now;
|
|
110
|
+
const remainingSec = Math.round((deadline - now) / 1000);
|
|
111
|
+
await onProgress(`Still waiting for sign-in at ${loginUrl} ` +
|
|
112
|
+
`(${remainingSec}s left before this call times out).`);
|
|
113
|
+
}
|
|
103
114
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
104
115
|
}
|
|
105
116
|
throw new Error('Timed out waiting for login. Finish signing in at the opened ' +
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { agentDisabled } from "../../consts.js";
|
|
2
|
-
import { defineTool } from "../../mcp/server.js";
|
|
2
|
+
import { defineTool, } from "../../mcp/server.js";
|
|
3
3
|
import { persistIdentity, toEthUid } from "../identity.js";
|
|
4
4
|
import { captureFirebaseTokenViaCdp } from "../login-cdp.js";
|
|
5
5
|
/**
|
|
@@ -69,7 +69,7 @@ export function authenticateTool(client) {
|
|
|
69
69
|
},
|
|
70
70
|
},
|
|
71
71
|
},
|
|
72
|
-
}, async ({ token, ethAddress, loginUrl, timeoutMs, useBrowser }) => {
|
|
72
|
+
}, async ({ token, ethAddress, loginUrl, timeoutMs, useBrowser }, ctx) => {
|
|
73
73
|
if (token && ethAddress) {
|
|
74
74
|
throw new Error('Provide only ONE of `token` or `ethAddress`, not both.');
|
|
75
75
|
}
|
|
@@ -113,7 +113,11 @@ export function authenticateTool(client) {
|
|
|
113
113
|
throw new Error('Browser login is unavailable (RECLAIM_AGENT_DISABLED=1). Pass a ' +
|
|
114
114
|
'`token` (Firebase ID token) or `ethAddress` instead.');
|
|
115
115
|
}
|
|
116
|
-
const result = await captureFirebaseTokenViaCdp({
|
|
116
|
+
const result = await captureFirebaseTokenViaCdp({
|
|
117
|
+
loginUrl,
|
|
118
|
+
timeoutMs,
|
|
119
|
+
onProgress: ctx.reportProgress,
|
|
120
|
+
});
|
|
117
121
|
const identity = { kind: 'bearer', token: result.token };
|
|
118
122
|
await validate(identity);
|
|
119
123
|
persistIdentity(identity);
|
package/lib/old/tools/build.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ReclaimOldClient } from '../client.ts';
|
|
|
3
3
|
/**
|
|
4
4
|
* The backend-facing tools for OLD-devtools mode. Mirrors
|
|
5
5
|
* `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
|
|
6
|
-
* list for `registerToolList`. These
|
|
7
|
-
*
|
|
6
|
+
* list for `registerToolList`. These replace the builder credential + publish
|
|
7
|
+
* tools and the (disabled) generated provider tools.
|
|
8
8
|
*/
|
|
9
9
|
export declare function buildOldBackendTools(client: ReclaimOldClient): RegisteredTool[];
|
package/lib/old/tools/build.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { authenticateTool } from "./authenticate.js";
|
|
2
2
|
import { getMeProvidersTool } from "./me-providers.js";
|
|
3
3
|
import { createVersionFromCaptureTool } from "./publish.js";
|
|
4
|
+
import { sessionAnalyticsLogsTool } from "./session-analytics-logs.js";
|
|
4
5
|
/**
|
|
5
6
|
* The backend-facing tools for OLD-devtools mode. Mirrors
|
|
6
7
|
* `buildCredentialTools` / `buildAgentTools`: returns a flat `RegisteredTool`
|
|
7
|
-
* list for `registerToolList`. These
|
|
8
|
-
*
|
|
8
|
+
* list for `registerToolList`. These replace the builder credential + publish
|
|
9
|
+
* tools and the (disabled) generated provider tools.
|
|
9
10
|
*/
|
|
10
11
|
export function buildOldBackendTools(client) {
|
|
11
12
|
return [
|
|
12
13
|
authenticateTool(client),
|
|
13
14
|
createVersionFromCaptureTool(client),
|
|
14
15
|
getMeProvidersTool(client),
|
|
16
|
+
sessionAnalyticsLogsTool(client),
|
|
15
17
|
];
|
|
16
18
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type RegisteredTool } from '../../mcp/server.ts';
|
|
2
|
+
import type { ReclaimOldClient } from '../client.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Fetch analytics logs for a verification session from the OLD-devtools logs
|
|
5
|
+
* service (`GET https://logs.reclaimprotocol.org/api/analytics-logs/session/
|
|
6
|
+
* :sessionId`). Old mode only — useful for diagnosing why a session failed or
|
|
7
|
+
* stalled. The logs service is a separate host from the devtools API.
|
|
8
|
+
*/
|
|
9
|
+
export declare function sessionAnalyticsLogsTool(client: ReclaimOldClient): RegisteredTool;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { defineTool } from "../../mcp/server.js";
|
|
2
|
+
/**
|
|
3
|
+
* Fetch analytics logs for a verification session from the OLD-devtools logs
|
|
4
|
+
* service (`GET https://logs.reclaimprotocol.org/api/analytics-logs/session/
|
|
5
|
+
* :sessionId`). Old mode only — useful for diagnosing why a session failed or
|
|
6
|
+
* stalled. The logs service is a separate host from the devtools API.
|
|
7
|
+
*/
|
|
8
|
+
export function sessionAnalyticsLogsTool(client) {
|
|
9
|
+
return defineTool({
|
|
10
|
+
name: 'session_analytics_logs',
|
|
11
|
+
description: 'Fetch analytics logs for a verification session by id from the '
|
|
12
|
+
+ 'old-devtools logs service — use to diagnose what happened in a '
|
|
13
|
+
+ 'session (matches, claims, failures). Pass the `sessionId`.',
|
|
14
|
+
inputSchema: {
|
|
15
|
+
type: 'object',
|
|
16
|
+
properties: {
|
|
17
|
+
sessionId: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'The verification session id to fetch logs for.',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ['sessionId'],
|
|
23
|
+
},
|
|
24
|
+
}, async (args) => client.getSessionAnalyticsLogs(args.sessionId));
|
|
25
|
+
}
|
|
@@ -18,7 +18,7 @@ export declare function ensureAttestorCrypto(): void;
|
|
|
18
18
|
* default. Resolved per-call so an env change or per-request override takes
|
|
19
19
|
* effect without restarting. */
|
|
20
20
|
export declare function resolveAttestorUrl(override?: string): string;
|
|
21
|
-
/** Map a
|
|
21
|
+
/** Map a provider's recipe straight to the attestor's HTTP
|
|
22
22
|
* params — never synthesized from captured traffic. */
|
|
23
23
|
export declare function buildHttpParams(provider: ReclaimProvider): ProviderParams<'http'>;
|
|
24
24
|
export interface ParsedClaim {
|
package/lib/proof/proof-core.js
CHANGED
|
@@ -21,7 +21,7 @@ export function ensureAttestorCrypto() {
|
|
|
21
21
|
export function resolveAttestorUrl(override) {
|
|
22
22
|
return (override ?? attestorUrlOverride() ?? FALLBACK_ATTESTOR_URL);
|
|
23
23
|
}
|
|
24
|
-
/** Map a
|
|
24
|
+
/** Map a provider's recipe straight to the attestor's HTTP
|
|
25
25
|
* params — never synthesized from captured traffic. */
|
|
26
26
|
export function buildHttpParams(provider) {
|
|
27
27
|
// A blank egress is treated as unset: omit it so the local proof uses the
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE tool primitive for the agent. A tool is authored once with
|
|
3
|
+
* {@link defineTool} — `{ definition, handler }`, where the handler receives
|
|
4
|
+
* args already parsed + validated against the definition's JSON Schema (via
|
|
5
|
+
* zod), so the schema shown to the model and the validation applied to inbound
|
|
6
|
+
* args cannot drift.
|
|
7
|
+
*
|
|
8
|
+
* This lives outside `mcp/` on purpose: it depends only on zod (NOT the MCP
|
|
9
|
+
* SDK), so non-MCP consumers — the cloud authoring loop — can author and run
|
|
10
|
+
* these tools without pulling in `@modelcontextprotocol/sdk`. The MCP-specific
|
|
11
|
+
* glue (`registerToolList` onto an `McpServer`) stays in `mcp/server.ts`, which
|
|
12
|
+
* re-exports this primitive for the MCP-side tool files.
|
|
13
|
+
*/
|
|
14
|
+
import type { JSONSchema } from 'zod/v4/core';
|
|
15
|
+
export interface ToolDefinition {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: JSONSchema.ObjectSchema;
|
|
19
|
+
}
|
|
20
|
+
export interface RegisteredTool {
|
|
21
|
+
definition: ToolDefinition;
|
|
22
|
+
/**
|
|
23
|
+
* @param args Tool arguments (already zod-validated against the schema).
|
|
24
|
+
* @returns A JSON-stringifiable value to return to the LLM.
|
|
25
|
+
* @throws Any error; the host (MCP `registerToolList` / the cloud loop)
|
|
26
|
+
* catches it and surfaces it as an error tool-result.
|
|
27
|
+
*/
|
|
28
|
+
handler: (args: unknown) => Promise<unknown>;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Define a tool with a single inputSchema (JSON Schema) as the source of truth.
|
|
32
|
+
* The handler receives args already parsed and type-narrowed via a Zod schema
|
|
33
|
+
* derived from that same JSON Schema — so the description sent to the client
|
|
34
|
+
* and the validation applied to inbound args cannot drift.
|
|
35
|
+
*/
|
|
36
|
+
export declare function defineTool<TArgs>(definition: ToolDefinition, handler: (args: TArgs) => Promise<unknown>): RegisteredTool;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { toZod } from "../mcp/api/to-zod.js";
|
|
2
|
+
/**
|
|
3
|
+
* Define a tool with a single inputSchema (JSON Schema) as the source of truth.
|
|
4
|
+
* The handler receives args already parsed and type-narrowed via a Zod schema
|
|
5
|
+
* derived from that same JSON Schema — so the description sent to the client
|
|
6
|
+
* and the validation applied to inbound args cannot drift.
|
|
7
|
+
*/
|
|
8
|
+
export function defineTool(definition, handler) {
|
|
9
|
+
const schema = toZod(definition.inputSchema);
|
|
10
|
+
return {
|
|
11
|
+
definition,
|
|
12
|
+
handler: async (raw) => {
|
|
13
|
+
const args = schema.parse(raw);
|
|
14
|
+
return await handler(args);
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reclaimprotocol/agent",
|
|
3
3
|
"title": "Reclaim Agent",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
"./mcp/register": {
|
|
27
27
|
"types": "./lib/mcp/register.d.ts",
|
|
28
28
|
"import": "./lib/mcp/register.js"
|
|
29
|
-
}
|
|
30
|
-
"./skill": "./skill/SKILL.md"
|
|
29
|
+
}
|
|
31
30
|
},
|
|
32
31
|
"bin": {
|
|
33
32
|
"reclaim": "./lib/cli.js",
|
|
@@ -1,39 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
> correct the developer for using one or the other.
|
|
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.
|
|
13
12
|
|
|
14
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.
|
|
15
14
|
|
|
16
|
-
## Modes
|
|
17
|
-
|
|
18
|
-
This MCP server targets one of two backends. Detect which from the tools available to you:
|
|
19
|
-
|
|
20
|
-
- **Old devtools (the default).** `get_me_providers` is present; the generated API tools (`get_me`, `create_verification`, `list_orgs`, …) are absent.
|
|
21
|
-
- **Builder.** The generated API tools are present; `get_me_providers` is absent.
|
|
22
|
-
|
|
23
|
-
Only **authentication** (below), **publishing** (the final step), and the **consumer-supplied param namespace** differ between modes: values the Consumer supplies at verification time are templated `{{context.<name>}}` on builder vs `{{context_<name>}}` on old devtools (no dotted interpolation there) — see step 10. The capture → draft → replay → prove loop in between is identical, and OPRF is optional and supported in both.
|
|
24
|
-
|
|
25
15
|
## Getting authenticated
|
|
26
16
|
|
|
27
|
-
### Old devtools mode
|
|
28
|
-
|
|
29
|
-
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."_
|
|
30
|
-
|
|
31
|
-
- Force a fresh login (ignore the cached identity): `reclaim_authenticate({ useBrowser: true })`.
|
|
32
|
-
- Skip the browser: `reclaim_authenticate({ token: "<firebase-id-token>" })`, or `reclaim_authenticate({ ethAddress: "0x..." })` for an already-provisioned eth user.
|
|
33
|
-
- Tokens are short-lived (~1h). If a later call returns 401, call `reclaim_authenticate({})` again.
|
|
34
|
-
|
|
35
|
-
### Builder mode
|
|
36
|
-
|
|
37
17
|
**Check first:** try `get_me`. If it returns the user's profile, you're authenticated — proceed to the loop.
|
|
38
18
|
|
|
39
19
|
**On 401:** call `authenticate({})` to start a device-pairing session:
|
|
@@ -150,15 +130,14 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
150
130
|
|
|
151
131
|
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:
|
|
152
132
|
|
|
153
|
-
|
|
154
|
-
- **Old devtools mode:** same idea, but the legacy backend doesn't support dotted interpolation — use `{{context_<name>}}` and `paramValues: { ..., "context_username": "Scratch-net" }`.
|
|
133
|
+
replace with `{{context.<name>}}`. E.g. `https://github.com/Scratch-net/repos` → `https://github.com/{{context.username}}/repos`, with `paramValues: { ..., "context.username": "Scratch-net" }` for replay/proof (the paramValues key must match the placeholder name exactly, dot included). Declare these in the version's `requiredContext` at publish time (step 13).
|
|
155
134
|
- Query parameters with user-specific values → same treatment.
|
|
156
135
|
- Generic segments (`/api/v3`, `/users`, `/repos`) stay literal.
|
|
157
136
|
- If the URL has no user-identifying segments (e.g. `/settings/profile`), leave it untouched — the matcher already handled the value via paramValues.
|
|
158
137
|
|
|
159
138
|
**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.
|
|
160
139
|
|
|
161
|
-
**How the verification client resolves these at runtime
|
|
140
|
+
**How the verification client resolves these at runtime**:
|
|
162
141
|
|
|
163
142
|
- Requests are matched by **exact origin+path** (query ignored). A `{{context.<name>}}` segment matches **only the value the consumer sent** in the session context (validated by `requiredContext`); a bare `{{name}}` segment matches any one path segment and its value is **auto-extracted from the live traffic**.
|
|
164
143
|
- Interpolation and `contains` matching are **plain text** — they work for any captured content (JSON, XML, HTML, form-encoded bodies, …).
|
|
@@ -198,17 +177,9 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
198
177
|
|
|
199
178
|
**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.
|
|
200
179
|
|
|
201
|
-
13. **Publish the proved provider.**
|
|
202
|
-
|
|
203
|
-
**Old devtools mode** — call `create_provider_version_from_capture`:
|
|
204
|
-
- **New verification:** omit `providerId`. It registers a new provider (version 1.0.0). Pass `provider`, `initialUrl`, and optionally `description`, `geoLocation`, `providerType` (`PRIVATE` default | `PUBLIC`).
|
|
205
|
-
- **New version of an existing verification:** pass its `providerId` — the patch is auto-bumped above the highest existing version. Pass `notes` (≥10 chars) as the change note.
|
|
206
|
-
- `get_me_providers({ searchQuery? })` lists the verifications you own — use it to find a `providerId` or to confirm the result. It's the only provider-listing tool in this mode.
|
|
207
|
-
- There's no separate "create-then-version" step and no org / public-review workflow here.
|
|
208
|
-
- Consumer-supplied params (step 10) keep the `{{context_<name>}}` form in this mode; there is no `requiredContext` declaration on the legacy backend.
|
|
180
|
+
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.
|
|
209
181
|
|
|
210
|
-
**
|
|
211
|
-
1. **Create the verification if it doesn't exist:** `create_verification(orgId, domain, slug, title, description)` — find `orgId` via `list_orgs`. `domain` e.g. `github.com`; `slug` e.g. `github-username`; `title` e.g. "GitHub Username". Starts PRIVATE with an empty seed version. Skip if the dev already has a `providerId`.
|
|
182
|
+
1. **Create the provider if it doesn't exist:** `create_provider(orgId, domain, slug, title, description)` — find `orgId` via `list_orgs`. `domain` e.g. `github.com`; `slug` e.g. `github-username`; `title` e.g. "GitHub Username". Starts PRIVATE with an empty seed version. Skip if the dev already has a `providerId`.
|
|
212
183
|
2. **Persist the version:** `create_provider_version_from_capture(providerId, provider, initialUrl, notes)` — `notes` e.g. `"Initial draft from browser capture"`. The version lands on the `draft` branch.
|
|
213
184
|
3. **`requiredContext` is derived for you.** The publish tool scans the recipe's url/body for `{{context.<name>}}` placeholders (step 10) and emits a JSON Schema requiring each one — the Builder then validates every session-create's `context` against it (a clear 400 instead of a late attestor failure). Extra context fields are always allowed. The derived properties are plain strings; to refine types or add human-readable descriptions, call `update_provider_version` with a `requiredContext` override while the version is on `draft`, e.g.:
|
|
214
185
|
|
|
@@ -216,7 +187,7 @@ The 2-day token is cached in `~/.reclaim/config.json` and reused on later runs (
|
|
|
216
187
|
{ "requiredContext": { "type": "object", "required": ["username"], "properties": { "username": { "type": "string", "description": "GitHub username to verify" } } } }
|
|
217
188
|
```
|
|
218
189
|
4. **Go live:** PRIVATE → `publish_provider_version`; PUBLIC → `submit_provider_version_for_review(providerId, version)` and tell the dev it's awaiting admin review.
|
|
219
|
-
5. **Tell the dev how consumers call it:** sessions for this
|
|
190
|
+
5. **Tell the dev how consumers call it:** sessions for this provider must include the context params, e.g. `context: { "username": "..." }` in `POST /verifications/sessions`. Quote the `requiredContext` returned on the created version — a session missing those fields gets a 400, and verification matches the supplied values exactly.
|
|
220
191
|
|
|
221
192
|
When done, tell the developer the version is live (PRIVATE) or submitted for review (PUBLIC), and that they can integrate using the `providerId`.
|
|
222
193
|
|