@reclaimprotocol/agent 0.1.33 → 0.1.34
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/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 +6433 -328
- package/lib/mcp/api/tools.js +21 -8
- package/lib/mcp/instructions.js +11 -11
- package/lib/mcp/server.d.ts +1 -1
- package/lib/mcp/server.js +3 -0
- 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 +3 -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/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 +10 -7
- package/lib/mcp/tools/agent/pod-proof.js +7 -11
- 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.js +3 -2
- package/lib/mcp/tools/agent/start-integration.d.ts +15 -0
- package/lib/mcp/tools/agent/start-integration.js +61 -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/result.js +10 -7
- 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 +77 -74
- package/skill/SKILL.common.md +2 -1
- package/skill/builder-mode.md +135 -138
- package/skill/start-integration.md +49 -0
- package/skill/user-script.md +64 -0
- package/skill/verification-client-integration.md +28 -22
- 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 +6477 -364
- package/src/mcp/api/tools.ts +21 -8
- package/src/mcp/instructions.ts +11 -11
- package/src/mcp/server.ts +4 -0
- 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 +5 -1
- package/src/mcp/tools/agent/capture.ts +20 -12
- 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 +10 -7
- package/src/mcp/tools/agent/pod-proof.ts +7 -11
- 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 +7 -2
- package/src/mcp/tools/agent/start-integration.ts +83 -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/result.ts +10 -7
- package/src/tools/registry.ts +14 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { fileURLToPath } from 'node:url'
|
|
3
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
4
|
+
|
|
5
|
+
interface StartIntegrationArgs {
|
|
6
|
+
goal: string
|
|
7
|
+
orgId?: string
|
|
8
|
+
providerIds?: string[]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the plan a coding agent follows to add Reclaim verification to the
|
|
13
|
+
* developer's app, from sign-in to a tested Verify button. The plan lives in
|
|
14
|
+
* `skill/start-integration.md`; this tool fills in the inputs.
|
|
15
|
+
*/
|
|
16
|
+
export function startIntegrationTool(): RegisteredTool {
|
|
17
|
+
return defineTool<StartIntegrationArgs>(
|
|
18
|
+
{
|
|
19
|
+
name: 'start_integration',
|
|
20
|
+
description:
|
|
21
|
+
'Returns a step-by-step plan for adding Reclaim verification to the '
|
|
22
|
+
+ 'current codebase: sign in, build or choose a provider, then add the '
|
|
23
|
+
+ 'session route, callback, and Verify button. Call it first when a '
|
|
24
|
+
+ 'developer wants their users to prove something, and follow the '
|
|
25
|
+
+ 'plan to the end. Changes nothing itself.',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
required: ['goal'],
|
|
29
|
+
properties: {
|
|
30
|
+
goal: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
minLength: 1,
|
|
33
|
+
maxLength: 300,
|
|
34
|
+
description:
|
|
35
|
+
'What the developer\'s users prove, for example, "they have '
|
|
36
|
+
+ 'a GitHub account with 100 followers".',
|
|
37
|
+
},
|
|
38
|
+
orgId: {
|
|
39
|
+
type: 'string',
|
|
40
|
+
format: 'uuid',
|
|
41
|
+
description: 'Organization to use. Omit to choose from the list.',
|
|
42
|
+
},
|
|
43
|
+
providerIds: {
|
|
44
|
+
type: 'array',
|
|
45
|
+
maxItems: 10,
|
|
46
|
+
items: { type: 'string' },
|
|
47
|
+
description:
|
|
48
|
+
'Existing provider IDs to use. Omit to build a new provider.',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
53
|
+
},
|
|
54
|
+
async(args) => renderStartIntegration(args),
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Fill the plan template. Exported for tests. */
|
|
59
|
+
export function renderStartIntegration(args: StartIntegrationArgs): string {
|
|
60
|
+
const providerIds = args.providerIds?.length
|
|
61
|
+
? args.providerIds.map((id) => `\`${id}\``).join(', ')
|
|
62
|
+
: undefined
|
|
63
|
+
const values: Record<string, string | undefined> = {
|
|
64
|
+
goal: args.goal,
|
|
65
|
+
orgId: args.orgId,
|
|
66
|
+
providerIds,
|
|
67
|
+
}
|
|
68
|
+
const template = readFileSync(
|
|
69
|
+
fileURLToPath(
|
|
70
|
+
new URL('../../../../skill/start-integration.md', import.meta.url),
|
|
71
|
+
),
|
|
72
|
+
'utf8',
|
|
73
|
+
)
|
|
74
|
+
return template
|
|
75
|
+
// Keep a conditional block only when its input was given (or not given).
|
|
76
|
+
.replace(
|
|
77
|
+
/<!-- (if|ifnot):(\w+) -->\n([\s\S]*?)<!-- endif -->\n/g,
|
|
78
|
+
(_, kind: string, name: string, body: string) => {
|
|
79
|
+
return (kind === 'if') === Boolean(values[name]) ? body : ''
|
|
80
|
+
},
|
|
81
|
+
)
|
|
82
|
+
.replace(/\{\{(\w+)\}\}/g, (_, name: string) => values[name] ?? '')
|
|
83
|
+
}
|
|
@@ -25,18 +25,16 @@ export function authenticateTool(
|
|
|
25
25
|
+ 'get_me_providers; those need the separate old-devtools login, '
|
|
26
26
|
+ 'reclaim_authenticate. Check both with get_devtools_mode. '
|
|
27
27
|
: '')
|
|
28
|
-
+ '
|
|
29
|
-
+ '
|
|
30
|
-
+ '
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'issuing an org secret). Programmatic verification calls use a ' +
|
|
37
|
-
'separate org secret ' +
|
|
38
|
-
'(rorg_…, set as RECLAIM_ORG_SECRET).',
|
|
28
|
+
+ 'Signs the developer in to Builder. Opens the Builder sign-in page '
|
|
29
|
+
+ 'in their browser (OAuth 2.0 authorization code with PKCE, '
|
|
30
|
+
+ 'redirecting to a localhost port this server opens) and waits for '
|
|
31
|
+
+ 'them to approve. Saves the access token in ~/.reclaim/config.json '
|
|
32
|
+
+ 'with mode 0600 and uses it for this session; the token is never '
|
|
33
|
+
+ 'returned. The token authorizes organization-management tools such '
|
|
34
|
+
+ 'as `list_orgs`. Programmatic verification tools use a separate '
|
|
35
|
+
+ 'organization key; see `issue_org_token`. Call with no arguments.',
|
|
39
36
|
inputSchema: { type: 'object', properties: {} },
|
|
37
|
+
annotations: { openWorldHint: true, idempotentHint: true },
|
|
40
38
|
},
|
|
41
39
|
async() => runOAuthLoopback(client),
|
|
42
40
|
)
|
|
@@ -3,6 +3,7 @@ import type { RegisteredTool } from '../../server.ts'
|
|
|
3
3
|
import { authenticateTool } from './authenticate.ts'
|
|
4
4
|
import { importTool } from './import.ts'
|
|
5
5
|
import { issueTool } from './issue.ts'
|
|
6
|
+
import { issueOrgTokenTool } from './issue-org-token.ts'
|
|
6
7
|
import { resolveOwnerKeyTool } from './resolve-owner-key.ts'
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -27,6 +28,7 @@ export function buildLocalCredentialTools(): RegisteredTool[] {
|
|
|
27
28
|
export function buildCredentialTools(client: ReclaimClient): RegisteredTool[] {
|
|
28
29
|
return [
|
|
29
30
|
authenticateTool(client),
|
|
31
|
+
issueOrgTokenTool(client),
|
|
30
32
|
...buildLocalCredentialTools(),
|
|
31
33
|
]
|
|
32
34
|
}
|
|
@@ -16,8 +16,20 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { deriveFromPrivateKey } from '@reclaimprotocol/client'
|
|
18
18
|
import assert from 'node:assert'
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
chmodSync,
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readFileSync,
|
|
24
|
+
writeFileSync,
|
|
25
|
+
} from 'node:fs'
|
|
26
|
+
import {
|
|
27
|
+
dirname,
|
|
28
|
+
isAbsolute,
|
|
29
|
+
join,
|
|
30
|
+
relative,
|
|
31
|
+
resolve
|
|
32
|
+
} from 'node:path'
|
|
21
33
|
import { privateKey, privateKeyFile } from '../../../consts.ts'
|
|
22
34
|
import { findProjectRoot } from '../../../paths.ts'
|
|
23
35
|
|
|
@@ -124,7 +136,7 @@ export function resolveEthKey(
|
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
/** Append `entry` to the project `.gitignore` if not already present. */
|
|
127
|
-
function ensureGitignored(projectDir: string, entry: string) {
|
|
139
|
+
export function ensureGitignored(projectDir: string, entry: string) {
|
|
128
140
|
const path = join(projectDir, '.gitignore')
|
|
129
141
|
const current = existsSync(path) ? readFileSync(path, 'utf8') : ''
|
|
130
142
|
if(current.split('\n').some((l) => l.trim() === entry)) {
|
|
@@ -136,29 +148,47 @@ function ensureGitignored(projectDir: string, entry: string) {
|
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
* hex). Returns the derived address and the `.env` path.
|
|
151
|
+
* Set `name=value` in a dotenv file, replacing an existing line or appending
|
|
152
|
+
* one. Writes with mode 0600 and adds the file to the project `.gitignore`.
|
|
153
|
+
* `envFile` is relative to `projectDir`.
|
|
143
154
|
*/
|
|
144
|
-
export function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
155
|
+
export function upsertEnvVar(
|
|
156
|
+
projectDir: string,
|
|
157
|
+
envFile: string,
|
|
158
|
+
name: string,
|
|
159
|
+
value: string,
|
|
160
|
+
): string {
|
|
161
|
+
const envPath = resolve(projectDir, envFile)
|
|
162
|
+
const line = `${name}=${value}`
|
|
151
163
|
const existing = existsSync(envPath) ? readFileSync(envPath, 'utf8') : ''
|
|
152
|
-
const re = new RegExp(`^\\s*${
|
|
164
|
+
const re = new RegExp(`^\\s*${name}\\s*=.*$`, 'm')
|
|
153
165
|
let next: string
|
|
154
166
|
if(re.test(existing)) {
|
|
155
|
-
|
|
167
|
+
// A function replacer keeps `$` sequences in the value literal.
|
|
168
|
+
next = existing.replace(re, () => line)
|
|
156
169
|
} else {
|
|
157
170
|
const prefix = existing && !existing.endsWith('\n') ? '\n' : ''
|
|
158
171
|
next = existing + prefix + line + '\n'
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
mkdirSync(dirname(envPath), { recursive: true })
|
|
161
175
|
writeFileSync(envPath, next, { mode: 0o600 })
|
|
162
|
-
|
|
163
|
-
|
|
176
|
+
chmodSync(envPath, 0o600)
|
|
177
|
+
ensureGitignored(projectDir, relative(projectDir, envPath))
|
|
178
|
+
return envPath
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Persist a raw eth private key to the project `.env` as
|
|
183
|
+
* `RECLAIM_PRIVATE_KEY=0x…` (upsert; mode 0600; `.env` added to `.gitignore`).
|
|
184
|
+
* Used by `issue_credentials` (generate) and `import_credentials` (supplied
|
|
185
|
+
* hex). Returns the derived address and the `.env` path.
|
|
186
|
+
*/
|
|
187
|
+
export function writeEthKey(
|
|
188
|
+
privateKeyHex: string,
|
|
189
|
+
projectDir = findProjectRoot(process.cwd()),
|
|
190
|
+
): { path: string, address: string } {
|
|
191
|
+
const hex = normalizeEthHex(privateKeyHex)
|
|
192
|
+
const path = upsertEnvVar(projectDir, ENV_FILE, ENV_VAR, hex)
|
|
193
|
+
return { path, address: deriveFromPrivateKey(hex).address }
|
|
164
194
|
}
|
|
@@ -5,68 +5,53 @@ import { writeEthKey } from './eth-key.ts'
|
|
|
5
5
|
|
|
6
6
|
export function importTool(): RegisteredTool {
|
|
7
7
|
return defineTool<{
|
|
8
|
-
|
|
9
|
-
keyFile?: string
|
|
8
|
+
keyFile: string
|
|
10
9
|
projectDir?: string
|
|
11
10
|
}>(
|
|
12
11
|
{
|
|
13
12
|
name: 'import_credentials',
|
|
14
13
|
description:
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'The
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'unrelated to org auth or encryption).',
|
|
14
|
+
'Imports an eth proof-owner key from a file the developer already '
|
|
15
|
+
+ 'holds. Reads the raw 0x-hex key from `keyFile` and writes it to '
|
|
16
|
+
+ 'the project `.env` as RECLAIM_PRIVATE_KEY (mode 0600; `.env` '
|
|
17
|
+
+ 'added to .gitignore). Returns the derived address and the `.env` '
|
|
18
|
+
+ 'path; never the key. The key is a local signing key only: it is '
|
|
19
|
+
+ 'not registered with any server and is unrelated to org auth or '
|
|
20
|
+
+ 'encryption. To create a new key instead, use `issue_credentials`.',
|
|
23
21
|
inputSchema: {
|
|
24
22
|
type: 'object',
|
|
23
|
+
required: ['keyFile'],
|
|
25
24
|
properties: {
|
|
26
|
-
privateKey: {
|
|
27
|
-
type: 'string',
|
|
28
|
-
pattern: '^0x?[0-9a-fA-F]{64}$',
|
|
29
|
-
description:
|
|
30
|
-
'Raw Ethereum private key, 0x-prefixed hex (64 hex chars).',
|
|
31
|
-
},
|
|
32
25
|
keyFile: {
|
|
33
26
|
type: 'string',
|
|
34
27
|
description:
|
|
35
|
-
'Path to a file containing the raw 0x-hex private key '
|
|
36
|
-
'(absolute or
|
|
28
|
+
'Path to a file containing the raw 0x-hex private key '
|
|
29
|
+
+ '(absolute or relative to the working directory).',
|
|
37
30
|
},
|
|
38
31
|
projectDir: {
|
|
39
32
|
type: 'string',
|
|
40
33
|
description:
|
|
41
|
-
'Project directory whose `.env` the key is written to. '
|
|
42
|
-
'Defaults to the closest ancestor of cwd containing .git or '
|
|
43
|
-
'package.json.',
|
|
34
|
+
'Project directory whose `.env` the key is written to. '
|
|
35
|
+
+ 'Defaults to the closest ancestor of cwd containing .git or '
|
|
36
|
+
+ 'package.json.',
|
|
44
37
|
},
|
|
45
38
|
},
|
|
46
39
|
},
|
|
40
|
+
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
47
41
|
},
|
|
48
42
|
async(args) => {
|
|
49
|
-
if(!args.privateKey && !args.keyFile) {
|
|
50
|
-
throw new Error('Provide either `privateKey` (0x-hex) or `keyFile`.')
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
if(args.privateKey && args.keyFile) {
|
|
54
|
-
throw new Error('Provide only one of `privateKey` or `keyFile`.')
|
|
55
|
-
}
|
|
56
|
-
|
|
57
43
|
const projectDir = findProjectRoot(args.projectDir || process.cwd())
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const { path, address } = writeEthKey(privateKeyHex, projectDir)
|
|
44
|
+
const { path, address } = writeEthKey(
|
|
45
|
+
readFileSync(args.keyFile, 'utf8'), projectDir,
|
|
46
|
+
)
|
|
62
47
|
|
|
63
48
|
return {
|
|
64
49
|
address,
|
|
65
50
|
path,
|
|
66
51
|
note:
|
|
67
|
-
'Eth proof-owner key written to .env as RECLAIM_PRIVATE_KEY '
|
|
68
|
-
'(mode 0600). resolve_owner_key resolves it from .env. Keep .env '
|
|
69
|
-
'secret
|
|
52
|
+
'Eth proof-owner key written to .env as RECLAIM_PRIVATE_KEY '
|
|
53
|
+
+ '(mode 0600). resolve_owner_key resolves it from .env. Keep .env '
|
|
54
|
+
+ 'secret: it has been added to .gitignore.',
|
|
70
55
|
}
|
|
71
56
|
},
|
|
72
57
|
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { ReclaimClient } from '@reclaimprotocol/client/api'
|
|
2
|
+
import assert from 'node:assert'
|
|
3
|
+
import { isAbsolute, relative, resolve } from 'node:path'
|
|
4
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
5
|
+
import { upsertEnvVar } from './eth-key.ts'
|
|
6
|
+
import { saveOrgKey } from './org-key-store.ts'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Issues an organization API key and stores it on disk, so the key never
|
|
10
|
+
* passes through the agent's transcript. Replaces the generated
|
|
11
|
+
* `issue_org_token` operation, which returned the key in its response.
|
|
12
|
+
*/
|
|
13
|
+
export function issueOrgTokenTool(client: ReclaimClient): RegisteredTool {
|
|
14
|
+
return defineTool<{
|
|
15
|
+
orgId: string
|
|
16
|
+
projectDir: string
|
|
17
|
+
envFile?: string
|
|
18
|
+
envVar?: string
|
|
19
|
+
}>(
|
|
20
|
+
{
|
|
21
|
+
name: 'issue_org_token',
|
|
22
|
+
description:
|
|
23
|
+
'Issues an organization API key (owner only) and writes it to a '
|
|
24
|
+
+ 'server env file in the project, so the app can read it at runtime. '
|
|
25
|
+
+ 'Also saves it on this machine for the org-key tools. Returns the '
|
|
26
|
+
+ 'org, file, variable, and last four characters; never the key. '
|
|
27
|
+
+ 'Replaces an existing variable of the same name in the file. '
|
|
28
|
+
+ 'Adds the file to .gitignore. Use when the app needs a key, or '
|
|
29
|
+
+ 'before `create_verification_session`.',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
required: ['orgId', 'projectDir'],
|
|
33
|
+
properties: {
|
|
34
|
+
orgId: { type: 'string', format: 'uuid' },
|
|
35
|
+
projectDir: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Absolute path of the app\'s project directory.',
|
|
38
|
+
},
|
|
39
|
+
envFile: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description:
|
|
42
|
+
'Env file to write, relative to `projectDir`. Default `.env`.',
|
|
43
|
+
},
|
|
44
|
+
envVar: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
|
|
47
|
+
description:
|
|
48
|
+
'Variable name to set. Default `RECLAIM_ORG_SECRET`.',
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
annotations: { openWorldHint: true, destructiveHint: false },
|
|
53
|
+
},
|
|
54
|
+
async(args) => {
|
|
55
|
+
assert(
|
|
56
|
+
isAbsolute(args.projectDir),
|
|
57
|
+
new Error('`projectDir` must be an absolute path.'),
|
|
58
|
+
)
|
|
59
|
+
const envFile = args.envFile || '.env'
|
|
60
|
+
const envVar = args.envVar || 'RECLAIM_ORG_SECRET'
|
|
61
|
+
const inside = relative(
|
|
62
|
+
args.projectDir, resolve(args.projectDir, envFile),
|
|
63
|
+
)
|
|
64
|
+
assert(
|
|
65
|
+
inside && !inside.startsWith('..') && !isAbsolute(inside),
|
|
66
|
+
new Error('`envFile` must be inside `projectDir`.'),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const { data } = await client.call('IssueOrgToken', {
|
|
70
|
+
params: { orgId: args.orgId },
|
|
71
|
+
})
|
|
72
|
+
const path = upsertEnvVar(
|
|
73
|
+
args.projectDir, envFile, envVar, data.orgSecret,
|
|
74
|
+
)
|
|
75
|
+
saveOrgKey(args.orgId, data.orgSecret)
|
|
76
|
+
return {
|
|
77
|
+
orgId: args.orgId,
|
|
78
|
+
envFile: path,
|
|
79
|
+
envVar,
|
|
80
|
+
last4: data.last4,
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -8,15 +8,14 @@ export function issueTool(): RegisteredTool {
|
|
|
8
8
|
{
|
|
9
9
|
name: 'issue_credentials',
|
|
10
10
|
description:
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'registered with any server and is unrelated to org auth
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'returned in the response.',
|
|
11
|
+
'Generates a new secp256k1 eth proof-owner key and writes the '
|
|
12
|
+
+ 'private key to the project `.env` as RECLAIM_PRIVATE_KEY (mode '
|
|
13
|
+
+ '0600; `.env` added to .gitignore). Returns the address and the '
|
|
14
|
+
+ '`.env` path; never the key. The key is a local signing key only: '
|
|
15
|
+
+ 'it is not registered with any server and is unrelated to org auth '
|
|
16
|
+
+ 'or encryption. Confirm with the developer before replacing an '
|
|
17
|
+
+ 'existing key. `run_proof` then picks it up through '
|
|
18
|
+
+ '`resolve_owner_key`.',
|
|
20
19
|
inputSchema: {
|
|
21
20
|
type: 'object',
|
|
22
21
|
properties: {
|
|
@@ -29,6 +28,7 @@ export function issueTool(): RegisteredTool {
|
|
|
29
28
|
},
|
|
30
29
|
},
|
|
31
30
|
},
|
|
31
|
+
annotations: { destructiveHint: false },
|
|
32
32
|
},
|
|
33
33
|
async(args) => {
|
|
34
34
|
const projectDir = findProjectRoot(args.projectDir || process.cwd())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ConfigStore } from '../../../config/store.ts'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Save an organization API key (`rorg_…`) in the local config (mode 0600), so
|
|
5
|
+
* the org-key tools can use it without the key passing through a transcript.
|
|
6
|
+
*/
|
|
7
|
+
export function saveOrgKey(orgId: string, secret: string) {
|
|
8
|
+
const saved = new ConfigStore().read()?.org_keys
|
|
9
|
+
new ConfigStore().write({
|
|
10
|
+
org_keys: {
|
|
11
|
+
byOrg: { ...saved?.byOrg, [orgId]: secret },
|
|
12
|
+
recent: orgId,
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The saved key for `orgId`, or the most recently saved key when no org is
|
|
19
|
+
* given. Returns `undefined` when none is saved.
|
|
20
|
+
*/
|
|
21
|
+
export function savedOrgKey(orgId?: string): string | undefined {
|
|
22
|
+
const saved = new ConfigStore().read()?.org_keys
|
|
23
|
+
const id = orgId || saved?.recent
|
|
24
|
+
return id ? saved?.byOrg[id] : undefined
|
|
25
|
+
}
|
|
@@ -19,18 +19,16 @@ export function resolveOwnerKeyTool(): RegisteredTool {
|
|
|
19
19
|
{
|
|
20
20
|
name: 'resolve_owner_key',
|
|
21
21
|
description:
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'(issue_credentials) or import one they already hold ' +
|
|
31
|
-
'(import_credentials, accepts 0x-hex or a key-file path), and call ' +
|
|
32
|
-
'resolve_owner_key again. Never exposes the private key.',
|
|
22
|
+
'Finds the eth proof-owner key `run_proof` signs with. Call it '
|
|
23
|
+
+ 'before `run_proof`. Checks RECLAIM_PRIVATE_KEY in the MCP env, a '
|
|
24
|
+
+ 'RECLAIM_PRIVATE_KEY line in the project .env, then the file at '
|
|
25
|
+
+ 'RECLAIM_PRIVATE_KEY_FILE. Returns { ready: true, address, source '
|
|
26
|
+
+ '}: pass `address` to `run_proof` as `ownerAddress`. Returns { '
|
|
27
|
+
+ 'ready: false } when none exists: ask the developer to create one '
|
|
28
|
+
+ 'with `issue_credentials` or import one with `import_credentials` '
|
|
29
|
+
+ '(a key file), then call it again. Never returns the key.',
|
|
33
30
|
inputSchema: { type: 'object', properties: {} },
|
|
31
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
34
32
|
},
|
|
35
33
|
async() => {
|
|
36
34
|
const projectDir = findProjectRoot(process.cwd())
|
|
@@ -51,7 +49,7 @@ export function resolveOwnerKeyTool(): RegisteredTool {
|
|
|
51
49
|
`env, no RECLAIM_PRIVATE_KEY in ${join(projectDir, ENV_FILE)}, and ` +
|
|
52
50
|
'no RECLAIM_PRIVATE_KEY_FILE. Ask the dev to generate one via ' +
|
|
53
51
|
'issue_credentials, or import one they already hold via ' +
|
|
54
|
-
'import_credentials (
|
|
52
|
+
'import_credentials (a key file). Then call ' +
|
|
55
53
|
'resolve_owner_key again and pass its address to run_proof as ' +
|
|
56
54
|
'ownerAddress.',
|
|
57
55
|
}
|
|
@@ -53,6 +53,7 @@ export function buildModeTools(
|
|
|
53
53
|
},
|
|
54
54
|
required: ['mode'],
|
|
55
55
|
},
|
|
56
|
+
annotations: { openWorldHint: false, idempotentHint: true },
|
|
56
57
|
},
|
|
57
58
|
async(parsed) => {
|
|
58
59
|
config.write({ mode: parsed.mode })
|
|
@@ -84,6 +85,7 @@ export function buildModeTools(
|
|
|
84
85
|
+ 'the staleness with an error naming the tool to re-run. Which '
|
|
85
86
|
+ 'login authorizes what: how_it_works({ topic: "auth" }).',
|
|
86
87
|
inputSchema: { type: 'object', properties: {} },
|
|
88
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
87
89
|
},
|
|
88
90
|
async() => {
|
|
89
91
|
const persisted = config.read()?.mode
|
|
@@ -7,6 +7,7 @@ import assert from 'node:assert'
|
|
|
7
7
|
import { apiUrl, orgSecret } from '../../../consts.ts'
|
|
8
8
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
9
9
|
import { resolveDecryptionKey } from '../authenticate/decrypt-key.ts'
|
|
10
|
+
import { savedOrgKey } from '../authenticate/org-key-store.ts'
|
|
10
11
|
|
|
11
12
|
/** Statuses that can carry a Builder-managed terminal result. */
|
|
12
13
|
const TERMINAL = new Set([
|
|
@@ -32,14 +33,13 @@ const RESULT_EVENTS = new Set([
|
|
|
32
33
|
* `orgToken`, else `RECLAIM_ORG_SECRET`.
|
|
33
34
|
*/
|
|
34
35
|
function resolveOrgSecret(orgToken?: string): string {
|
|
35
|
-
const secret = orgToken?.trim() || orgSecret()?.trim()
|
|
36
|
+
const secret = orgToken?.trim() || orgSecret()?.trim() || savedOrgKey()
|
|
36
37
|
assert(
|
|
37
38
|
secret,
|
|
38
39
|
new Error(
|
|
39
40
|
'No organization API key. Verification reads authenticate with the org '
|
|
40
|
-
+ 'secret (rorg_…):
|
|
41
|
-
+ '
|
|
42
|
-
+ 'MCP server.',
|
|
41
|
+
+ 'secret (rorg_…): run issue_org_token to save a key, pass orgToken, '
|
|
42
|
+
+ 'or export RECLAIM_ORG_SECRET for the MCP server.',
|
|
43
43
|
),
|
|
44
44
|
)
|
|
45
45
|
return secret
|
|
@@ -64,7 +64,8 @@ export function verificationResultTool(): RegisteredTool {
|
|
|
64
64
|
+ 'while Builder still holds it: until its callback is delivered, or '
|
|
65
65
|
+ '7 days undelivered. The callback is the normal way to get results. '
|
|
66
66
|
+ 'Authenticates with the org API key: `orgToken`, else '
|
|
67
|
-
+ '`RECLAIM_ORG_SECRET
|
|
67
|
+
+ '`RECLAIM_ORG_SECRET`, else the key saved by `issue_org_token`. '
|
|
68
|
+
+ 'Encrypted deliveries also require '
|
|
68
69
|
+ 'the matching ETH decryption key. Returns `ready: false` until a '
|
|
69
70
|
+ 'payload is available. Invalid results fail verification and are not '
|
|
70
71
|
+ 'returned. Use `list_verification_events` to diagnose '
|
|
@@ -84,11 +85,13 @@ export function verificationResultTool(): RegisteredTool {
|
|
|
84
85
|
maxLength: 200,
|
|
85
86
|
description: 'The organization API key (`rorg_…`): from '
|
|
86
87
|
+ '`issue_org_token`, or the app\'s RECLAIM_ORG_SECRET. '
|
|
87
|
-
+ 'Defaults to the MCP server\'s RECLAIM_ORG_SECRET
|
|
88
|
-
+ 'it back to the
|
|
88
|
+
+ 'Defaults to the MCP server\'s RECLAIM_ORG_SECRET, then the '
|
|
89
|
+
+ 'key saved by `issue_org_token`. Never echo it back to the '
|
|
90
|
+
+ 'user.',
|
|
89
91
|
},
|
|
90
92
|
},
|
|
91
93
|
},
|
|
94
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
92
95
|
},
|
|
93
96
|
async(args) => {
|
|
94
97
|
const secret = resolveOrgSecret(args.orgToken)
|
package/src/tools/registry.ts
CHANGED
|
@@ -16,10 +16,23 @@ import { isProblemDetails, isProblemError } from '@reclaimprotocol/client/api'
|
|
|
16
16
|
import type { JSONSchema } from 'zod/v4/core'
|
|
17
17
|
import { toZod } from '../mcp/api/to-zod.ts'
|
|
18
18
|
|
|
19
|
+
/** MCP tool hints that let a client decide when to ask for approval. */
|
|
20
|
+
export interface ToolAnnotations {
|
|
21
|
+
/** The tool never changes state. */
|
|
22
|
+
readOnlyHint?: boolean
|
|
23
|
+
/** The tool can delete or overwrite state. */
|
|
24
|
+
destructiveHint?: boolean
|
|
25
|
+
/** Repeating a call with the same input has no further effect. */
|
|
26
|
+
idempotentHint?: boolean
|
|
27
|
+
/** The tool reaches systems outside this machine. */
|
|
28
|
+
openWorldHint?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
export interface ToolDefinition {
|
|
20
32
|
name: string
|
|
21
33
|
description: string
|
|
22
34
|
inputSchema: JSONSchema.ObjectSchema
|
|
35
|
+
annotations?: ToolAnnotations
|
|
23
36
|
}
|
|
24
37
|
|
|
25
38
|
export interface RegisteredTool {
|
|
@@ -108,6 +121,7 @@ export function extendTool(
|
|
|
108
121
|
name: base.name,
|
|
109
122
|
description: extra.description ?? base.description,
|
|
110
123
|
inputSchema,
|
|
124
|
+
...(base.annotations ? { annotations: base.annotations } : {}),
|
|
111
125
|
},
|
|
112
126
|
handler: tool.handler,
|
|
113
127
|
}
|