@reclaimprotocol/agent 0.1.42 → 0.1.44
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/mcp/api/operations.gen.d.ts +17 -0
- package/lib/mcp/api/operations.gen.js +151 -1
- package/lib/mcp/instructions.js +4 -0
- package/lib/mcp/tools/agent/publish.js +8 -1
- package/lib/mcp/tools/agent/start-integration.d.ts +1 -0
- package/lib/mcp/tools/agent/start-integration.js +20 -3
- package/lib/mcp/tools/verification/build.js +2 -0
- package/lib/mcp/tools/verification/wait-for-ai-session.d.ts +46 -0
- package/lib/mcp/tools/verification/wait-for-ai-session.js +228 -0
- package/package.json +2 -2
- package/skill/SKILL.builder.md +10 -1
- package/skill/ai-sessions.md +87 -0
- package/skill/builder-mode.md +3 -3
- package/skill/interception.md +36 -1
- package/skill/start-ai-session-integration.md +58 -0
- package/skill/start-integration.md +2 -0
- package/skill/verification-client-integration.md +69 -0
- package/src/mcp/api/operations.gen.ts +157 -1
- package/src/mcp/instructions.ts +4 -0
- package/src/mcp/tools/agent/publish.ts +8 -1
- package/src/mcp/tools/agent/start-integration.ts +22 -3
- package/src/mcp/tools/verification/build.ts +2 -0
- package/src/mcp/tools/verification/wait-for-ai-session.ts +304 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { isProblemError, ReclaimClient } from '@reclaimprotocol/client/api'
|
|
2
|
+
import { apiUrl } from '../../../consts.ts'
|
|
3
|
+
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
4
|
+
import { resolveOrgSecret } from './result.ts'
|
|
5
|
+
|
|
6
|
+
const POLL_MS = 2_000
|
|
7
|
+
const DEFAULT_TIMEOUT_S = 120
|
|
8
|
+
const MAX_TIMEOUT_S = 600
|
|
9
|
+
const MAX_EVENTS = 5
|
|
10
|
+
|
|
11
|
+
type LookupStatus = 'resolving' | 'ready' | 'started' | 'failed'
|
|
12
|
+
|
|
13
|
+
type Lookup = {
|
|
14
|
+
status: LookupStatus
|
|
15
|
+
error?: { title?: string, status?: number, detail?: string }
|
|
16
|
+
dataPoints?: { name: string, isOptional: boolean }[]
|
|
17
|
+
candidates?: { loginUrl: string, source: string, confidence: number }[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SessionState = {
|
|
21
|
+
status: string
|
|
22
|
+
callbackDeliveryStatus?: string
|
|
23
|
+
verificationUrl?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type SessionEvent = {
|
|
27
|
+
event: string
|
|
28
|
+
eventData?: Record<string, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The three reads the tool needs. Injectable for tests. */
|
|
32
|
+
export type AiSessionApi = {
|
|
33
|
+
getLookup(sessionId: string): Promise<Lookup>
|
|
34
|
+
getSession(sessionId: string): Promise<SessionState>
|
|
35
|
+
listEvents(sessionId: string): Promise<SessionEvent[]>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type Deps = {
|
|
39
|
+
createApi?: (orgToken?: string) => AiSessionApi
|
|
40
|
+
sleep?: (ms: number) => Promise<void>
|
|
41
|
+
now?: () => number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const FALLBACK_HINT = 'Retry once with adjusted `dataPoints` or `loginUrls`. '
|
|
45
|
+
+ 'If that fails too, fall back to the provider route: call '
|
|
46
|
+
+ '`start_integration` with `route: \'provider\'`.'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `wait_for_ai_session` — polls an AI verification session (lookup, session
|
|
50
|
+
* status, and recent events) until something notable happens, and returns a
|
|
51
|
+
* compact, value-free summary with a `next` step and a fallback verdict.
|
|
52
|
+
*/
|
|
53
|
+
export function waitForAiSessionTool(deps: Deps = {}): RegisteredTool {
|
|
54
|
+
const createApi = deps.createApi ?? defaultApi
|
|
55
|
+
const sleep = deps.sleep ?? defaultSleep
|
|
56
|
+
const now = deps.now ?? Date.now
|
|
57
|
+
|
|
58
|
+
return defineTool<{
|
|
59
|
+
sessionId: string
|
|
60
|
+
timeoutSeconds?: number
|
|
61
|
+
untilTerminal?: boolean
|
|
62
|
+
orgToken?: string
|
|
63
|
+
}>(
|
|
64
|
+
{
|
|
65
|
+
name: 'wait_for_ai_session',
|
|
66
|
+
description:
|
|
67
|
+
'Waits for an AI verification session and returns a summary. '
|
|
68
|
+
+ 'Polls every 2 seconds until the lookup is ready or failed, or '
|
|
69
|
+
+ 'the session ends, or the timeout passes. Set `untilTerminal` to '
|
|
70
|
+
+ 'keep waiting until the session ends, for example after you share '
|
|
71
|
+
+ 'the verification URL. The result has the lookup state, the session '
|
|
72
|
+
+ 'status, recent event types, and `next`: `share_url`, '
|
|
73
|
+
+ '`waiting_for_user`, `waiting_for_lookup`, `done`, or `none`. '
|
|
74
|
+
+ 'When `fallback.recommended` is true, follow its `reason`. '
|
|
75
|
+
+ 'Authenticates with the org API key: `orgToken`, else '
|
|
76
|
+
+ '`RECLAIM_ORG_SECRET`, else the key saved by `issue_org_token`.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
required: ['sessionId'],
|
|
80
|
+
properties: {
|
|
81
|
+
sessionId: {
|
|
82
|
+
type: 'string',
|
|
83
|
+
description:
|
|
84
|
+
'The session ID from `create_ai_verification_session`.',
|
|
85
|
+
},
|
|
86
|
+
timeoutSeconds: {
|
|
87
|
+
type: 'integer',
|
|
88
|
+
minimum: 1,
|
|
89
|
+
maximum: MAX_TIMEOUT_S,
|
|
90
|
+
description: `How long to wait. Default ${DEFAULT_TIMEOUT_S}, `
|
|
91
|
+
+ `maximum ${MAX_TIMEOUT_S}.`,
|
|
92
|
+
},
|
|
93
|
+
untilTerminal: {
|
|
94
|
+
type: 'boolean',
|
|
95
|
+
description: 'If true, return only when the session ends or '
|
|
96
|
+
+ 'the lookup fails, not when the lookup is ready.',
|
|
97
|
+
},
|
|
98
|
+
orgToken: {
|
|
99
|
+
type: 'string',
|
|
100
|
+
pattern: '^rorg_',
|
|
101
|
+
maxLength: 200,
|
|
102
|
+
description: 'The organization API key (`rorg_…`). Defaults to '
|
|
103
|
+
+ 'the MCP server\'s RECLAIM_ORG_SECRET, then the key saved by '
|
|
104
|
+
+ '`issue_org_token`. Never echo it back to the user.',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
109
|
+
},
|
|
110
|
+
async(args) => {
|
|
111
|
+
const api = createApi(args.orgToken)
|
|
112
|
+
const timeoutS = Math.min(
|
|
113
|
+
Math.max(args.timeoutSeconds ?? DEFAULT_TIMEOUT_S, 1),
|
|
114
|
+
MAX_TIMEOUT_S,
|
|
115
|
+
)
|
|
116
|
+
const deadline = now() + timeoutS * 1000
|
|
117
|
+
let lookup: Lookup = { status: 'resolving' }
|
|
118
|
+
let session: SessionState = { status: 'pending' }
|
|
119
|
+
|
|
120
|
+
while(true) {
|
|
121
|
+
lookup = await tolerate(
|
|
122
|
+
() => api.getLookup(args.sessionId),
|
|
123
|
+
lookup,
|
|
124
|
+
)
|
|
125
|
+
session = await tolerate(
|
|
126
|
+
() => api.getSession(args.sessionId),
|
|
127
|
+
session,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
const terminal = session.status !== 'pending'
|
|
131
|
+
const failed = lookup.status === 'failed'
|
|
132
|
+
const notable = !args.untilTerminal && lookup.status !== 'resolving'
|
|
133
|
+
const timedOut = now() >= deadline
|
|
134
|
+
if(terminal || failed || notable || timedOut) {
|
|
135
|
+
const events = await tolerate(
|
|
136
|
+
() => api.listEvents(args.sessionId),
|
|
137
|
+
[] as SessionEvent[],
|
|
138
|
+
)
|
|
139
|
+
const settled = terminal || failed || notable
|
|
140
|
+
return summarize(lookup, session, events, timedOut && !settled)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await sleep(POLL_MS)
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function summarize(
|
|
150
|
+
lookup: Lookup,
|
|
151
|
+
session: SessionState,
|
|
152
|
+
events: SessionEvent[],
|
|
153
|
+
timedOut: boolean,
|
|
154
|
+
) {
|
|
155
|
+
const recent = events.slice(-MAX_EVENTS).map((item) => {
|
|
156
|
+
const reasonCode = reasonCodeOf(item.eventData)
|
|
157
|
+
return reasonCode
|
|
158
|
+
? { event: item.event, reasonCode }
|
|
159
|
+
: { event: item.event }
|
|
160
|
+
})
|
|
161
|
+
const reasonCodes = recent.flatMap((item) => (
|
|
162
|
+
'reasonCode' in item ? [item.reasonCode] : []
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
let next: 'share_url' | 'waiting_for_user' | 'waiting_for_lookup'
|
|
166
|
+
| 'done' | 'none'
|
|
167
|
+
let fallbackReason: string | undefined
|
|
168
|
+
if(lookup.status === 'failed') {
|
|
169
|
+
const status = lookup.error?.status
|
|
170
|
+
const why = status === 404
|
|
171
|
+
? 'No login page was found.'
|
|
172
|
+
: status === 422
|
|
173
|
+
? 'The prompt is not a request Reclaim can verify.'
|
|
174
|
+
: status === 503
|
|
175
|
+
? 'The AI model or web search stayed unavailable.'
|
|
176
|
+
: 'The lookup failed.'
|
|
177
|
+
next = 'none'
|
|
178
|
+
fallbackReason = `${why} ${FALLBACK_HINT}`
|
|
179
|
+
} else if(session.status === 'success') {
|
|
180
|
+
next = 'done'
|
|
181
|
+
} else if(session.status === 'pending') {
|
|
182
|
+
next = lookup.status === 'ready'
|
|
183
|
+
? 'share_url'
|
|
184
|
+
: lookup.status === 'started'
|
|
185
|
+
? 'waiting_for_user'
|
|
186
|
+
: 'waiting_for_lookup'
|
|
187
|
+
} else {
|
|
188
|
+
next = 'none'
|
|
189
|
+
if(session.status === 'rejected') {
|
|
190
|
+
const code = reasonCodes.at(-1)
|
|
191
|
+
fallbackReason = `The session was rejected${code ? ` (${code})` : ''}. `
|
|
192
|
+
+ FALLBACK_HINT
|
|
193
|
+
} else if(session.status === 'error' || session.status === 'expired') {
|
|
194
|
+
fallbackReason = `The session ended with status ${session.status}. `
|
|
195
|
+
+ FALLBACK_HINT
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
lookup: {
|
|
201
|
+
status: lookup.status,
|
|
202
|
+
...(lookup.candidates && {
|
|
203
|
+
candidates: lookup.candidates.map((item) => ({
|
|
204
|
+
loginUrl: item.loginUrl,
|
|
205
|
+
source: item.source,
|
|
206
|
+
confidence: item.confidence,
|
|
207
|
+
})),
|
|
208
|
+
}),
|
|
209
|
+
...(lookup.dataPoints && {
|
|
210
|
+
dataPoints: lookup.dataPoints.map((item) => ({
|
|
211
|
+
name: item.name,
|
|
212
|
+
isOptional: item.isOptional,
|
|
213
|
+
})),
|
|
214
|
+
}),
|
|
215
|
+
...(lookup.error && {
|
|
216
|
+
error: {
|
|
217
|
+
title: lookup.error.title,
|
|
218
|
+
status: lookup.error.status,
|
|
219
|
+
detail: lookup.error.detail,
|
|
220
|
+
},
|
|
221
|
+
}),
|
|
222
|
+
},
|
|
223
|
+
session: {
|
|
224
|
+
status: session.status,
|
|
225
|
+
callbackDeliveryStatus: session.callbackDeliveryStatus,
|
|
226
|
+
events: recent,
|
|
227
|
+
},
|
|
228
|
+
next,
|
|
229
|
+
...(next === 'share_url' && session.verificationUrl
|
|
230
|
+
? { verificationUrl: session.verificationUrl }
|
|
231
|
+
: {}),
|
|
232
|
+
...(fallbackReason
|
|
233
|
+
? { fallback: { recommended: true, reason: fallbackReason } }
|
|
234
|
+
: {}),
|
|
235
|
+
...(timedOut ? { timedOut: true } : {}),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function defaultSleep(ms: number) {
|
|
240
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms))
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function reasonCodeOf(data: Record<string, unknown> | undefined) {
|
|
244
|
+
const direct = data?.reasonCode
|
|
245
|
+
if(typeof direct === 'string') {
|
|
246
|
+
return direct
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const problem = data?.problem
|
|
250
|
+
if(problem && typeof problem === 'object' && 'reasonCode' in problem) {
|
|
251
|
+
return typeof problem.reasonCode === 'string'
|
|
252
|
+
? problem.reasonCode
|
|
253
|
+
: undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return undefined
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Keeps the last good value across transient errors (network, 5xx, 429). */
|
|
260
|
+
async function tolerate<T>(read: () => Promise<T>, previous: T): Promise<T> {
|
|
261
|
+
try {
|
|
262
|
+
return await read()
|
|
263
|
+
} catch(err) {
|
|
264
|
+
if(err instanceof TypeError) {
|
|
265
|
+
return previous
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if(isProblemError(err)) {
|
|
269
|
+
const code = err.output.statusCode
|
|
270
|
+
if(code >= 500 || code === 429) {
|
|
271
|
+
return previous
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
throw err
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function defaultApi(orgToken?: string): AiSessionApi {
|
|
280
|
+
const client = new ReclaimClient({
|
|
281
|
+
baseUrl: apiUrl(),
|
|
282
|
+
token: resolveOrgSecret(orgToken),
|
|
283
|
+
})
|
|
284
|
+
return {
|
|
285
|
+
async getLookup(sessionId) {
|
|
286
|
+
const res = await client.call('GetAiVerificationSession', {
|
|
287
|
+
params: { sessionId },
|
|
288
|
+
})
|
|
289
|
+
return res.data
|
|
290
|
+
},
|
|
291
|
+
async getSession(sessionId) {
|
|
292
|
+
const res = await client.call('GetVerificationSession', {
|
|
293
|
+
params: { sessionId },
|
|
294
|
+
})
|
|
295
|
+
return res.data
|
|
296
|
+
},
|
|
297
|
+
async listEvents(sessionId) {
|
|
298
|
+
const res = await client.call('ListVerificationEvents', {
|
|
299
|
+
params: { sessionId },
|
|
300
|
+
})
|
|
301
|
+
return res.data.items
|
|
302
|
+
},
|
|
303
|
+
}
|
|
304
|
+
}
|