@reclaimprotocol/agent 0.1.11 → 0.1.12

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.
@@ -0,0 +1,242 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
+ import { isAbsolute, join } from 'node:path';
3
+ import { defineTool } from "../../mcp/server.js";
4
+ import { findProjectRoot } from "../../paths.js";
5
+ /** Corrected against `@reclaimprotocol/js-sdk` v5.8.1 (`Reclaim.ts`): awaits
6
+ * `getRequestUrl()`, uses `setContext` (not deprecated `addContext`), JSON-
7
+ * parses the raw webhook body before `verifyProof`, passes the required
8
+ * `{ providerId, providerVersion }` config, and reads `result.isVerified`
9
+ * (not `is_verified`). `providerVersion` is left unpinned (`undefined`) by
10
+ * default — the built-in "example" providerId isn't versioned, so pinning
11
+ * one is only meaningful once you swap in your own provider. */
12
+ function serverJs(providerId, providerVersion) {
13
+ return `import express from "express";
14
+ import "dotenv/config";
15
+ import { ReclaimProofRequest, verifyProof } from "@reclaimprotocol/js-sdk";
16
+ import { randomUUID } from "node:crypto";
17
+
18
+ const app = express();
19
+ const port = process.env.PORT || 3000;
20
+ const providerId = ${JSON.stringify(providerId)};
21
+ // Left unpinned by default — set this if you need to pin a specific version.
22
+ const providerVersion = ${providerVersion ? JSON.stringify(providerVersion) : 'undefined'};
23
+
24
+ app.set("trust proxy", true);
25
+ app.use(express.json({ limit: "2mb" }));
26
+
27
+ app.get("/health", (_req, res) => {
28
+ res.json({ ok: true });
29
+ });
30
+
31
+ app.get("/verify", (_req, res) => {
32
+ res.type("html").send(\`<!doctype html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="utf-8" />
36
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
37
+ <title>Reclaim demo verification</title>
38
+ <style>
39
+ body { font-family: system-ui, sans-serif; max-width: 680px; margin: 64px auto; padding: 0 20px; line-height: 1.5; }
40
+ code { background: #f3f4f6; padding: 2px 6px; border-radius: 4px; }
41
+ button { background: #111827; border: 0; border-radius: 8px; color: white; cursor: pointer; font: inherit; padding: 12px 18px; }
42
+ button:disabled { cursor: wait; opacity: 0.7; }
43
+ pre { background: #f3f4f6; border-radius: 8px; overflow: auto; padding: 12px; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <h1>Reclaim demo verification</h1>
48
+ <p>Uses provider <code>\${providerId}</code>.</p>
49
+ <button id="start">Start verification</button>
50
+ <pre id="status">Ready.</pre>
51
+ <p>Webhook endpoint: <code>/webhooks/reclaim</code></p>
52
+ <script>
53
+ const button = document.getElementById("start");
54
+ const status = document.getElementById("status");
55
+ button.addEventListener("click", async () => {
56
+ button.disabled = true;
57
+ status.textContent = "Creating verification request...";
58
+ try {
59
+ const response = await fetch("/api/reclaim/request", { method: "POST" });
60
+ if (!response.ok) throw new Error(await response.text());
61
+ const { requestUrl } = await response.json();
62
+ status.textContent = "Opening Reclaim verification...";
63
+ window.open(requestUrl, "_blank", "noopener,noreferrer");
64
+ status.textContent = "Verification opened. Complete it in the Reclaim window.";
65
+ } catch (error) {
66
+ status.textContent = error instanceof Error ? error.message : String(error);
67
+ button.disabled = false;
68
+ }
69
+ });
70
+ </script>
71
+ </body>
72
+ </html>\`);
73
+ });
74
+
75
+ app.post("/api/reclaim/request", async (req, res, next) => {
76
+ try {
77
+ const appId = process.env.RECLAIM_APP_ID;
78
+ const appSecret = process.env.RECLAIM_APP_SECRET;
79
+ if (!appId || !appSecret) {
80
+ return res.status(500).json({ error: "Missing RECLAIM_APP_ID or RECLAIM_APP_SECRET" });
81
+ }
82
+
83
+ const origin = process.env.PUBLIC_URL || \`\${req.protocol}://\${req.get("host")}\`;
84
+ const reclaimProofRequest = await ReclaimProofRequest.init(appId, appSecret, providerId, {
85
+ providerVersion,
86
+ });
87
+ reclaimProofRequest.setAppCallbackUrl(\`\${origin}/webhooks/reclaim\`, true);
88
+ reclaimProofRequest.setContext(
89
+ randomUUID(),
90
+ JSON.stringify({ createdAt: new Date().toISOString() }),
91
+ );
92
+
93
+ // getRequestUrl() is ASYNC in the current SDK — must be awaited.
94
+ const requestUrl = await reclaimProofRequest.getRequestUrl();
95
+
96
+ res.json({
97
+ request: reclaimProofRequest.toJsonString(),
98
+ requestUrl,
99
+ });
100
+ } catch (error) {
101
+ next(error);
102
+ }
103
+ });
104
+
105
+ app.post("/webhooks/reclaim", async (req, res, next) => {
106
+ try {
107
+ // With jsonProofResponse: true + express.json() above, req.body is
108
+ // already a parsed Proof/Proof[]. Without either of those, the raw body
109
+ // is text and must be JSON.parse()'d (and URL-decoded if form-encoded)
110
+ // before calling verifyProof.
111
+ const proof = req.body;
112
+
113
+ // The second argument is REQUIRED — must include providerId.
114
+ const result = await verifyProof(proof, { providerId, providerVersion });
115
+
116
+ console.log("Received Reclaim verification:");
117
+ console.log(JSON.stringify({ isVerified: result.isVerified, data: result.data, error: result.error }, null, 2));
118
+
119
+ res.status(200).json({ received: true, verified: result.isVerified });
120
+ } catch (error) {
121
+ next(error);
122
+ }
123
+ });
124
+
125
+ app.use((err, _req, res, _next) => {
126
+ console.error(err);
127
+ res.status(500).json({ error: err instanceof Error ? err.message : "Internal server error" });
128
+ });
129
+
130
+ app.listen(port, () => {
131
+ console.log(\`Reclaim demo server listening on http://localhost:\${port}\`);
132
+ console.log(\`Verify page: http://localhost:\${port}/verify\`);
133
+ console.log(\`Webhook endpoint: http://localhost:\${port}/webhooks/reclaim\`);
134
+ if (!process.env.PUBLIC_URL) {
135
+ console.log("Set PUBLIC_URL (e.g. an ngrok URL) so the webhook is reachable from Reclaim's servers.");
136
+ }
137
+ });
138
+ `;
139
+ }
140
+ const PACKAGE_JSON = (name) => JSON.stringify({
141
+ name,
142
+ private: true,
143
+ type: 'module',
144
+ scripts: { start: 'node server.js' },
145
+ dependencies: {
146
+ '@reclaimprotocol/js-sdk': '^5.8.1',
147
+ dotenv: '^16.4.5',
148
+ express: '^4.21.2',
149
+ },
150
+ }, null, 2) + '\n';
151
+ const ENV_EXAMPLE = `RECLAIM_APP_ID=
152
+ RECLAIM_APP_SECRET=
153
+ PORT=3000
154
+ # REQUIRED for the webhook callback to land — Reclaim's servers can't reach
155
+ # localhost. Run: ngrok http 3000 (in a separate terminal) and paste the
156
+ # https://<id>.ngrok-free.app URL it prints here. Without this, only the
157
+ # frontend-side flow works (opening requestUrl) — the webhook never fires.
158
+ PUBLIC_URL=
159
+ `;
160
+ const README = `# Reclaim demo server
161
+
162
+ 1. \`npm install\`
163
+ 2. Fill in \`.env\` (copy from \`.env.example\`) — get \`RECLAIM_APP_ID\`/
164
+ \`RECLAIM_APP_SECRET\` from the \`issue_app_credentials\` MCP tool, or your
165
+ existing app.
166
+ 3. In a separate terminal, run \`ngrok http 3000\` (install from
167
+ https://ngrok.com if needed) and copy the \`https://<id>.ngrok-free.app\`
168
+ URL it prints into \`PUBLIC_URL\` in \`.env\`. This is REQUIRED — Reclaim's
169
+ servers must reach your webhook over the public internet, and
170
+ \`localhost\` is not reachable from there.
171
+ 4. \`npm start\`, then open http://localhost:3000/verify and click "Start
172
+ verification".
173
+ 5. Watch the server logs for the \`/webhooks/reclaim\` payload once you
174
+ complete verification.
175
+
176
+ This demo defaults to providerId \`"example"\` — a built-in smoke-test
177
+ provider, so you can confirm the whole loop (request → open → callback)
178
+ works before wiring in a real provider.
179
+ `;
180
+ /**
181
+ * Scaffold a small, runnable Express demo that exercises the full
182
+ * @reclaimprotocol/js-sdk flow end-to-end: request a proof (backend), open it
183
+ * (frontend), and verify the async webhook callback (backend). Corrected
184
+ * against the CURRENT SDK API (see js-sdk-integration-guide) rather than
185
+ * older examples that predate several signature changes.
186
+ */
187
+ const DEFAULT_PROVIDER_ID = 'example';
188
+ export function scaffoldDemoServerTool() {
189
+ return defineTool({
190
+ name: 'scaffold_reclaim_demo_server',
191
+ description: 'Write a small runnable Express demo (server.js, package.json, ' +
192
+ '.env.example, README.md) that requests a Reclaim proof and ' +
193
+ 'verifies the async webhook callback, using the CURRENT ' +
194
+ '@reclaimprotocol/js-sdk API. Defaults `providerId` to the built-in ' +
195
+ '"example" smoke-test provider so you can confirm the whole loop ' +
196
+ 'without authoring one first, and requires ngrok (or similar) to ' +
197
+ 'expose the local server so the webhook callback can reach it — ' +
198
+ 'the generated README walks through that step. Use ' +
199
+ 'get_js_sdk_integration_guide for the annotated write-up of the ' +
200
+ 'same flow.',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ targetDir: {
205
+ type: 'string',
206
+ description: 'Directory to write the demo into (created if missing). ' +
207
+ 'Relative paths resolve against the project root.',
208
+ },
209
+ providerId: {
210
+ type: 'string',
211
+ description: 'Provider id the demo requests a proof for (default ' +
212
+ '"example", a built-in smoke-test provider — good for ' +
213
+ 'confirming the flow works before wiring in a real one).',
214
+ },
215
+ providerVersion: {
216
+ type: 'string',
217
+ description: 'Provider version to pin (default: unpinned — "example" ' +
218
+ 'isn\'t versioned, so only set this once you swap in your ' +
219
+ 'own provider).',
220
+ },
221
+ },
222
+ required: ['targetDir'],
223
+ },
224
+ }, async ({ targetDir, providerId, providerVersion }) => {
225
+ const dir = isAbsolute(targetDir)
226
+ ? targetDir
227
+ : join(findProjectRoot(process.cwd()), targetDir);
228
+ mkdirSync(dir, { recursive: true });
229
+ const id = providerId ?? DEFAULT_PROVIDER_ID;
230
+ writeFileSync(join(dir, 'server.js'), serverJs(id, providerVersion));
231
+ writeFileSync(join(dir, 'package.json'), PACKAGE_JSON('reclaim-demo-server'));
232
+ writeFileSync(join(dir, '.env.example'), ENV_EXAMPLE);
233
+ writeFileSync(join(dir, 'README.md'), README);
234
+ return {
235
+ dir,
236
+ providerId: id,
237
+ files: ['server.js', 'package.json', '.env.example', 'README.md'],
238
+ next: 'cd into the directory, npm install, copy .env.example to .env ' +
239
+ 'and fill it in (including PUBLIC_URL from ngrok), then npm start.',
240
+ };
241
+ });
242
+ }
@@ -0,0 +1,9 @@
1
+ import { type RegisteredTool } from '../../mcp/server.ts';
2
+ import type { ReclaimOldClient } from '../client.ts';
3
+ /**
4
+ * Health-check a project's @reclaimprotocol/js-sdk setup: SDK installed,
5
+ * RECLAIM_APP_ID/RECLAIM_APP_SECRET present (env or .env), and (if an appId
6
+ * is found) whether that app is actually linked/has quota, via the real
7
+ * check_app_status endpoint — not just static file checks.
8
+ */
9
+ export declare function verifySdkSetupTool(client: ReclaimOldClient): RegisteredTool;
@@ -0,0 +1,128 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { defineTool } from "../../mcp/server.js";
4
+ import { findProjectRoot } from "../../paths.js";
5
+ function readEnvFile(dir) {
6
+ const path = join(dir, '.env');
7
+ if (!existsSync(path)) {
8
+ return {};
9
+ }
10
+ const out = {};
11
+ for (const line of readFileSync(path, 'utf8').split('\n')) {
12
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
13
+ if (m) {
14
+ out[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
15
+ }
16
+ }
17
+ return out;
18
+ }
19
+ /**
20
+ * Health-check a project's @reclaimprotocol/js-sdk setup: SDK installed,
21
+ * RECLAIM_APP_ID/RECLAIM_APP_SECRET present (env or .env), and (if an appId
22
+ * is found) whether that app is actually linked/has quota, via the real
23
+ * check_app_status endpoint — not just static file checks.
24
+ */
25
+ export function verifySdkSetupTool(client) {
26
+ return defineTool({
27
+ name: 'verify_reclaim_sdk_setup',
28
+ description: 'Health-check a project\'s @reclaimprotocol/js-sdk integration: SDK ' +
29
+ 'installed, RECLAIM_APP_ID/RECLAIM_APP_SECRET present, and (if an ' +
30
+ 'appId is found) whether that app is actually linked and has quota ' +
31
+ 'left, by calling the real status endpoint.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ projectDir: {
36
+ type: 'string',
37
+ description: 'Project directory (default: detected project root).',
38
+ },
39
+ },
40
+ },
41
+ }, async ({ projectDir }) => {
42
+ const dir = projectDir ?? findProjectRoot(process.cwd());
43
+ const checks = [];
44
+ const pkgPath = join(dir, 'package.json');
45
+ let sdkInPkg = false;
46
+ if (existsSync(pkgPath)) {
47
+ try {
48
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
49
+ sdkInPkg = Boolean(pkg.dependencies?.['@reclaimprotocol/js-sdk'] ||
50
+ pkg.devDependencies?.['@reclaimprotocol/js-sdk']);
51
+ }
52
+ catch {
53
+ // malformed package.json — fall through, reported below
54
+ }
55
+ }
56
+ const sdkInstalled = existsSync(join(dir, 'node_modules', '@reclaimprotocol', 'js-sdk'));
57
+ checks.push({
58
+ name: 'sdk_dependency',
59
+ ok: sdkInPkg,
60
+ detail: sdkInPkg
61
+ ? '@reclaimprotocol/js-sdk is listed in package.json'
62
+ : '@reclaimprotocol/js-sdk is NOT in package.json — run ' +
63
+ '`npm install @reclaimprotocol/js-sdk`.',
64
+ });
65
+ checks.push({
66
+ name: 'sdk_installed',
67
+ ok: sdkInstalled,
68
+ detail: sdkInstalled
69
+ ? '@reclaimprotocol/js-sdk found in node_modules'
70
+ : 'Not found in node_modules — run your package manager\'s ' +
71
+ 'install command.',
72
+ });
73
+ const env = { ...readEnvFile(dir), ...process.env };
74
+ const appId = env.RECLAIM_APP_ID;
75
+ const appSecret = env.RECLAIM_APP_SECRET;
76
+ checks.push({
77
+ name: 'app_id',
78
+ ok: Boolean(appId),
79
+ detail: appId
80
+ ? `RECLAIM_APP_ID set (${appId})`
81
+ : 'RECLAIM_APP_ID missing (env or .env) — run ' +
82
+ 'issue_app_credentials or link_app_to_account.',
83
+ });
84
+ checks.push({
85
+ name: 'app_secret',
86
+ ok: Boolean(appSecret),
87
+ detail: appSecret
88
+ ? 'RECLAIM_APP_SECRET set'
89
+ : 'RECLAIM_APP_SECRET missing (env or .env).',
90
+ });
91
+ // Informational only — PUBLIC_URL is only needed for the async
92
+ // webhook callback, not the frontend-only flow, so a missing value
93
+ // doesn't fail the overall check.
94
+ const publicUrl = env.PUBLIC_URL;
95
+ checks.push({
96
+ name: 'public_url',
97
+ ok: true,
98
+ detail: publicUrl
99
+ ? `PUBLIC_URL set (${publicUrl}) — used as the webhook callback ` +
100
+ 'base for setAppCallbackUrl.'
101
+ : 'PUBLIC_URL not set — fine if you only read the proof from the ' +
102
+ 'frontend flow, but the async webhook callback needs a ' +
103
+ 'publicly-reachable URL (e.g. from `ngrok http <port>`) or it ' +
104
+ 'will never fire.',
105
+ });
106
+ if (appId) {
107
+ try {
108
+ const status = await client.getApplicationStatus(appId);
109
+ const linked = status?.isLinked === true;
110
+ checks.push({
111
+ name: 'app_status',
112
+ ok: status?.available !== false,
113
+ detail: `isLinked=${linked}, sandboxMode=${status?.sandboxMode}, ` +
114
+ `available=${status?.available}` +
115
+ (linked ? '' : ' — run link_app_to_account to lift the sandbox limit.'),
116
+ });
117
+ }
118
+ catch (err) {
119
+ checks.push({
120
+ name: 'app_status',
121
+ ok: false,
122
+ detail: `Could not reach status endpoint: ${err instanceof Error ? err.message : String(err)}`,
123
+ });
124
+ }
125
+ }
126
+ return { projectDir: dir, checks, ok: checks.every((c) => c.ok) };
127
+ });
128
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reclaimprotocol/agent",
3
3
  "title": "Reclaim Agent",
