@reclaimprotocol/agent 0.1.14 → 0.1.17
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/lib/authoring/index.d.ts +1 -1
- package/lib/authoring/index.js +1 -1
- package/lib/authoring/util.js +3 -0
- package/lib/config/store.d.ts +7 -0
- package/lib/consts.d.ts +8 -0
- package/lib/consts.js +20 -0
- package/lib/index.js +1 -1
- package/lib/mcp/api/operations.gen.d.ts +8 -16
- package/lib/mcp/api/operations.gen.js +428 -479
- package/lib/mcp/remap_errors.js +7 -14
- package/lib/mcp/start.js +10 -3
- package/lib/mcp/tools/agent/proof.js +3 -3
- package/lib/mcp/tools/authenticate/authenticate.js +178 -93
- package/lib/mcp/tools/authenticate/eth-key.js +2 -3
- package/lib/mcp/tools/index.d.ts +1 -0
- package/lib/mcp/tools/index.js +1 -0
- package/lib/mcp/tools/mode/index.d.ts +1 -0
- package/lib/mcp/tools/mode/index.js +3 -0
- package/lib/mcp/tools/mode/mode.d.ts +16 -0
- package/lib/mcp/tools/mode/mode.js +49 -0
- package/lib/old/tools/publish.js +75 -4
- package/lib/provider/to-version.js +4 -0
- package/lib/tools/registry.d.ts +6 -13
- package/lib/tools/registry.js +29 -0
- package/package.json +5 -8
- package/scripts/prepend-shebang.js +1 -1
- package/skill/injection-api.md +98 -7
- package/src/authoring/index.ts +1 -0
- package/src/authoring/util.ts +3 -0
- package/src/cdp/launcher.ts +1 -1
- package/src/config/store.ts +8 -0
- package/src/consts.ts +24 -0
- package/src/index.ts +1 -1
- package/src/mcp/api/operations.gen.ts +430 -487
- package/src/mcp/remap_errors.ts +8 -15
- package/src/mcp/start.ts +11 -2
- package/src/mcp/tools/agent/proof.ts +3 -3
- package/src/mcp/tools/authenticate/authenticate.ts +238 -108
- package/src/mcp/tools/authenticate/eth-key.ts +7 -5
- package/src/mcp/tools/index.ts +1 -0
- package/src/mcp/tools/mode/index.ts +3 -0
- package/src/mcp/tools/mode/mode.ts +58 -0
- package/src/old/tools/publish.ts +78 -4
- package/src/proof/debug-response-dump.ts +1 -1
- package/src/proof/proof-core.ts +1 -1
- package/src/proof/zk-files.ts +2 -2
- package/src/provider/dom-xpath.ts +1 -1
- package/src/provider/draft.ts +3 -3
- package/src/provider/to-version.ts +4 -0
- package/src/tools/registry.ts +19 -0
- package/src/cli/args.ts +0 -20
- package/src/cli/commands.ts +0 -27
- package/src/cli.ts +0 -7
package/lib/mcp/remap_errors.js
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { isBoom } from '@hapi/boom';
|
|
2
|
-
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// For every other error, let them get thrown so the main try-catch
|
|
6
|
-
// handles them in tool registration.
|
|
2
|
+
import { errorAsToolText } from "../tools/registry.js";
|
|
3
|
+
// One error shape for every MCP tool. The text comes from errorAsToolText
|
|
4
|
+
// (shared with the cloud loop); Boom problems also attach structuredContent.
|
|
7
5
|
export const remapErrorAsResponse = (err) => {
|
|
6
|
+
const text = errorAsToolText(err);
|
|
8
7
|
// The client throws a Boom-style ProblemError, so the status lives on
|
|
9
8
|
// `output.statusCode` / `problem.status`, not `status`.
|
|
10
9
|
if (!isBoom(err) || !err.output?.statusCode) {
|
|
11
10
|
return {
|
|
12
11
|
isError: true,
|
|
13
|
-
content: [{ type: 'text', text
|
|
12
|
+
content: [{ type: 'text', text }],
|
|
14
13
|
};
|
|
15
14
|
}
|
|
16
|
-
const problemError = err;
|
|
17
15
|
return {
|
|
18
16
|
isError: true,
|
|
19
|
-
structuredContent:
|
|
20
|
-
content: [
|
|
21
|
-
{
|
|
22
|
-
type: 'text',
|
|
23
|
-
text: `${problemError.message}: ${problemError.data?.detail}`
|
|
24
|
-
}
|
|
25
|
-
],
|
|
17
|
+
structuredContent: err.data,
|
|
18
|
+
content: [{ type: 'text', text }],
|
|
26
19
|
};
|
|
27
20
|
};
|
package/lib/mcp/start.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { ReclaimClient } from '@reclaimprotocol/client';
|
|
4
|
-
import {
|
|
4
|
+
import { ConfigStore } from "../config/store.js";
|
|
5
|
+
import { agentDisabled, apiToken, apiUrl, oldApiUrl, oldLogsUrl, resolveOldMode, sdkApiUrl, } from "../consts.js";
|
|
5
6
|
import { ReclaimOldClient } from "../old/client.js";
|
|
6
7
|
import { resolveInitialIdentity } from "../old/identity.js";
|
|
7
8
|
import { OLD_MODE_INSTRUCTIONS } from "../old/instructions.js";
|
|
@@ -9,11 +10,14 @@ import { ReclaimSdkClient } from "../old/sdk-client.js";
|
|
|
9
10
|
import { buildOldBackendTools } from "../old/tools/build.js";
|
|
10
11
|
import { NAME, VERSION } from "../pkg.js";
|
|
11
12
|
import { registerTools } from "./api/tools.js";
|
|
12
|
-
import { buildAgentTools, buildCredentialTools, buildLocalCredentialTools, buildVerificationTools, readSession, } from "./tools/index.js";
|
|
13
|
+
import { buildAgentTools, buildCredentialTools, buildLocalCredentialTools, buildModeTools, buildVerificationTools, readSession, } from "./tools/index.js";
|
|
13
14
|
import { BUILDER_MODE_INSTRUCTIONS } from "./instructions.js";
|
|
14
15
|
import { registerToolList } from "./server.js";
|
|
15
16
|
export async function startMcpServer() {
|
|
16
|
-
|
|
17
|
+
// Mode precedence: explicit USE_OLD_DEVTOOLS env → persisted config → old.
|
|
18
|
+
// The same store instance backs the set_devtools_mode toggle below.
|
|
19
|
+
const config = new ConfigStore();
|
|
20
|
+
const oldMode = resolveOldMode(config.read()?.mode);
|
|
17
21
|
const server = new McpServer({ name: NAME, version: VERSION }, {
|
|
18
22
|
instructions: oldMode
|
|
19
23
|
? OLD_MODE_INSTRUCTIONS
|
|
@@ -24,6 +28,9 @@ export async function startMcpServer() {
|
|
|
24
28
|
// deployments — set RECLAIM_AGENT_DISABLED=1 to register only the API
|
|
25
29
|
// surface.
|
|
26
30
|
const agentToolsEnabled = !agentDisabled();
|
|
31
|
+
// Backend-mode toggle — available in both modes and regardless of whether
|
|
32
|
+
// the agent tools are enabled, so a user can always switch either direction.
|
|
33
|
+
registerToolList(server, buildModeTools(config, oldMode));
|
|
27
34
|
if (oldMode) {
|
|
28
35
|
// OLD-devtools mode (the default): the legacy backend is still in
|
|
29
36
|
// production while builder is built. All auto-generated builder tools
|
|
@@ -118,11 +118,11 @@ export function shapeRichProofResult(result, opts) {
|
|
|
118
118
|
}
|
|
119
119
|
// Trim the LLM-visible logs — full stream goes to runLogPath.
|
|
120
120
|
if (result.attestorLogs !== undefined && result.attestorLogs.length > 0) {
|
|
121
|
-
|
|
121
|
+
const logs = result.verified
|
|
122
122
|
? warnsAndErrors(result.attestorLogs)
|
|
123
123
|
: tailLogs(result.attestorLogs);
|
|
124
|
-
if (
|
|
125
|
-
|
|
124
|
+
if (logs.length > 0) {
|
|
125
|
+
out.attestorLogs = logs;
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
if (result.sentParams !== undefined) {
|
|
@@ -1,107 +1,192 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
1
5
|
import { defineTool } from "../../server.js";
|
|
2
6
|
import { writeSession } from "./session-cache.js";
|
|
7
|
+
const SCOPE = 'openid profile email';
|
|
8
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
3
9
|
export function authenticateTool(client) {
|
|
4
10
|
return defineTool({
|
|
5
11
|
name: 'reclaim_authenticate',
|
|
6
|
-
description: 'Authorize the CLI / MCP to make authenticated API calls
|
|
7
|
-
'the
|
|
8
|
-
'
|
|
9
|
-
'
|
|
10
|
-
'
|
|
11
|
-
'with
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
'calls use a separate org secret (rorg_…, set as RECLAIM_ORG_SECRET).',
|
|
18
|
-
inputSchema: {
|
|
19
|
-
type: 'object',
|
|
20
|
-
properties: {
|
|
21
|
-
code: {
|
|
22
|
-
type: 'string',
|
|
23
|
-
pattern: '^[A-HJ-NP-Z2-9]{5}$',
|
|
24
|
-
description: '5-character code from a previous step-1 call. ' +
|
|
25
|
-
'When provided, checks whether the user has ' +
|
|
26
|
-
'confirmed that session instead of opening a new one.',
|
|
27
|
-
},
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
}, async (args) => {
|
|
31
|
-
// Step 2: check an existing session
|
|
32
|
-
if (args.code) {
|
|
33
|
-
return checkSession(client, args.code);
|
|
34
|
-
}
|
|
35
|
-
// Step 1: open a new auth-only session
|
|
36
|
-
const sessionResult = (await client.call('CreateAttachSession'));
|
|
37
|
-
const session = sessionResult.data;
|
|
38
|
-
return {
|
|
39
|
-
status: 'PENDING',
|
|
40
|
-
code: session.code,
|
|
41
|
-
attachUrl: session.attachUrl,
|
|
42
|
-
expiresAt: session.expiresAt,
|
|
43
|
-
instructions: `Open ${session.attachUrl} in your browser, sign in, and click ` +
|
|
44
|
-
'"Authorize this device". ' +
|
|
45
|
-
'Once done, call authenticate again with code: ' +
|
|
46
|
-
`"${session.code}" to confirm.`,
|
|
47
|
-
};
|
|
48
|
-
});
|
|
12
|
+
description: 'Authorize the CLI / MCP to make authenticated API calls. Opens the ' +
|
|
13
|
+
'browser to the builder sign-in page (standard OAuth 2.0 ' +
|
|
14
|
+
'authorization-code + PKCE with a localhost redirect) and waits for ' +
|
|
15
|
+
'the user to sign in and approve. On success the access token is ' +
|
|
16
|
+
'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
|
|
17
|
+
'Call with no arguments. This bearer token authenticates ' +
|
|
18
|
+
'dashboard/org-management calls (e.g. list_orgs, issuing an org ' +
|
|
19
|
+
'secret). Programmatic verification calls use a separate org secret ' +
|
|
20
|
+
'(rorg_…, set as RECLAIM_ORG_SECRET).',
|
|
21
|
+
inputSchema: { type: 'object', properties: {} },
|
|
22
|
+
}, async () => runOAuthLoopback(client));
|
|
49
23
|
}
|
|
50
|
-
async function
|
|
51
|
-
|
|
24
|
+
async function runOAuthLoopback(client) {
|
|
25
|
+
const authBase = `${client.baseUrl}/auth`;
|
|
26
|
+
const verifier = base64url(randomBytes(32));
|
|
27
|
+
const challenge = base64url(createHash('sha256').update(verifier).digest());
|
|
28
|
+
const state = base64url(randomBytes(16));
|
|
29
|
+
// Bind the callback port first: the client is registered for this exact
|
|
30
|
+
// loopback redirect, and the same client_id has to be presented again at the
|
|
31
|
+
// token endpoint (public clients authenticate by client_id alone).
|
|
32
|
+
const callback = await listenForCallback(state);
|
|
33
|
+
let token;
|
|
52
34
|
try {
|
|
53
|
-
const
|
|
54
|
-
|
|
35
|
+
const clientId = await registerClient(authBase, callback.redirectUri);
|
|
36
|
+
openBrowser(authorizeUrl(authBase, {
|
|
37
|
+
clientId,
|
|
38
|
+
redirectUri: callback.redirectUri,
|
|
39
|
+
challenge,
|
|
40
|
+
state,
|
|
55
41
|
}));
|
|
56
|
-
|
|
42
|
+
const code = await callback.code;
|
|
43
|
+
token = await exchangeCode(authBase, {
|
|
44
|
+
code,
|
|
45
|
+
verifier,
|
|
46
|
+
redirectUri: callback.redirectUri,
|
|
47
|
+
clientId,
|
|
48
|
+
});
|
|
57
49
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (status === 404) {
|
|
61
|
-
return {
|
|
62
|
-
status: 'NOT_FOUND',
|
|
63
|
-
hint: 'Session not found — it may have expired (15-min window). ' +
|
|
64
|
-
'Call authenticate without a code to start a new session.',
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
throw new Error(`Failed to check session: ${err.message}`);
|
|
50
|
+
finally {
|
|
51
|
+
callback.close();
|
|
68
52
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
53
|
+
const expiresAt = new Date(Date.now() + token.expires_in * 1000).toISOString();
|
|
54
|
+
writeSession({ token: token.access_token, expiresAt });
|
|
55
|
+
client.setToken(token.access_token);
|
|
56
|
+
return {
|
|
57
|
+
status: 'AUTHENTICATED',
|
|
58
|
+
note: 'Access token cached in ~/.reclaim/config.json and active now.',
|
|
59
|
+
expiresAt,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Bind an ephemeral loopback port and start listening for the provider's
|
|
64
|
+
* redirect. Resolves as soon as the port is known — the `code` promise settles
|
|
65
|
+
* later, when the browser hits `/callback` — so the caller can register a
|
|
66
|
+
* client for this exact redirect URI before sending the user off to authorize.
|
|
67
|
+
* The ephemeral port lets any number of CLIs run concurrently. Defined below
|
|
68
|
+
* its caller per repo convention.
|
|
69
|
+
*/
|
|
70
|
+
async function listenForCallback(state) {
|
|
71
|
+
let resolveCode;
|
|
72
|
+
let rejectCode;
|
|
73
|
+
const code = new Promise((resolve, reject) => {
|
|
74
|
+
resolveCode = resolve;
|
|
75
|
+
rejectCode = reject;
|
|
76
|
+
});
|
|
77
|
+
const server = createServer();
|
|
78
|
+
const timer = setTimeout(() => rejectCode(new Error('Timed out waiting for sign-in (5 min).')), CALLBACK_TIMEOUT_MS);
|
|
79
|
+
server.on('request', (req, res) => {
|
|
80
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
81
|
+
if (url.pathname !== '/callback') {
|
|
82
|
+
res.writeHead(404).end();
|
|
83
|
+
return;
|
|
82
84
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
85
|
+
res.writeHead(200, { 'content-type': 'text/html' });
|
|
86
|
+
res.end('<!doctype html><meta charset=utf-8><body style="font:16px '
|
|
87
|
+
+ 'system-ui;padding:3rem;text-align:center">Signed in. You can '
|
|
88
|
+
+ 'close this tab and return to your terminal.</body>');
|
|
89
|
+
const authCode = url.searchParams.get('code');
|
|
90
|
+
if (!authCode || url.searchParams.get('state') !== state) {
|
|
91
|
+
// A denied or failed authorization comes back as `error`/
|
|
92
|
+
// `error_description` (RFC 6749 §4.1.2.1) — surface it verbatim.
|
|
93
|
+
const err = url.searchParams.get('error_description')
|
|
94
|
+
?? url.searchParams.get('error');
|
|
95
|
+
rejectCode(new Error(err ?? 'Sign-in failed or was cancelled.'));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolveCode(authCode);
|
|
99
|
+
});
|
|
100
|
+
const port = await new Promise((resolve, reject) => {
|
|
101
|
+
server.once('error', reject);
|
|
102
|
+
server.listen(0, '127.0.0.1', () => {
|
|
103
|
+
const addr = server.address();
|
|
104
|
+
if (!addr || typeof addr === 'string') {
|
|
105
|
+
reject(new Error('Could not bind a local callback port.'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
resolve(addr.port);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// Past bind, a socket error fails the pending wait rather than throwing.
|
|
112
|
+
server.on('error', (err) => rejectCode(err));
|
|
97
113
|
return {
|
|
98
|
-
|
|
99
|
-
code
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
`Call authenticate again with code: "${session.code}"` +
|
|
105
|
-
' once you have done so.',
|
|
114
|
+
redirectUri: `http://127.0.0.1:${port}/callback`,
|
|
115
|
+
code,
|
|
116
|
+
close: () => {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
server.close();
|
|
119
|
+
},
|
|
106
120
|
};
|
|
107
121
|
}
|
|
122
|
+
function authorizeUrl(authBase, opts) {
|
|
123
|
+
return `${authBase}/oauth2/authorize?`
|
|
124
|
+
+ new URLSearchParams({
|
|
125
|
+
response_type: 'code',
|
|
126
|
+
client_id: opts.clientId,
|
|
127
|
+
redirect_uri: opts.redirectUri,
|
|
128
|
+
scope: SCOPE,
|
|
129
|
+
state: opts.state,
|
|
130
|
+
code_challenge: opts.challenge,
|
|
131
|
+
code_challenge_method: 'S256',
|
|
132
|
+
}).toString();
|
|
133
|
+
}
|
|
134
|
+
/** Dynamic client registration (RFC 7591) for a public PKCE client. */
|
|
135
|
+
async function registerClient(authBase, redirectUri) {
|
|
136
|
+
const res = await fetch(`${authBase}/oauth2/register`, {
|
|
137
|
+
method: 'POST',
|
|
138
|
+
headers: { 'content-type': 'application/json' },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
client_name: 'Reclaim CLI',
|
|
141
|
+
redirect_uris: [redirectUri],
|
|
142
|
+
token_endpoint_auth_method: 'none',
|
|
143
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
144
|
+
response_types: ['code'],
|
|
145
|
+
scope: SCOPE,
|
|
146
|
+
}),
|
|
147
|
+
});
|
|
148
|
+
assert(res.ok, new Error(`Client registration failed (${res.status}).`));
|
|
149
|
+
const json = (await res.json());
|
|
150
|
+
assert(json.client_id, new Error('Client registration returned no client_id.'));
|
|
151
|
+
return json.client_id;
|
|
152
|
+
}
|
|
153
|
+
async function exchangeCode(authBase, opts) {
|
|
154
|
+
const res = await fetch(`${authBase}/oauth2/token`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
157
|
+
body: new URLSearchParams({
|
|
158
|
+
grant_type: 'authorization_code',
|
|
159
|
+
code: opts.code,
|
|
160
|
+
redirect_uri: opts.redirectUri,
|
|
161
|
+
code_verifier: opts.verifier,
|
|
162
|
+
// Required even though this is a public client with no secret: it's
|
|
163
|
+
// how the token endpoint identifies the client (RFC 6749 §4.1.3).
|
|
164
|
+
client_id: opts.clientId,
|
|
165
|
+
}).toString(),
|
|
166
|
+
});
|
|
167
|
+
assert(res.ok, new Error(`Token exchange failed (${res.status}): ${await res.text()}`));
|
|
168
|
+
const json = (await res.json());
|
|
169
|
+
assert(json.access_token, new Error('Token exchange returned no access_token.'));
|
|
170
|
+
return {
|
|
171
|
+
access_token: json.access_token,
|
|
172
|
+
expires_in: json.expires_in ?? 3600,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function base64url(buf) {
|
|
176
|
+
return buf.toString('base64url');
|
|
177
|
+
}
|
|
178
|
+
function openBrowser(url) {
|
|
179
|
+
const cmd = process.platform === 'darwin'
|
|
180
|
+
? 'open'
|
|
181
|
+
: process.platform === 'win32'
|
|
182
|
+
? 'cmd'
|
|
183
|
+
: 'xdg-open';
|
|
184
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
185
|
+
try {
|
|
186
|
+
spawn(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// Non-fatal: the URL is still logged for the user to open manually.
|
|
190
|
+
}
|
|
191
|
+
process.stderr.write(`\nOpen this URL to sign in:\n${url}\n\n`);
|
|
192
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* (the eth decrypt key).
|
|
16
16
|
*/
|
|
17
17
|
import { deriveFromPrivateKey } from '@reclaimprotocol/client';
|
|
18
|
+
import assert from 'node:assert';
|
|
18
19
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
20
|
import { isAbsolute, join } from 'node:path';
|
|
20
21
|
import { privateKey, privateKeyFile } from "../../../consts.js";
|
|
@@ -25,9 +26,7 @@ export const ENV_FILE = '.env';
|
|
|
25
26
|
/** Validate + normalize a raw Ethereum private key to lowercase 0x-hex. */
|
|
26
27
|
export function normalizeEthHex(value) {
|
|
27
28
|
const v = value.trim().replace(/^['"]|['"]$/g, '');
|
|
28
|
-
|
|
29
|
-
throw new Error('not a valid Ethereum private key (expected a 32-byte / 64-hex scalar)');
|
|
30
|
-
}
|
|
29
|
+
assert(/^(0x)?[0-9a-fA-F]{64}$/.test(v), new Error('not a valid Ethereum private key (expected a 32-byte / 64-hex scalar)'));
|
|
31
30
|
return '0x' + v.replace(/^0x/, '').toLowerCase();
|
|
32
31
|
}
|
|
33
32
|
/** Read `RECLAIM_PRIVATE_KEY` from a dotenv file (simple KEY=VALUE parse). */
|
package/lib/mcp/tools/index.d.ts
CHANGED
package/lib/mcp/tools/index.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { buildModeTools } from './mode.ts';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ConfigStore } from '../../../config/store.ts';
|
|
2
|
+
import { type RegisteredTool } from '../../server.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The `set_devtools_mode` tool: persist which backend the MCP server targets
|
|
5
|
+
* (legacy `old` devtools vs the new `builder`). The choice is written to
|
|
6
|
+
* `~/.reclaim/config.json` and applied the NEXT time the server starts —
|
|
7
|
+
* the registered tool set, server instructions, and backend client are all
|
|
8
|
+
* fixed at connect time, so the switch requires a restart/reconnect. Follows
|
|
9
|
+
* the `attach_chrome` persist-through-config pattern.
|
|
10
|
+
*
|
|
11
|
+
* Registered in BOTH modes (so the user can switch either direction) and
|
|
12
|
+
* outside `buildAgentTools` (so it survives `RECLAIM_AGENT_DISABLED=1`). An
|
|
13
|
+
* explicit `USE_OLD_DEVTOOLS` env var still overrides the persisted value —
|
|
14
|
+
* see `resolveOldMode` in consts.ts.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildModeTools(config: ConfigStore, currentOldMode: boolean): RegisteredTool[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { defaultConfigPath } from "../../../config/store.js";
|
|
2
|
+
import { defineTool } from "../../server.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `set_devtools_mode` tool: persist which backend the MCP server targets
|
|
5
|
+
* (legacy `old` devtools vs the new `builder`). The choice is written to
|
|
6
|
+
* `~/.reclaim/config.json` and applied the NEXT time the server starts —
|
|
7
|
+
* the registered tool set, server instructions, and backend client are all
|
|
8
|
+
* fixed at connect time, so the switch requires a restart/reconnect. Follows
|
|
9
|
+
* the `attach_chrome` persist-through-config pattern.
|
|
10
|
+
*
|
|
11
|
+
* Registered in BOTH modes (so the user can switch either direction) and
|
|
12
|
+
* outside `buildAgentTools` (so it survives `RECLAIM_AGENT_DISABLED=1`). An
|
|
13
|
+
* explicit `USE_OLD_DEVTOOLS` env var still overrides the persisted value —
|
|
14
|
+
* see `resolveOldMode` in consts.ts.
|
|
15
|
+
*/
|
|
16
|
+
export function buildModeTools(config, currentOldMode) {
|
|
17
|
+
const current = currentOldMode ? 'old' : 'builder';
|
|
18
|
+
return [
|
|
19
|
+
defineTool({
|
|
20
|
+
name: 'set_devtools_mode',
|
|
21
|
+
description: 'Choose which backend the MCP server targets: "old" (legacy '
|
|
22
|
+
+ 'Reclaim devtools) or "builder" (the new builder backend). '
|
|
23
|
+
+ 'Persists the choice to ~/.reclaim/config.json; it takes effect '
|
|
24
|
+
+ 'the next time the MCP server starts, so restart/reconnect after '
|
|
25
|
+
+ 'switching. An explicit USE_OLD_DEVTOOLS env var overrides this.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
mode: { type: 'string', enum: ['old', 'builder'] },
|
|
30
|
+
},
|
|
31
|
+
required: ['mode'],
|
|
32
|
+
},
|
|
33
|
+
}, async (parsed) => {
|
|
34
|
+
config.write({ mode: parsed.mode });
|
|
35
|
+
const restartRequired = parsed.mode !== current;
|
|
36
|
+
return {
|
|
37
|
+
saved: true,
|
|
38
|
+
mode: parsed.mode,
|
|
39
|
+
effectiveNow: current,
|
|
40
|
+
restartRequired,
|
|
41
|
+
configPath: defaultConfigPath(),
|
|
42
|
+
message: restartRequired
|
|
43
|
+
? 'Saved. Restart / reconnect the MCP server for '
|
|
44
|
+
+ `"${parsed.mode}" mode to take effect.`
|
|
45
|
+
: `Already running in "${parsed.mode}" mode; nothing to restart.`,
|
|
46
|
+
};
|
|
47
|
+
}),
|
|
48
|
+
];
|
|
49
|
+
}
|
package/lib/old/tools/publish.js
CHANGED
|
@@ -189,15 +189,86 @@ export function createVersionFromCaptureTool(client) {
|
|
|
189
189
|
description: 'Extra requests the customInjection script is allowed to ' +
|
|
190
190
|
'fire (e.g. pagination) — add-version ONLY, the register ' +
|
|
191
191
|
'endpoint ignores this. Each item has the same shape as ' +
|
|
192
|
-
'`provider`, plus `multiple`/`required` (default true).'
|
|
192
|
+
'`provider`, plus `multiple`/`required` (default true). ' +
|
|
193
|
+
'IMPORTANT whenever a claim\'s request HAS a body: set `body` ' +
|
|
194
|
+
'here (see its own description below) — if only a small part ' +
|
|
195
|
+
'of the real body varies per user/run (e.g. one id field, not ' +
|
|
196
|
+
'the whole payload), template just that part with ' +
|
|
197
|
+
'`{{paramName}}` and keep the rest as fixed literal text; see ' +
|
|
198
|
+
'the "bodySniff" section of the customInjection field\'s guide ' +
|
|
199
|
+
'for the exact mechanics and a worked example.',
|
|
193
200
|
items: {
|
|
194
201
|
type: 'object',
|
|
195
202
|
properties: {
|
|
196
203
|
url: { type: 'string' },
|
|
197
204
|
method: { type: 'string' },
|
|
198
|
-
body: {
|
|
199
|
-
|
|
200
|
-
|
|
205
|
+
body: {
|
|
206
|
+
type: 'string',
|
|
207
|
+
description: 'Becomes `bodySniff: { enabled: true, template: <this ' +
|
|
208
|
+
'string> }` — set this whenever the request has a ' +
|
|
209
|
+
'body at all (skip only for a genuinely bodyless ' +
|
|
210
|
+
'request, typically GET). If the real body is FIXED ' +
|
|
211
|
+
'(identical for every user/run), put that literal body ' +
|
|
212
|
+
'text here verbatim. If only PART of it varies (e.g. ' +
|
|
213
|
+
'`{"request":1,"type":"user_info","id":"12345"}` where ' +
|
|
214
|
+
'only `id` differs per user), template ONLY that part: ' +
|
|
215
|
+
'`{"request":1,"type":"user_info",' +
|
|
216
|
+
'"id":"{{REQ_BODY_USER_ID}}"}` — keep everything else ' +
|
|
217
|
+
'as fixed literal text rather than templating the whole ' +
|
|
218
|
+
'body. Confirmed directly against attestor-core\'s ' +
|
|
219
|
+
'`hashProviderParams`: the hash embeds this string ' +
|
|
220
|
+
'LITERALLY, unresolved braces and all, never the real ' +
|
|
221
|
+
'substituted body, so a claim built with the SAME ' +
|
|
222
|
+
'literal template (plus a witness/extracted param per ' +
|
|
223
|
+
'placeholder, supplying the real value for ' +
|
|
224
|
+
'verification) hashes identically regardless of what ' +
|
|
225
|
+
'those real values are. This is the OPPOSITE of ' +
|
|
226
|
+
'`templateParams`\' `${var}` syntax below, which ' +
|
|
227
|
+
'genuinely IS substituted before hashing — do not ' +
|
|
228
|
+
'confuse `{{param}}` (body, never resolved) with ' +
|
|
229
|
+
'`${var}` (responseMatches/responseRedactions, resolved ' +
|
|
230
|
+
'at verify time). Before publishing, hand-verify that ' +
|
|
231
|
+
'substituting real sample values into this template ' +
|
|
232
|
+
'reproduces the real captured request body ' +
|
|
233
|
+
'byte-for-byte. Fallback if per-field templating keeps ' +
|
|
234
|
+
'mismatching (more fields vary than expected, unstable ' +
|
|
235
|
+
'key order, etc.) and you can\'t pin down every ' +
|
|
236
|
+
'variable field: template the WHOLE body as one ' +
|
|
237
|
+
'placeholder instead, `{{REQ_BODY_GRD}}` — the `GRD` ' +
|
|
238
|
+
'suffix makes it a greedy match (captures everything) ' +
|
|
239
|
+
'rather than the default non-greedy match, which is ' +
|
|
240
|
+
'needed when nothing fixed follows the placeholder. ' +
|
|
241
|
+
'This pins down nothing about body structure, so prefer ' +
|
|
242
|
+
'minimal per-field placeholders whenever you can ' +
|
|
243
|
+
'actually identify which fields vary.',
|
|
244
|
+
},
|
|
245
|
+
responseMatches: {
|
|
246
|
+
type: 'array',
|
|
247
|
+
description: 'Each item: `{ value: string, type?: "contains" | ' +
|
|
248
|
+
'"regex", invert?: boolean, isOptional?: boolean }`. ' +
|
|
249
|
+
'`value` must literally appear (or match, if `regex`) ' +
|
|
250
|
+
'somewhere in the response body for the claim to be ' +
|
|
251
|
+
'accepted; it commonly embeds one or more ' +
|
|
252
|
+
'`{{paramName}}` extraction placeholders (e.g. ' +
|
|
253
|
+
'`"name":"{{name_0}}"`) whose real value is redacted ' +
|
|
254
|
+
'out and supplied separately as a witness/extracted ' +
|
|
255
|
+
'param. Use `${var}` (not `{{var}}`) inside this string ' +
|
|
256
|
+
'only if you also declare that name in this entry\'s ' +
|
|
257
|
+
'own `templateParams` below — that\'s a DIFFERENT, ' +
|
|
258
|
+
'verify-time-substituted placeholder, resolved from the ' +
|
|
259
|
+
'submitted proof before hashing (unlike a body ' +
|
|
260
|
+
'`{{param}}`, which never resolves — see `body` above).',
|
|
261
|
+
},
|
|
262
|
+
responseRedactions: {
|
|
263
|
+
type: 'array',
|
|
264
|
+
description: 'Each item: `{ jsonPath?: string, xPath?: string, ' +
|
|
265
|
+
'regex?: string, hash?: string }`. Identifies WHICH ' +
|
|
266
|
+
'part of the response the matching `{{paramName}}` in ' +
|
|
267
|
+
'`responseMatches` actually redacts/extracts — e.g. ' +
|
|
268
|
+
'`jsonPath: "$.data.items[0].name"` for a JSON response. ' +
|
|
269
|
+
'Order/count should line up with `responseMatches`\' ' +
|
|
270
|
+
'placeholders one-to-one for a typical claim.',
|
|
271
|
+
},
|
|
201
272
|
multiple: {
|
|
202
273
|
type: 'boolean',
|
|
203
274
|
description: 'Allow matching many requests. Default true.',
|
|
@@ -102,6 +102,10 @@ export function providerToCreateVersionRequest(providers, initialUrl, opts = {})
|
|
|
102
102
|
...providerToRequestSelection(req),
|
|
103
103
|
multiple: req.multiple ?? true,
|
|
104
104
|
required: req.required ?? true,
|
|
105
|
+
templateParamsMode: req.templateParamsMode ?? 'separate',
|
|
106
|
+
...(req.templateParams?.length
|
|
107
|
+
? { templateParams: req.templateParams }
|
|
108
|
+
: {}),
|
|
105
109
|
}));
|
|
106
110
|
const script = jsUserScripts?.trim();
|
|
107
111
|
return {
|
package/lib/tools/registry.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
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
1
|
import type { JSONSchema } from 'zod/v4/core';
|
|
15
2
|
export interface ToolDefinition {
|
|
16
3
|
name: string;
|
|
@@ -34,6 +21,12 @@ export interface RegisteredTool {
|
|
|
34
21
|
* and the validation applied to inbound args cannot drift.
|
|
35
22
|
*/
|
|
36
23
|
export declare function defineTool<TArgs>(definition: ToolDefinition, handler: (args: TArgs) => Promise<unknown>): RegisteredTool;
|
|
24
|
+
/**
|
|
25
|
+
* Render a tool-handler error as the text every host shows the LLM.
|
|
26
|
+
* Boom errors carrying an RFC 9457 problem include its `detail`; anything
|
|
27
|
+
* else falls back to the plain message.
|
|
28
|
+
*/
|
|
29
|
+
export declare function errorAsToolText(err: unknown): string;
|
|
37
30
|
/**
|
|
38
31
|
* Adapt a tool to a host by WIDENING its input schema — add properties, mark
|
|
39
32
|
* some required, optionally override the description — without touching the
|