@reclaimprotocol/agent 0.1.31 → 0.1.33

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.
@@ -1,9 +1,12 @@
1
+ import { unauthorized } from '@hapi/boom'
1
2
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import type { ReclaimClient } from '@reclaimprotocol/client/api'
3
+ import { ReclaimClient } from '@reclaimprotocol/client/api'
3
4
  import type {
4
5
  IOperationArgs,
5
6
  IOperationId
6
7
  } from '@reclaimprotocol/client/openapi'
8
+ import assert from 'node:assert'
9
+ import { orgSecret } from '../../consts.ts'
7
10
  import { remapErrorAsResponse } from '../remap_errors.ts'
8
11
  import { successResponse } from '../success-response.ts'
9
12
  import { MCP_TOOLS } from './operations.gen.ts'
@@ -22,8 +25,29 @@ function toSnakeCase(name: string): string {
22
25
  .toLowerCase()
23
26
  }
24
27
 
25
- export function registerTools(server: McpServer, client: ReclaimClient) {
26
- for(const [opId, shape] of Object.entries(MCP_TOOLS)) {
28
+ /** The client for a call that presents the organization's API key. */
29
+ export type OrgClientFactory = (orgToken: string) => ReclaimClient
30
+
31
+ /**
32
+ * Register a tool per generated operation. Calls go out with the developer's
33
+ * session token, except operations only an org API key (`rorg_…`) authorizes:
34
+ * those take an `orgToken` input (else the server's RECLAIM_ORG_SECRET) and go
35
+ * out through `orgClient` with that key.
36
+ */
37
+ export function registerTools(
38
+ server: McpServer,
39
+ client: ReclaimClient,
40
+ orgClient: OrgClientFactory = (orgToken) => new ReclaimClient({
41
+ baseUrl: client.baseUrl,
42
+ token: orgToken,
43
+ }),
44
+ ) {
45
+ for(const [opId, tool] of Object.entries(MCP_TOOLS)) {
46
+ const orgTokenOnly = 'orgTokenOnly' in tool && tool.orgTokenOnly === true
47
+ const shape = {
48
+ description: tool.description,
49
+ inputSchema: tool.inputSchema,
50
+ }
27
51
  if(!isOperationId(opId)) {
28
52
  throw new Error(
29
53
  `Generated MCP operation is not in the API client: ${opId}`,
@@ -37,13 +61,21 @@ export function registerTools(server: McpServer, client: ReclaimClient) {
37
61
  server.registerTool(
38
62
  toSnakeCase(opId),
39
63
  shape,
40
- async(args: unknown) => {
64
+ async(input: unknown) => {
41
65
  try {
66
+ if(!isRecord(input)) {
67
+ throw new Error(`Invalid generated MCP arguments for ${opId}`)
68
+ }
69
+
70
+ const { orgToken, ...args } = input
42
71
  if(!isOperationArgs(opId, args)) {
43
72
  throw new Error(`Invalid generated MCP arguments for ${opId}`)
44
73
  }
45
74
 
46
- const result = await client.call(
75
+ const caller = orgTokenOnly
76
+ ? orgClient(orgTokenFor(toSnakeCase(opId), orgToken))
77
+ : client
78
+ const result = await caller.call(
47
79
  opId,
48
80
  args,
49
81
  )
@@ -79,3 +111,23 @@ function isOperationArgs<T extends IOperationId>(
79
111
  ): value is IOperationArgs<T> {
80
112
  return typeof value === 'object' && value !== null && !Array.isArray(value)
81
113
  }
114
+
115
+ function isRecord(value: unknown): value is Record<string, unknown> {
116
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
117
+ }
118
+
119
+ /** The org API key a call authenticates with: its `orgToken`, else the
120
+ * server's RECLAIM_ORG_SECRET. The session token (the developer's sign-in)
121
+ * can't stand in, since these routes accept only an org key. */
122
+ function orgTokenFor(tool: string, orgToken: unknown) {
123
+ const token = (typeof orgToken === 'string' && orgToken) || orgSecret()
124
+ assert(
125
+ token,
126
+ unauthorized(
127
+ `${tool} authenticates with the organization's API key. Pass `
128
+ + 'orgToken: one from issue_org_token, or the app\'s '
129
+ + 'RECLAIM_ORG_SECRET. Or set RECLAIM_ORG_SECRET for the MCP server.',
130
+ ),
131
+ )
132
+ return token
133
+ }
@@ -78,7 +78,7 @@ const TOPIC_SUMMARIES: Record<string, string> = {
78
78
  'discover': 'Finding providers, resolving them, and creating a verification '
79
79
  + 'session.',
80
80
  'integration': 'Set up a Builder token, callback, frontend and backend '
81
- + 'with @reclaimprotocol/client; direct `api=2` API and result '
81
+ + 'over the HTTP session API; direct `api=2` API and result '
82
82
  + 'verification.',
83
83
  'user-script': 'Authoring jsUserScripts / customInjection to drive '
84
84
  + 'login-gated pages.',
package/src/mcp/start.ts CHANGED
@@ -116,7 +116,12 @@ export async function startMcpServer() {
116
116
 
117
117
  // Add auto-generated tools with snake_case wrappers
118
118
  // for every MCP-tagged operation.
119
- registerTools(server, client)
119
+ // Org-key-only operations go out with the call's `orgToken` instead.
120
+ registerTools(server, client, (orgToken) => new ReclaimClient({
121
+ baseUrl: apiUrl(),
122
+ token: orgToken,
123
+ headers: { 'User-Agent': `${NAME}/${VERSION}` },
124
+ }))
120
125
 
121
126
  if(agentToolsEnabled) {
122
127
  agentState = buildAgentState()
@@ -17,19 +17,29 @@ const TERMINAL = new Set([
17
17
  'expired',
18
18
  ])
19
19
 
20
+ /** Events whose delivery is a signed result; the rest are plaintext event
21
+ * notifications. Mirrors `verification_result_events()`. */
22
+ const RESULT_EVENTS = new Set([
23
+ 'verification_success',
24
+ 'verification_rejected',
25
+ 'verification_error',
26
+ 'verification_cancelled',
27
+ ])
28
+
20
29
  /**
21
30
  * Resolve the org secret used to authenticate verification reads. Auth is a
22
- * single per-org bearer secret (`rorg_…`), not an eth signature — read it from
23
- * `RECLAIM_ORG_SECRET`.
31
+ * single per-org bearer secret (`rorg_…`), not an eth signature: the call's
32
+ * `orgToken`, else `RECLAIM_ORG_SECRET`.
24
33
  */
25
- function resolveOrgSecret(): string {
26
- const secret = orgSecret()?.trim()
34
+ function resolveOrgSecret(orgToken?: string): string {
35
+ const secret = orgToken?.trim() || orgSecret()?.trim()
27
36
  assert(
28
37
  secret,
29
38
  new Error(
30
- 'RECLAIM_ORG_SECRET is not set. Verification reads authenticate with ' +
31
- 'the org secret (rorg_…) — issue one from the dashboard ' +
32
- '(POST /orgs/{orgId}/token) and export it as RECLAIM_ORG_SECRET.',
39
+ 'No organization API key. Verification reads authenticate with the org '
40
+ + 'secret (rorg_…): pass orgToken (from issue_org_token, or the '
41
+ + 'app\'s RECLAIM_ORG_SECRET), or export RECLAIM_ORG_SECRET for the '
42
+ + 'MCP server.',
33
43
  ),
34
44
  )
35
45
  return secret
@@ -46,12 +56,15 @@ function resolveOrgSecret(): string {
46
56
  * Decryption, when needed, happens here.
47
57
  */
48
58
  export function verificationResultTool(): RegisteredTool {
49
- return defineTool<{ reclaimSessionId: string }>(
59
+ return defineTool<{ reclaimSessionId: string, orgToken?: string }>(
50
60
  {
51
61
  name: 'get_verification_result',
52
62
  description:
53
- 'Fetches and verifies a terminal Builder result for one session. '
54
- + 'Requires `RECLAIM_ORG_SECRET`; encrypted deliveries also require '
63
+ 'Fetches and verifies a terminal Builder result for one session, '
64
+ + 'while Builder still holds it: until its callback is delivered, or '
65
+ + '7 days undelivered. The callback is the normal way to get results. '
66
+ + 'Authenticates with the org API key: `orgToken`, else '
67
+ + '`RECLAIM_ORG_SECRET`. Encrypted deliveries also require '
55
68
  + 'the matching ETH decryption key. Returns `ready: false` until a '
56
69
  + 'payload is available. Invalid results fail verification and are not '
57
70
  + 'returned. Use `list_verification_events` to diagnose '
@@ -65,11 +78,20 @@ export function verificationResultTool(): RegisteredTool {
65
78
  description:
66
79
  'The verification session id from create_verification_session.',
67
80
  },
81
+ orgToken: {
82
+ type: 'string',
83
+ pattern: '^rorg_',
84
+ maxLength: 200,
85
+ description: 'The organization API key (`rorg_…`): from '
86
+ + '`issue_org_token`, or the app\'s RECLAIM_ORG_SECRET. '
87
+ + 'Defaults to the MCP server\'s RECLAIM_ORG_SECRET. Never echo '
88
+ + 'it back to the user.',
89
+ },
68
90
  },
69
91
  },
70
92
  },
71
93
  async(args) => {
72
- const secret = resolveOrgSecret()
94
+ const secret = resolveOrgSecret(args.orgToken)
73
95
  // Optional eth decryption key — absent ⇒ plaintext-only.
74
96
  const decryptionKey: DecryptionKey | undefined =
75
97
  await resolveDecryptionKey()
@@ -91,14 +113,34 @@ export function verificationResultTool(): RegisteredTool {
91
113
  const resultRes = await reclaim.api('GetVerificationResult', {
92
114
  params: { sessionId: args.reclaimSessionId },
93
115
  })
94
- const { items } = resultRes.data
116
+ const signed = resultRes.data.items
117
+ .filter((item) => RESULT_EVENTS.has(item.event))
118
+ if(!signed.length) {
119
+ return {
120
+ reclaimSessionId: args.reclaimSessionId,
121
+ status,
122
+ ready: false,
123
+ note: 'Session is terminal but no result is stored: none submitted '
124
+ + 'yet, or no callback subscription covers its ending event.',
125
+ }
126
+ }
95
127
 
128
+ // Builder keeps a result (which can carry proofs) only until its
129
+ // callback is delivered, or 7 days undelivered.
130
+ const items = signed.flatMap((item) => (
131
+ item.payload === null ? [] : [{ ...item, payload: item.payload }]
132
+ ))
96
133
  if(!items.length) {
97
134
  return {
98
135
  reclaimSessionId: args.reclaimSessionId,
99
136
  status,
100
137
  ready: false,
101
- note: 'Session is terminal but no result has been submitted yet.',
138
+ delivered: signed.some((item) => (
139
+ item.deliveryStatus === 'delivered'
140
+ )),
141
+ note: 'Builder no longer holds this result: it was delivered to '
142
+ + 'the callback (read it there), or went undelivered for 7 days. '
143
+ + 'See list_verification_deliveries.',
102
144
  }
103
145
  }
104
146
 
@@ -41,7 +41,9 @@ export function getProviderInfoTool(client: ReclaimSdkClient): RegisteredTool {
41
41
  'Read a PUBLISHED provider from the public production SDK backend ' +
42
42
  '(api.reclaimprotocol.org — what the verification SDK/attestor ' +
43
43
  'itself fetches at runtime), for ANY provider, no auth required. ' +
44
- 'This is the provider-read tool for old-devtools mode. \n\n' +
44
+ 'This is the provider-read tool for old-devtools mode. A ' +
45
+ 'published provider is usable as-is: don\'t re-verify it ' +
46
+ '(capture/replay/proof) unless the user asks to test it.\n\n' +
45
47
  'Returns two complementary reads for the given version (latest ' +
46
48
  'when `versionNumber` is omitted): `provider` (the FULL provider ' +
47
49
  '+ providerConfig — `customInjection`, `userAgent`, ' +