4
- "version": "0.1.11",
4
+ "version": "0.1.12",
5
5
  "description": "Reclaim Protocol's tools for agents — Reclaim Protocol lets your app verify **data points** about its users — a follower count, an account balance, an order history. This package gives your AI coding agent the tools to **find, build, test and fix the _data verification providers_** (the recipes that verify a data point) by inspecting and replaying captured requests, checking sessions, evaluate proofs, publishing updates, and much more.",
6
6
  "type": "module",
7
7
  "main": "./lib/index.js",
@@ -88,6 +88,7 @@
88
88
  "@reclaimprotocol/tls": "^0.1.3",
89
89
  "chrome-remote-interface": "^0.33.0",
90
90
  "domhandler": "^6.0.0",
91
+ "ethers": "^6.17.0",
91
92
  "parse5": "^8.0.0",
92
93
  "parse5-htmlparser2-tree-adapter": "^8.0.0",
93
94
  "pino": "^9.14.0",
package/src/old/client.ts CHANGED
@@ -74,6 +74,22 @@ export interface MyProvidersQuery {
74
74
  searchQuery?: string
75
75
  }
76
76
 
77
+ /** `POST /api/applications/issue-credentials` response — a brand-new,
78
+ * unassociated (sandboxed) app. `appSecret` is the raw eth private key;
79
+ * it's returned once and never stored server-side in recoverable form. */
80
+ export interface IssuedAppCredentials {
81
+ appId: string
82
+ appSecret: string
83
+ }
84
+
85
+ /** `GET /api/applications/link/nonce` response — a one-time, 5-minute-TTL
86
+ * challenge the caller signs with the app's private key to prove ownership
87
+ * in `linkAppToAccount`. */
88
+ export interface LinkNonce {
89
+ nonce: string
90
+ expiresInSeconds: number
91
+ }
92
+
77
93
  interface RequestOpts {
78
94
  query?: Record<string, string | number | undefined>
79
95
  body?: unknown
@@ -187,6 +203,50 @@ export class ReclaimOldClient {
187
203
  return json
188
204
  }
189
205
 
206
+ /** Mint a brand-new, unassociated (sandboxed) app — no auth required. Link
207
+ * it to an account afterwards with `getLinkNonce` + `linkAppToAccount`, or
208
+ * use it as-is within the sandbox session limit. */
209
+ async issueCredentials(): Promise<IssuedAppCredentials> {
210
+ const res = await this.#request('POST', '/applications/issue-credentials')
211
+ return (res as { data: IssuedAppCredentials }).data
212
+ }
213
+
214
+ /** One-time, 5-minute challenge for `linkAppToAccount`. Requires auth —
215
+ * the nonce is scoped to the authenticated user's uid. */
216
+ async getLinkNonce(): Promise<LinkNonce> {
217
+ const res = await this.#request('GET', '/applications/link/nonce', {
218
+ auth: true,
219
+ })
220
+ return (res as { data: LinkNonce }).data
221
+ }
222
+
223
+ /** Link an unassociated (`issueCredentials`-minted) app to the
224
+ * authenticated account. `signature` must be an EIP-191 signature, made
225
+ * with the app's private key, over the exact string
226
+ * `` `Link ${appId} to account ${userId} | nonce: ${nonce}` `` — see
227
+ * `buildLinkMessage` in the backend's `application.controller.ts`. Fails
228
+ * with `APP_ALREADY_LINKED` (400) if the app already belongs to someone. */
229
+ linkAppToAccount(
230
+ appId: string,
231
+ signature: string,
232
+ nonce: string,
233
+ ): Promise<unknown> {
234
+ return this.#request('POST', '/applications/link', {
235
+ body: { appId, signature, nonce },
236
+ auth: true,
237
+ })
238
+ }
239
+
240
+ /** Public, unauthenticated app status — `isLinked`, sandbox/quota info.
241
+ * Works for any appId, owned or not. */
242
+ async getApplicationStatus(appId: string): Promise<unknown> {
243
+ const res = await this.#request(
244
+ 'GET',
245
+ `/applications/status/${encodeURIComponent(appId)}`,
246
+ )
247
+ return (res as { data: unknown }).data
248
+ }
249
+
190
250
  getMyProviders(query: MyProvidersQuery = {}): Promise<unknown> {
191
251
  // Every one of these query params is REQUIRED by the backend
192
252
  // validator (missing/invalid → 400), so default them all here.
@@ -2,11 +2,17 @@ import type { RegisteredTool } from '../../mcp/server.ts'
2
2
  import type { ReclaimOldClient } from '../client.ts'
3
3
  import type { ReclaimSdkClient } from '../sdk-client.ts'
4
4
  import { authenticateTool } from './authenticate.ts'
5
+ import { checkAppStatusTool } from './check-app-status.ts'
5
6
  import { getProviderInfoTool } from './get-provider-info.ts'
7
+ import { issueAppCredentialsTool } from './issue-app-credentials.ts'
8
+ import { getJsSdkIntegrationGuideTool } from './js-sdk-integration-guide.ts'
9
+ import { linkAppTool } from './link-app.ts'
6
10
  import { getMeProvidersTool } from './me-providers.ts'
7
11
  import { createVersionFromCaptureTool } from './publish.ts'
12
+ import { scaffoldDemoServerTool } from './scaffold-demo-server.ts'
8
13
  import { sessionAnalyticsLogsTool } from './session-analytics-logs.ts'
9
14
  import { updateProviderMetadataTool } from './update-provider-metadata.ts'
15
+ import { verifySdkSetupTool } from './verify-sdk-setup.ts'
10
16
 
11
17
  /**
12
18
  * The backend-facing tools for OLD-devtools mode. Mirrors
@@ -29,5 +35,11 @@ export function buildOldBackendTools(
29
35
  getProviderInfoTool(sdkClient),
30
36
  updateProviderMetadataTool(client),
31
37
  sessionAnalyticsLogsTool(client),
38
+ issueAppCredentialsTool(client),
39
+ linkAppTool(client),
40
+ checkAppStatusTool(client),
41
+ getJsSdkIntegrationGuideTool(),
42
+ scaffoldDemoServerTool(),
43
+ verifySdkSetupTool(client),
32
44
  ]
33
45
  }
@@ -0,0 +1,33 @@
1
+ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
2
+ import type { ReclaimOldClient } from '../client.ts'
3
+
4
+ /**
5
+ * Public, unauthenticated app status lookup — works for any `appId`, owned
6
+ * or not, linked or sandboxed. Mirrors the backend's
7
+ * `GET /api/applications/status/:appId`.
8
+ */
9
+ export function checkAppStatusTool(client: ReclaimOldClient): RegisteredTool {
10
+ return defineTool<{ appId: string }>(
11
+ {
12
+ name: 'check_app_status',
13
+ description:
14
+ 'Check a Reclaim app\'s link/sandbox status by appId — no ' +
15
+ 'authentication required. Returns whether the app is linked to a ' +
16
+ 'dashboard account (`isLinked`), and for linked apps, remaining ' +
17
+ 'session quota. A 404 means the appId has never been used yet — ' +
18
+ 'it is created lazily on the first ReclaimProofRequest.init() call ' +
19
+ 'or via issue_app_credentials.',
20
+ inputSchema: {
21
+ type: 'object',
22
+ properties: {
23
+ appId: {
24
+ type: 'string',
25
+ description: 'The eth-address appId to check.',
26
+ },
27
+ },
28
+ required: ['appId'],
29
+ },
30
+ },
31
+ async({ appId }) => client.getApplicationStatus(appId),
32
+ )
33
+ }
@@ -0,0 +1,42 @@
1
+ import { defineTool, type RegisteredTool } from '../../mcp/server.ts'
2
+ import type { ReclaimOldClient } from '../client.ts'
3
+
4
+ /**
5
+ * Mint a brand-new eth-keypair APP (as opposed to authenticating as a
6
+ * dashboard user). `appId` is the eth address, `appSecret` the raw private
7
+ * key — this is the pair `@reclaimprotocol/js-sdk`'s `ReclaimProofRequest.init`
8
+ * expects. No auth required: the app starts unassociated (`creatorUid: null`)
9
+ * and sandboxed (`sandboxLimit: 100` sessions). Use `link_app_to_account` to
10
+ * attach it to your dashboard account, or `check_app_status` to inspect it.
11
+ */
12
+ export function issueAppCredentialsTool(client: ReclaimOldClient): RegisteredTool {
13
+ return defineTool<Record<string, never>>(
14
+ {
15
+ name: 'issue_app_credentials',
16
+ description:
17
+ 'Mint a new Reclaim APP (eth keypair) on the old devtools backend — ' +
18
+ 'no authentication required. Returns `{ appId, appSecret }`: ' +
19
+ '`appId` is the eth address, `appSecret` the raw private key. This ' +
20
+ 'is the appId/appSecret pair for @reclaimprotocol/js-sdk ' +
21
+ '(`ReclaimProofRequest.init(appId, appSecret, providerId)`). The app ' +
22
+ 'starts UNLINKED and sandboxed (100 sessions) — call ' +
23
+ 'link_app_to_account to attach it to your dashboard account and ' +
24
+ 'check_app_status to check its state. Store `appSecret` like a ' +
25
+ 'password (e.g. in a gitignored .env as RECLAIM_APP_SECRET) — it ' +
26
+ 'is returned once and cannot be recovered from the server.',
27
+ inputSchema: { type: 'object', properties: {} },
28
+ },
29
+ async() => {
30
+ const { appId, appSecret } = await client.issueCredentials()
31
+ return {
32
+ appId,
33
+ appSecret,
34
+ note:
35
+ 'Unlinked, sandboxed app (100 session limit). Set RECLAIM_APP_ID ' +
36
+ 'and RECLAIM_APP_SECRET in your .env (gitignored). Run ' +
37
+ 'link_app_to_account to attach it to your dashboard account and ' +
38
+ 'lift the sandbox limit.',
39
+ }
40
+ },
41
+ )
42
+ }