@reclaimprotocol/agent 0.1.33 → 0.1.35
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 +3 -3
- package/lib/authoring/backend.d.ts +13 -0
- package/lib/authoring/index.d.ts +1 -1
- package/lib/authoring/tools.js +78 -42
- package/lib/config/store.d.ts +10 -0
- package/lib/mcp/api/operations.gen.d.ts +238 -4
- package/lib/mcp/api/operations.gen.js +6967 -672
- package/lib/mcp/api/tools.js +21 -8
- package/lib/mcp/instructions.js +12 -12
- package/lib/mcp/server.d.ts +1 -1
- package/lib/mcp/server.js +3 -0
- package/lib/mcp/start.js +8 -7
- package/lib/mcp/success-response.js +5 -1
- package/lib/mcp/tools/agent/analyze.js +5 -4
- package/lib/mcp/tools/agent/attach.js +11 -7
- package/lib/mcp/tools/agent/build.js +5 -1
- package/lib/mcp/tools/agent/capture.d.ts +2 -2
- package/lib/mcp/tools/agent/capture.js +15 -11
- package/lib/mcp/tools/agent/check-attestor.d.ts +5 -0
- package/lib/mcp/tools/agent/check-attestor.js +37 -0
- package/lib/mcp/tools/agent/eval.js +4 -3
- package/lib/mcp/tools/agent/find.js +9 -10
- package/lib/mcp/tools/agent/how-it-works.js +1 -0
- package/lib/mcp/tools/agent/live-view.js +49 -22
- package/lib/mcp/tools/agent/pod-proof.js +7 -11
- package/lib/mcp/tools/agent/proof.d.ts +12 -0
- package/lib/mcp/tools/agent/proof.js +32 -0
- package/lib/mcp/tools/agent/propose.js +4 -3
- package/lib/mcp/tools/agent/publish.d.ts +2 -1
- package/lib/mcp/tools/agent/publish.js +12 -6
- package/lib/mcp/tools/agent/replay.js +10 -7
- package/lib/mcp/tools/agent/session.js +16 -8
- package/lib/mcp/tools/agent/shared.d.ts +2 -1
- package/lib/mcp/tools/agent/shared.js +29 -24
- package/lib/mcp/tools/agent/start-integration.d.ts +15 -0
- package/lib/mcp/tools/agent/start-integration.js +62 -0
- package/lib/mcp/tools/authenticate/authenticate.js +9 -11
- package/lib/mcp/tools/authenticate/build.js +2 -0
- package/lib/mcp/tools/authenticate/builder-access.js +1 -0
- package/lib/mcp/tools/authenticate/eth-key.d.ts +8 -0
- package/lib/mcp/tools/authenticate/eth-key.js +27 -15
- package/lib/mcp/tools/authenticate/import.js +18 -31
- package/lib/mcp/tools/authenticate/issue-org-token.d.ts +8 -0
- package/lib/mcp/tools/authenticate/issue-org-token.js +60 -0
- package/lib/mcp/tools/authenticate/issue.js +9 -9
- package/lib/mcp/tools/authenticate/org-key-store.d.ts +10 -0
- package/lib/mcp/tools/authenticate/org-key-store.js +23 -0
- package/lib/mcp/tools/authenticate/resolve-owner-key.js +10 -12
- package/lib/mcp/tools/mode/mode.js +2 -0
- package/lib/mcp/tools/verification/build.d.ts +3 -1
- package/lib/mcp/tools/verification/build.js +10 -2
- package/lib/mcp/tools/verification/callback-listener.d.ts +51 -0
- package/lib/mcp/tools/verification/callback-listener.js +163 -0
- package/lib/mcp/tools/verification/index.d.ts +2 -0
- package/lib/mcp/tools/verification/index.js +2 -0
- package/lib/mcp/tools/verification/proxy-callback-events.d.ts +59 -0
- package/lib/mcp/tools/verification/proxy-callback-events.js +300 -0
- package/lib/mcp/tools/verification/read-callback-events.d.ts +3 -0
- package/lib/mcp/tools/verification/read-callback-events.js +135 -0
- package/lib/mcp/tools/verification/result.d.ts +6 -0
- package/lib/mcp/tools/verification/result.js +11 -8
- package/lib/proof/check-attestor.d.ts +67 -0
- package/lib/proof/check-attestor.js +156 -0
- package/lib/tools/registry.d.ts +12 -0
- package/lib/tools/registry.js +1 -0
- package/package.json +2 -2
- package/skill/SKILL.builder.md +84 -75
- package/skill/SKILL.common.md +23 -10
- package/skill/builder-mode.md +140 -138
- package/skill/start-integration.md +74 -0
- package/skill/user-script.md +64 -0
- package/skill/verification-client-integration.md +40 -28
- package/src/authoring/backend.ts +15 -0
- package/src/authoring/index.ts +2 -0
- package/src/authoring/tools.ts +86 -42
- package/src/config/store.ts +11 -0
- package/src/mcp/api/operations.gen.ts +7011 -704
- package/src/mcp/api/tools.ts +21 -8
- package/src/mcp/instructions.ts +12 -12
- package/src/mcp/server.ts +4 -0
- package/src/mcp/start.ts +9 -7
- package/src/mcp/success-response.ts +5 -1
- package/src/mcp/tools/agent/analyze.ts +5 -4
- package/src/mcp/tools/agent/attach.ts +11 -7
- package/src/mcp/tools/agent/build.ts +7 -1
- package/src/mcp/tools/agent/capture.ts +20 -12
- package/src/mcp/tools/agent/check-attestor.ts +49 -0
- package/src/mcp/tools/agent/eval.ts +4 -3
- package/src/mcp/tools/agent/find.ts +9 -10
- package/src/mcp/tools/agent/how-it-works.ts +1 -0
- package/src/mcp/tools/agent/live-view.ts +49 -22
- package/src/mcp/tools/agent/pod-proof.ts +7 -11
- package/src/mcp/tools/agent/proof.ts +55 -0
- package/src/mcp/tools/agent/propose.ts +4 -3
- package/src/mcp/tools/agent/publish.ts +12 -4
- package/src/mcp/tools/agent/replay.ts +10 -7
- package/src/mcp/tools/agent/session.ts +16 -8
- package/src/mcp/tools/agent/shared.ts +39 -24
- package/src/mcp/tools/agent/start-integration.ts +84 -0
- package/src/mcp/tools/authenticate/authenticate.ts +9 -11
- package/src/mcp/tools/authenticate/build.ts +2 -0
- package/src/mcp/tools/authenticate/builder-access.ts +1 -0
- package/src/mcp/tools/authenticate/eth-key.ts +48 -18
- package/src/mcp/tools/authenticate/import.ts +21 -36
- package/src/mcp/tools/authenticate/issue-org-token.ts +84 -0
- package/src/mcp/tools/authenticate/issue.ts +9 -9
- package/src/mcp/tools/authenticate/org-key-store.ts +25 -0
- package/src/mcp/tools/authenticate/resolve-owner-key.ts +10 -12
- package/src/mcp/tools/mode/mode.ts +2 -0
- package/src/mcp/tools/verification/build.ts +10 -2
- package/src/mcp/tools/verification/callback-listener.ts +203 -0
- package/src/mcp/tools/verification/index.ts +2 -0
- package/src/mcp/tools/verification/proxy-callback-events.ts +374 -0
- package/src/mcp/tools/verification/read-callback-events.ts +160 -0
- package/src/mcp/tools/verification/result.ts +11 -8
- package/src/proof/check-attestor.ts +209 -0
- package/src/tools/registry.ts +14 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
|
|
2
|
+
if (kind === "m") throw new TypeError("Private method is not writable");
|
|
3
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
|
|
4
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
|
|
5
|
+
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
|
|
6
|
+
};
|
|
7
|
+
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
|
|
8
|
+
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
|
|
9
|
+
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
|
|
10
|
+
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
|
|
11
|
+
};
|
|
12
|
+
var _Forwarder_instances, _Forwarder_client, _Forwarder_deps, _Forwarder_abort, _Forwarder_forwarded, _Forwarder_failed, _Forwarder_lastError, _Forwarder_recent, _Forwarder_unacked, _Forwarder_host, _Forwarder_running, _Forwarder_loop, _Forwarder_tick, _Forwarder_forward, _ForwarderRegistry_forwarders;
|
|
13
|
+
import { badRequest } from '@hapi/boom';
|
|
14
|
+
import assert from 'node:assert';
|
|
15
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
16
|
+
import { LOGGER } from "../../../logger.js";
|
|
17
|
+
import { defineTool } from "../../server.js";
|
|
18
|
+
import { ensureListener, orgClient, } from "./callback-listener.js";
|
|
19
|
+
import { resolveOrgSecret } from "./result.js";
|
|
20
|
+
const POLL_MS = 2000;
|
|
21
|
+
const MAX_BACKOFF_MS = 30000;
|
|
22
|
+
const FORWARD_TIMEOUT_MS = 10000;
|
|
23
|
+
const BATCH = 50;
|
|
24
|
+
const RECENT = 10;
|
|
25
|
+
/** Loopback addresses, in the order tried. Dev servers often bind only one. */
|
|
26
|
+
const HOSTS = ['127.0.0.1', '[::1]'];
|
|
27
|
+
/**
|
|
28
|
+
* Polls the org's pending pull events and POSTs each envelope to a local
|
|
29
|
+
* route, like `stripe listen --forward-to`. 2xx responses are acknowledged;
|
|
30
|
+
* anything else (redirects included) stays pending and is retried with
|
|
31
|
+
* exponential backoff. After a failure the batch stops, so events reach the
|
|
32
|
+
* app in order.
|
|
33
|
+
*/
|
|
34
|
+
class Forwarder {
|
|
35
|
+
constructor(client, listener, target, deps) {
|
|
36
|
+
_Forwarder_instances.add(this);
|
|
37
|
+
_Forwarder_client.set(this, void 0);
|
|
38
|
+
_Forwarder_deps.set(this, void 0);
|
|
39
|
+
_Forwarder_abort.set(this, new AbortController());
|
|
40
|
+
_Forwarder_forwarded.set(this, 0);
|
|
41
|
+
_Forwarder_failed.set(this, 0);
|
|
42
|
+
_Forwarder_lastError.set(this, void 0);
|
|
43
|
+
_Forwarder_recent.set(this, []
|
|
44
|
+
/** Delivered to the app but not yet acknowledged; never POSTed twice. */
|
|
45
|
+
);
|
|
46
|
+
/** Delivered to the app but not yet acknowledged; never POSTed twice. */
|
|
47
|
+
_Forwarder_unacked.set(this, new Set());
|
|
48
|
+
_Forwarder_host.set(this, HOSTS[0]);
|
|
49
|
+
_Forwarder_running.set(this, void 0);
|
|
50
|
+
__classPrivateFieldSet(this, _Forwarder_client, client, "f");
|
|
51
|
+
this.listener = listener;
|
|
52
|
+
this.target = target;
|
|
53
|
+
__classPrivateFieldSet(this, _Forwarder_deps, deps, "f");
|
|
54
|
+
__classPrivateFieldSet(this, _Forwarder_running, __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_loop).call(this), "f");
|
|
55
|
+
}
|
|
56
|
+
status() {
|
|
57
|
+
return {
|
|
58
|
+
forwarding: !__classPrivateFieldGet(this, _Forwarder_abort, "f").signal.aborted,
|
|
59
|
+
target: targetUrl(this.target, HOSTS[0]),
|
|
60
|
+
listener: this.listener,
|
|
61
|
+
forwarded: __classPrivateFieldGet(this, _Forwarder_forwarded, "f"),
|
|
62
|
+
failed: __classPrivateFieldGet(this, _Forwarder_failed, "f"),
|
|
63
|
+
...(__classPrivateFieldGet(this, _Forwarder_lastError, "f") ? { lastError: __classPrivateFieldGet(this, _Forwarder_lastError, "f") } : {}),
|
|
64
|
+
recent: [...__classPrivateFieldGet(this, _Forwarder_recent, "f")],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async stop() {
|
|
68
|
+
__classPrivateFieldGet(this, _Forwarder_abort, "f").abort();
|
|
69
|
+
await __classPrivateFieldGet(this, _Forwarder_running, "f");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
_Forwarder_client = new WeakMap(), _Forwarder_deps = new WeakMap(), _Forwarder_abort = new WeakMap(), _Forwarder_forwarded = new WeakMap(), _Forwarder_failed = new WeakMap(), _Forwarder_lastError = new WeakMap(), _Forwarder_recent = new WeakMap(), _Forwarder_unacked = new WeakMap(), _Forwarder_host = new WeakMap(), _Forwarder_running = new WeakMap(), _Forwarder_instances = new WeakSet(), _Forwarder_loop = async function _Forwarder_loop() {
|
|
73
|
+
const sleep = __classPrivateFieldGet(this, _Forwarder_deps, "f").sleep || sleepFor;
|
|
74
|
+
const signal = __classPrivateFieldGet(this, _Forwarder_abort, "f").signal;
|
|
75
|
+
let backoff = 0;
|
|
76
|
+
while (!signal.aborted) {
|
|
77
|
+
let wait = POLL_MS;
|
|
78
|
+
try {
|
|
79
|
+
const { ok, count } = await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_tick).call(this);
|
|
80
|
+
backoff = ok ? 0 : nextBackoff(backoff);
|
|
81
|
+
wait = ok ? (count >= BATCH ? 0 : POLL_MS) : backoff;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
__classPrivateFieldSet(this, _Forwarder_lastError, err instanceof Error ? err.message : String(err), "f");
|
|
85
|
+
LOGGER.warn({ err }, 'callback proxy poll failed');
|
|
86
|
+
backoff = nextBackoff(backoff);
|
|
87
|
+
wait = backoff;
|
|
88
|
+
}
|
|
89
|
+
if (wait && !signal.aborted) {
|
|
90
|
+
await sleep(wait, signal);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}, _Forwarder_tick = async function _Forwarder_tick() {
|
|
94
|
+
const { data } = await __classPrivateFieldGet(this, _Forwarder_client, "f").call('ListCallbackEvents', {
|
|
95
|
+
query: { pending: true, limit: BATCH },
|
|
96
|
+
});
|
|
97
|
+
const ack = [];
|
|
98
|
+
let ok = true;
|
|
99
|
+
for (const item of data.items) {
|
|
100
|
+
if (__classPrivateFieldGet(this, _Forwarder_abort, "f").signal.aborted) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
if (__classPrivateFieldGet(this, _Forwarder_unacked, "f").has(item.id) || await __classPrivateFieldGet(this, _Forwarder_instances, "m", _Forwarder_forward).call(this, item)) {
|
|
104
|
+
ack.push(item.id);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Stop the batch so later events don't overtake the failed one.
|
|
108
|
+
ok = false;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (ack.length) {
|
|
113
|
+
for (const id of ack) {
|
|
114
|
+
__classPrivateFieldGet(this, _Forwarder_unacked, "f").add(id);
|
|
115
|
+
}
|
|
116
|
+
await __classPrivateFieldGet(this, _Forwarder_client, "f").call('AcknowledgeCallbackEvents', {
|
|
117
|
+
body: { ids: ack },
|
|
118
|
+
});
|
|
119
|
+
for (const id of ack) {
|
|
120
|
+
__classPrivateFieldGet(this, _Forwarder_unacked, "f").delete(id);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { ok, count: data.items.length };
|
|
124
|
+
}, _Forwarder_forward = async function _Forwarder_forward(item) {
|
|
125
|
+
const doFetch = __classPrivateFieldGet(this, _Forwarder_deps, "f").forwardFetch || globalThis.fetch;
|
|
126
|
+
const record = (status, httpStatus) => {
|
|
127
|
+
__classPrivateFieldGet(this, _Forwarder_recent, "f").push({
|
|
128
|
+
id: item.id,
|
|
129
|
+
sessionId: item.sessionId,
|
|
130
|
+
event: item.event,
|
|
131
|
+
status,
|
|
132
|
+
...(httpStatus === undefined ? {} : { httpStatus }),
|
|
133
|
+
at: new Date().toISOString(),
|
|
134
|
+
});
|
|
135
|
+
__classPrivateFieldGet(this, _Forwarder_recent, "f").splice(0, __classPrivateFieldGet(this, _Forwarder_recent, "f").length - RECENT);
|
|
136
|
+
};
|
|
137
|
+
const hosts = [__classPrivateFieldGet(this, _Forwarder_host, "f"), ...HOSTS.filter((h) => h !== __classPrivateFieldGet(this, _Forwarder_host, "f"))];
|
|
138
|
+
for (const host of hosts) {
|
|
139
|
+
try {
|
|
140
|
+
const res = await doFetch(targetUrl(this.target, host), {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Content-Type': 'application/json' },
|
|
143
|
+
body: JSON.stringify(item.envelope),
|
|
144
|
+
signal: AbortSignal.timeout(FORWARD_TIMEOUT_MS),
|
|
145
|
+
// Never follow a redirect: it could re-POST the signed result
|
|
146
|
+
// to a host outside this machine.
|
|
147
|
+
redirect: 'manual',
|
|
148
|
+
});
|
|
149
|
+
__classPrivateFieldSet(this, _Forwarder_host, host, "f");
|
|
150
|
+
if (res.ok) {
|
|
151
|
+
__classPrivateFieldSet(this, _Forwarder_forwarded, __classPrivateFieldGet(this, _Forwarder_forwarded, "f") + 1, "f");
|
|
152
|
+
record('delivered', res.status);
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
__classPrivateFieldSet(this, _Forwarder_failed, __classPrivateFieldGet(this, _Forwarder_failed, "f") + 1, "f");
|
|
156
|
+
const url = targetUrl(this.target, host);
|
|
157
|
+
__classPrivateFieldSet(this, _Forwarder_lastError, res.status >= 300 && res.status < 400
|
|
158
|
+
? `${url} redirected (HTTP ${res.status}). Redirects are not `
|
|
159
|
+
+ 'followed; call again with the redirect target\'s `path`.'
|
|
160
|
+
: `${url} returned HTTP ${res.status}`, "f");
|
|
161
|
+
record('failed', res.status);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
__classPrivateFieldSet(this, _Forwarder_lastError, err instanceof Error ? err.message : String(err), "f");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// No loopback address accepted the connection.
|
|
169
|
+
__classPrivateFieldSet(this, _Forwarder_failed, __classPrivateFieldGet(this, _Forwarder_failed, "f") + 1, "f");
|
|
170
|
+
record('failed');
|
|
171
|
+
return false;
|
|
172
|
+
};
|
|
173
|
+
/** The forwarders of one MCP process, one per org key. */
|
|
174
|
+
export class ForwarderRegistry {
|
|
175
|
+
constructor() {
|
|
176
|
+
_ForwarderRegistry_forwarders.set(this, new Map());
|
|
177
|
+
}
|
|
178
|
+
get(secret) {
|
|
179
|
+
return __classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").get(secret);
|
|
180
|
+
}
|
|
181
|
+
set(secret, forwarder) {
|
|
182
|
+
__classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").set(secret, forwarder);
|
|
183
|
+
}
|
|
184
|
+
async stop(secret) {
|
|
185
|
+
const forwarder = __classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").get(secret);
|
|
186
|
+
__classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").delete(secret);
|
|
187
|
+
await forwarder?.stop();
|
|
188
|
+
return forwarder;
|
|
189
|
+
}
|
|
190
|
+
async stopAll() {
|
|
191
|
+
const all = [...__classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").values()];
|
|
192
|
+
__classPrivateFieldGet(this, _ForwarderRegistry_forwarders, "f").clear();
|
|
193
|
+
await Promise.all(all.map((f) => f.stop()));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
_ForwarderRegistry_forwarders = new WeakMap();
|
|
197
|
+
const defaultRegistry = new ForwarderRegistry();
|
|
198
|
+
/** Stops every forwarder. Called when the MCP server shuts down. */
|
|
199
|
+
export function stopAllForwarders() {
|
|
200
|
+
return defaultRegistry.stopAll();
|
|
201
|
+
}
|
|
202
|
+
export function proxyCallbackEventsTool(deps = {}) {
|
|
203
|
+
const registry = deps.registry || defaultRegistry;
|
|
204
|
+
return defineTool({
|
|
205
|
+
name: 'proxy_callback_events',
|
|
206
|
+
description: 'Forwards the events Reclaim sends to the org\'s pull listener '
|
|
207
|
+
+ 'to a local route, like `stripe listen --forward-to`. Call it '
|
|
208
|
+
+ 'when the app\'s callback route runs locally. It POSTs each '
|
|
209
|
+
+ 'envelope as JSON to `http://127.0.0.1:<port><path>` (loopback '
|
|
210
|
+
+ 'only; also tries `[::1]`) in order, acknowledges 2xx '
|
|
211
|
+
+ 'responses, and retries anything else, redirects included, '
|
|
212
|
+
+ 'with backoff. Creates the listener on first use, so it is not '
|
|
213
|
+
+ 'read-only. One forwarder runs per org: call again to read '
|
|
214
|
+
+ 'status, or with another `port` or `path` to retarget. '
|
|
215
|
+
+ '`stop: true` stops forwarding only. Forwarding lasts only '
|
|
216
|
+
+ 'while this MCP server runs; production needs '
|
|
217
|
+
+ '`create_org_callback_subscription`. Returns `{ forwarding, '
|
|
218
|
+
+ 'target, listener, forwarded, failed, lastError?, recent }`; '
|
|
219
|
+
+ '`listener.expiresAt` is the lease end, and `recent` lists the '
|
|
220
|
+
+ 'last events with `status` and `httpStatus`.',
|
|
221
|
+
inputSchema: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: {
|
|
224
|
+
port: {
|
|
225
|
+
type: 'integer',
|
|
226
|
+
minimum: 1,
|
|
227
|
+
maximum: 65535,
|
|
228
|
+
description: 'The local port the app listens on.',
|
|
229
|
+
},
|
|
230
|
+
path: {
|
|
231
|
+
type: 'string',
|
|
232
|
+
pattern: '^/[^\\s#\\\\]*$',
|
|
233
|
+
maxLength: 500,
|
|
234
|
+
description: 'The callback route, starting with `/`. Defaults to `/`.',
|
|
235
|
+
},
|
|
236
|
+
stop: {
|
|
237
|
+
type: 'boolean',
|
|
238
|
+
description: 'Stop forwarding. `port` is not needed.',
|
|
239
|
+
},
|
|
240
|
+
orgToken: {
|
|
241
|
+
type: 'string',
|
|
242
|
+
pattern: '^rorg_',
|
|
243
|
+
maxLength: 200,
|
|
244
|
+
description: 'The organization API key (`rorg_…`). Defaults to '
|
|
245
|
+
+ 'the server\'s `RECLAIM_ORG_SECRET`, then the key saved by '
|
|
246
|
+
+ '`issue_org_token`. Never echo it back to the user.',
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
annotations: {
|
|
251
|
+
readOnlyHint: false,
|
|
252
|
+
idempotentHint: true,
|
|
253
|
+
openWorldHint: true,
|
|
254
|
+
},
|
|
255
|
+
}, async (args) => {
|
|
256
|
+
const secret = resolveOrgSecret(args.orgToken);
|
|
257
|
+
if (args.stop) {
|
|
258
|
+
const stopped = await registry.stop(secret);
|
|
259
|
+
return {
|
|
260
|
+
...(stopped?.status() || { forwarding: false }),
|
|
261
|
+
_notes: 'Forwarding stopped. The listener stays alive until the '
|
|
262
|
+
+ 'MCP server exits, so `read_callback_events` still works.',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const existing = registry.get(secret);
|
|
266
|
+
if (args.port === undefined) {
|
|
267
|
+
assert(existing, badRequest('Pass `port`, the local port the app listens on.'));
|
|
268
|
+
existing.listener = await ensureListener(secret, deps);
|
|
269
|
+
return existing.status();
|
|
270
|
+
}
|
|
271
|
+
const target = validateTarget(args.port, args.path ?? '/');
|
|
272
|
+
if (existing) {
|
|
273
|
+
existing.target = target;
|
|
274
|
+
existing.listener = await ensureListener(secret, deps);
|
|
275
|
+
return existing.status();
|
|
276
|
+
}
|
|
277
|
+
const client = orgClient(secret, deps);
|
|
278
|
+
const listener = await ensureListener(secret, deps);
|
|
279
|
+
const forwarder = new Forwarder(client, listener, target, deps);
|
|
280
|
+
registry.set(secret, forwarder);
|
|
281
|
+
return forwarder.status();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
/** Rejects anything that is not a path on a loopback port. */
|
|
285
|
+
export function validateTarget(port, path) {
|
|
286
|
+
assert(Number.isInteger(port) && port >= 1 && port <= 65535, badRequest('`port` must be an integer from 1 to 65535.'));
|
|
287
|
+
assert(path.length <= 500 && /^\/[^\s#\\]*$/.test(path), badRequest('`path` must start with "/" and contain no host, space, or "#".'));
|
|
288
|
+
const url = new URL(targetUrl({ port, path }, HOSTS[0]));
|
|
289
|
+
assert(url.hostname === HOSTS[0] && url.port === String(port), badRequest('`path` must not change the target host.'));
|
|
290
|
+
return { port, path };
|
|
291
|
+
}
|
|
292
|
+
function targetUrl(target, host) {
|
|
293
|
+
return `http://${host}:${target.port}${target.path}`;
|
|
294
|
+
}
|
|
295
|
+
async function sleepFor(ms, signal) {
|
|
296
|
+
await delay(ms, undefined, { signal }).catch(() => undefined);
|
|
297
|
+
}
|
|
298
|
+
function nextBackoff(current) {
|
|
299
|
+
return Math.min(current ? current * 2 : POLL_MS, MAX_BACKOFF_MS);
|
|
300
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { decryptCallback, ReclaimVerification } from '@reclaimprotocol/client';
|
|
2
|
+
import { apiUrl } from "../../../consts.js";
|
|
3
|
+
import { defineTool } from "../../server.js";
|
|
4
|
+
import { resolveDecryptionKey } from "../authenticate/decrypt-key.js";
|
|
5
|
+
import { ensureListener, orgClient, } from "./callback-listener.js";
|
|
6
|
+
import { resolveOrgSecret } from "./result.js";
|
|
7
|
+
/** Events whose `data` is the signed result (encrypted or plaintext JWS). */
|
|
8
|
+
const RESULT_EVENTS = new Set([
|
|
9
|
+
'verification_success',
|
|
10
|
+
'verification_rejected',
|
|
11
|
+
'verification_error',
|
|
12
|
+
'verification_cancelled',
|
|
13
|
+
]);
|
|
14
|
+
export function readCallbackEventsTool(deps = {}) {
|
|
15
|
+
return defineTool({
|
|
16
|
+
name: 'read_callback_events',
|
|
17
|
+
description: 'Reads the events Reclaim sent to the organization\'s pull '
|
|
18
|
+
+ 'listener, and decodes them: each result event is decrypted and '
|
|
19
|
+
+ 'signature-verified into `result`, and each lifecycle event '
|
|
20
|
+
+ 'returns its `eventData`. Call it to try a provider without an '
|
|
21
|
+
+ 'app or to check what Reclaim sent for a session. Creates the '
|
|
22
|
+
+ 'listener on first use, so it is not read-only. Filter with '
|
|
23
|
+
+ '`sessionId`, or pass the previous `next` as `after` to read on. '
|
|
24
|
+
+ 'Items with a lower sequence can appear late, so for a complete '
|
|
25
|
+
+ 'read use `pending: true`. Returns `{ listener, items, next }`. '
|
|
26
|
+
+ 'Encrypted results need the `RECLAIM_DECRYPT_KEY` that matches '
|
|
27
|
+
+ 'the org keypair. To deliver events to your app\'s callback '
|
|
28
|
+
+ 'route, call `proxy_callback_events`.',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: 'object',
|
|
31
|
+
properties: {
|
|
32
|
+
sessionId: {
|
|
33
|
+
type: 'string',
|
|
34
|
+
format: 'uuid',
|
|
35
|
+
description: 'Only events of this verification session.',
|
|
36
|
+
},
|
|
37
|
+
after: {
|
|
38
|
+
type: 'string',
|
|
39
|
+
pattern: '^[0-9]{1,19}$',
|
|
40
|
+
description: 'The `next` value of a previous call.',
|
|
41
|
+
},
|
|
42
|
+
limit: {
|
|
43
|
+
type: 'integer',
|
|
44
|
+
minimum: 1,
|
|
45
|
+
maximum: 100,
|
|
46
|
+
description: 'Events to return. Defaults to 50.',
|
|
47
|
+
},
|
|
48
|
+
pending: {
|
|
49
|
+
type: 'boolean',
|
|
50
|
+
description: 'Only events not yet acknowledged. `proxy_callback_events` '
|
|
51
|
+
+ 'acknowledges the events it delivers.',
|
|
52
|
+
},
|
|
53
|
+
orgToken: {
|
|
54
|
+
type: 'string',
|
|
55
|
+
pattern: '^rorg_',
|
|
56
|
+
maxLength: 200,
|
|
57
|
+
description: 'The organization API key (`rorg_…`). Defaults to '
|
|
58
|
+
+ 'the server\'s `RECLAIM_ORG_SECRET`, then the key saved by '
|
|
59
|
+
+ '`issue_org_token`. Never echo it back to the user.',
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
annotations: {
|
|
64
|
+
readOnlyHint: false,
|
|
65
|
+
idempotentHint: true,
|
|
66
|
+
openWorldHint: true,
|
|
67
|
+
},
|
|
68
|
+
}, async (args) => {
|
|
69
|
+
const secret = resolveOrgSecret(args.orgToken);
|
|
70
|
+
const client = orgClient(secret, deps);
|
|
71
|
+
const listener = await ensureListener(secret, deps);
|
|
72
|
+
const { data } = await client.call('ListCallbackEvents', {
|
|
73
|
+
query: {
|
|
74
|
+
after: args.after,
|
|
75
|
+
limit: args.limit,
|
|
76
|
+
sessionId: args.sessionId,
|
|
77
|
+
pending: args.pending,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
const hasResults = data.items.some((e) => RESULT_EVENTS.has(e.event));
|
|
81
|
+
const key = hasResults ? await resolveDecryptionKey() : undefined;
|
|
82
|
+
const reclaim = ReclaimVerification.create({
|
|
83
|
+
baseUrl: apiUrl(),
|
|
84
|
+
orgSecret: secret,
|
|
85
|
+
fetch: deps.fetch,
|
|
86
|
+
});
|
|
87
|
+
const items = [];
|
|
88
|
+
for (const item of data.items) {
|
|
89
|
+
items.push(await decode(item));
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
listener,
|
|
93
|
+
items,
|
|
94
|
+
next: data.next,
|
|
95
|
+
...(listener.created
|
|
96
|
+
? {
|
|
97
|
+
_notes: 'Created the pull listener; only sessions created from '
|
|
98
|
+
+ 'now on are recorded. It works only while this MCP server '
|
|
99
|
+
+ 'runs.',
|
|
100
|
+
}
|
|
101
|
+
: {}),
|
|
102
|
+
};
|
|
103
|
+
async function decode(item) {
|
|
104
|
+
const base = {
|
|
105
|
+
id: item.id,
|
|
106
|
+
sessionId: item.sessionId,
|
|
107
|
+
event: item.event,
|
|
108
|
+
deliveryStatus: item.deliveryStatus,
|
|
109
|
+
createdAt: item.createdAt,
|
|
110
|
+
};
|
|
111
|
+
try {
|
|
112
|
+
if (!RESULT_EVENTS.has(item.event)) {
|
|
113
|
+
const received = await reclaim.results.receive(item.envelope);
|
|
114
|
+
return received.kind === 'event'
|
|
115
|
+
? { ...base, eventData: received.eventData }
|
|
116
|
+
: base;
|
|
117
|
+
}
|
|
118
|
+
const jws = await decryptCallback(key, item.envelope);
|
|
119
|
+
const outcome = await reclaim.results.verify(jws, {
|
|
120
|
+
reclaimSessionId: item.sessionId,
|
|
121
|
+
});
|
|
122
|
+
return {
|
|
123
|
+
...base,
|
|
124
|
+
result: { payload: outcome.payload, proofs: outcome.proofs },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
129
|
+
return RESULT_EVENTS.has(item.event)
|
|
130
|
+
? { ...base, resultError: message }
|
|
131
|
+
: { ...base, eventError: message };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type RegisteredTool } from '../../server.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the org secret used to authenticate verification reads. Auth is a
|
|
4
|
+
* single per-org bearer secret (`rorg_…`), not an eth signature: the call's
|
|
5
|
+
* `orgToken`, else `RECLAIM_ORG_SECRET`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveOrgSecret(orgToken?: string): string;
|
|
2
8
|
/**
|
|
3
9
|
* `get_verification_result` — check a session's status and, once a result has
|
|
4
10
|
* been submitted, pull the stored payload(s) and verify locally. Auth is the
|
|
@@ -3,6 +3,7 @@ import assert from 'node:assert';
|
|
|
3
3
|
import { apiUrl, orgSecret } from "../../../consts.js";
|
|
4
4
|
import { defineTool } from "../../server.js";
|
|
5
5
|
import { resolveDecryptionKey } from "../authenticate/decrypt-key.js";
|
|
6
|
+
import { savedOrgKey } from "../authenticate/org-key-store.js";
|
|
6
7
|
/** Statuses that can carry a Builder-managed terminal result. */
|
|
7
8
|
const TERMINAL = new Set([
|
|
8
9
|
'success',
|
|
@@ -24,12 +25,11 @@ const RESULT_EVENTS = new Set([
|
|
|
24
25
|
* single per-org bearer secret (`rorg_…`), not an eth signature: the call's
|
|
25
26
|
* `orgToken`, else `RECLAIM_ORG_SECRET`.
|
|
26
27
|
*/
|
|
27
|
-
function resolveOrgSecret(orgToken) {
|
|
28
|
-
const secret = orgToken?.trim() || orgSecret()?.trim();
|
|
28
|
+
export function resolveOrgSecret(orgToken) {
|
|
29
|
+
const secret = orgToken?.trim() || orgSecret()?.trim() || savedOrgKey();
|
|
29
30
|
assert(secret, new Error('No organization API key. Verification reads authenticate with the org '
|
|
30
|
-
+ 'secret (rorg_…):
|
|
31
|
-
+ '
|
|
32
|
-
+ 'MCP server.'));
|
|
31
|
+
+ 'secret (rorg_…): run issue_org_token to save a key, pass orgToken, '
|
|
32
|
+
+ 'or export RECLAIM_ORG_SECRET for the MCP server.'));
|
|
33
33
|
return secret;
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
@@ -49,7 +49,8 @@ export function verificationResultTool() {
|
|
|
49
49
|
+ 'while Builder still holds it: until its callback is delivered, or '
|
|
50
50
|
+ '7 days undelivered. The callback is the normal way to get results. '
|
|
51
51
|
+ 'Authenticates with the org API key: `orgToken`, else '
|
|
52
|
-
+ '`RECLAIM_ORG_SECRET
|
|
52
|
+
+ '`RECLAIM_ORG_SECRET`, else the key saved by `issue_org_token`. '
|
|
53
|
+
+ 'Encrypted deliveries also require '
|
|
53
54
|
+ 'the matching ETH decryption key. Returns `ready: false` until a '
|
|
54
55
|
+ 'payload is available. Invalid results fail verification and are not '
|
|
55
56
|
+ 'returned. Use `list_verification_events` to diagnose '
|
|
@@ -68,11 +69,13 @@ export function verificationResultTool() {
|
|
|
68
69
|
maxLength: 200,
|
|
69
70
|
description: 'The organization API key (`rorg_…`): from '
|
|
70
71
|
+ '`issue_org_token`, or the app\'s RECLAIM_ORG_SECRET. '
|
|
71
|
-
+ 'Defaults to the MCP server\'s RECLAIM_ORG_SECRET
|
|
72
|
-
+ 'it back to the
|
|
72
|
+
+ 'Defaults to the MCP server\'s RECLAIM_ORG_SECRET, then the '
|
|
73
|
+
+ 'key saved by `issue_org_token`. Never echo it back to the '
|
|
74
|
+
+ 'user.',
|
|
73
75
|
},
|
|
74
76
|
},
|
|
75
77
|
},
|
|
78
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
76
79
|
}, async (args) => {
|
|
77
80
|
const secret = resolveOrgSecret(args.orgToken);
|
|
78
81
|
// Optional eth decryption key — absent ⇒ plaintext-only.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attestor reachability check. Proves a public request with a throwaway owner
|
|
3
|
+
* key to show whether the attestor works from this machine, independent of any
|
|
4
|
+
* developer provider or site.
|
|
5
|
+
*/
|
|
6
|
+
import { createClaimOnAttestor } from '@reclaimprotocol/attestor-core';
|
|
7
|
+
import './patch-domhandler.ts';
|
|
8
|
+
/**
|
|
9
|
+
* Default time budget for one check. A live run takes about 8 s; a longer
|
|
10
|
+
* budget risks the MCP client's own tool-call timeout.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CHECK_ATTESTOR_TIMEOUT_MS = 45000;
|
|
13
|
+
/**
|
|
14
|
+
* Request params for the check: the public CoinGecko price request from the
|
|
15
|
+
* attestor-core examples. Source:
|
|
16
|
+
* https://raw.githubusercontent.com/reclaimprotocol/attestor-core/refs/heads/main/example/coin-gecko.json
|
|
17
|
+
*/
|
|
18
|
+
export declare const COIN_GECKO_PARAMS: {
|
|
19
|
+
url: string;
|
|
20
|
+
method: "GET";
|
|
21
|
+
body: string;
|
|
22
|
+
headers: {
|
|
23
|
+
'User-Agent': string;
|
|
24
|
+
'Sec-Fetch-Mode': string;
|
|
25
|
+
'Sec-Fetch-Site': string;
|
|
26
|
+
};
|
|
27
|
+
responseMatches: {
|
|
28
|
+
invert: boolean;
|
|
29
|
+
type: "regex";
|
|
30
|
+
value: string;
|
|
31
|
+
}[];
|
|
32
|
+
responseRedactions: {
|
|
33
|
+
jsonPath: string;
|
|
34
|
+
regex: string;
|
|
35
|
+
xPath: string;
|
|
36
|
+
}[];
|
|
37
|
+
additionalClientOptions: {};
|
|
38
|
+
};
|
|
39
|
+
export type AttestorCheckErrorKind =
|
|
40
|
+
/** Socket, connection, DNS, TLS, or timeout failure. */
|
|
41
|
+
'attestor-unreachable'
|
|
42
|
+
/** The attestor answered and rejected or failed the claim. */
|
|
43
|
+
| 'attestor-error'
|
|
44
|
+
/** Local setup (ZK files) failed before the attestor was contacted. */
|
|
45
|
+
| 'setup-failed';
|
|
46
|
+
export interface AttestorCheckResult {
|
|
47
|
+
ok: boolean;
|
|
48
|
+
attestorUrl: string;
|
|
49
|
+
durationMs: number;
|
|
50
|
+
extractedParameters?: Record<string, string>;
|
|
51
|
+
error?: string;
|
|
52
|
+
errorKind?: AttestorCheckErrorKind;
|
|
53
|
+
}
|
|
54
|
+
/** Creates the claim on the attestor (`createClaimOnAttestor` by default). */
|
|
55
|
+
export type CreateClaim = (args: Parameters<typeof createClaimOnAttestor>[0]) => Promise<unknown>;
|
|
56
|
+
export interface CheckAttestorOptions {
|
|
57
|
+
attestorUrl?: string;
|
|
58
|
+
timeoutMs?: number;
|
|
59
|
+
/** Replaces the network call in tests. */
|
|
60
|
+
createClaim?: CreateClaim;
|
|
61
|
+
/** Replaces the ZK file download in tests. */
|
|
62
|
+
prepare?: () => Promise<void>;
|
|
63
|
+
}
|
|
64
|
+
/** True when `message` looks like a network failure rather than a rejection. */
|
|
65
|
+
export declare function isNetworkError(message: string | undefined): boolean;
|
|
66
|
+
/** Prove the public CoinGecko request once and classify the outcome. */
|
|
67
|
+
export declare function checkAttestor(opts?: CheckAttestorOptions): Promise<AttestorCheckResult>;
|