@reclaimprotocol/agent 0.1.17 → 0.1.20
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 +6 -2
- package/lib/authoring/backend.d.ts +4 -0
- package/lib/authoring/tools.js +4 -1
- package/lib/cdp/capture.d.ts +2 -2
- package/lib/cdp/capture.js +25 -21
- package/lib/cdp/client.d.ts +39 -1
- package/lib/cdp/client.js +125 -1
- package/lib/consts.d.ts +7 -0
- package/lib/consts.js +15 -0
- package/lib/mcp/api/operations.gen.js +33 -9
- package/lib/mcp/instructions.js +27 -3
- package/lib/mcp/start.js +12 -8
- package/lib/mcp/tools/agent/attach.d.ts +18 -4
- package/lib/mcp/tools/agent/attach.js +142 -52
- package/lib/mcp/tools/agent/build.d.ts +1 -1
- package/lib/mcp/tools/agent/build.js +5 -3
- package/lib/mcp/tools/agent/capture.d.ts +1 -0
- package/lib/mcp/tools/agent/capture.js +8 -1
- package/lib/mcp/tools/agent/eval.js +2 -3
- package/lib/mcp/tools/agent/how-it-works.d.ts +1 -1
- package/lib/mcp/tools/agent/how-it-works.js +1 -1
- package/lib/mcp/tools/agent/publish.js +55 -1
- package/lib/mcp/tools/agent/session.d.ts +2 -2
- package/lib/mcp/tools/agent/session.js +21 -5
- package/lib/mcp/tools/agent/shared.js +4 -6
- package/lib/mcp/tools/authenticate/authenticate.d.ts +1 -1
- package/lib/mcp/tools/authenticate/authenticate.js +10 -4
- package/lib/mcp/tools/authenticate/builder-access.d.ts +8 -0
- package/lib/mcp/tools/authenticate/builder-access.js +31 -0
- package/lib/mcp/tools/authenticate/index.d.ts +1 -0
- package/lib/mcp/tools/authenticate/index.js +1 -0
- package/lib/mcp/tools/mode/mode.d.ts +3 -2
- package/lib/mcp/tools/mode/mode.js +24 -4
- package/lib/old/login-cdp.js +11 -3
- package/lib/old/to-register.d.ts +16 -8
- package/lib/old/to-register.js +29 -21
- package/lib/old/tools/publish.js +109 -9
- package/lib/provider/draft.d.ts +20 -0
- package/lib/provider/draft.js +75 -51
- package/lib/provider/index.d.ts +1 -1
- package/lib/provider/index.js +1 -1
- package/lib/provider/injection-doc.js +4 -3
- package/lib/provider/schema.d.ts +26 -1
- package/lib/provider/secrets.d.ts +37 -0
- package/lib/provider/secrets.js +147 -6
- package/lib/provider/to-version.d.ts +26 -2
- package/lib/provider/to-version.js +29 -5
- package/package.json +2 -2
- package/skill/SKILL.builder.md +43 -95
- package/skill/SKILL.common.md +102 -0
- package/skill/SKILL.old.md +20 -74
- package/skill/builder-mode.md +4 -2
- package/skill/injection-api.md +95 -10
- package/skill/old-mode.md +5 -3
- package/src/authoring/backend.ts +4 -0
- package/src/authoring/tools.ts +4 -1
- package/src/cdp/capture.ts +24 -24
- package/src/cdp/client.ts +197 -2
- package/src/consts.ts +20 -0
- package/src/mcp/api/operations.gen.ts +33 -9
- package/src/mcp/instructions.ts +36 -3
- package/src/mcp/start.ts +13 -8
- package/src/mcp/tools/agent/attach.ts +198 -57
- package/src/mcp/tools/agent/build.ts +9 -2
- package/src/mcp/tools/agent/capture.ts +10 -1
- package/src/mcp/tools/agent/eval.ts +2 -3
- package/src/mcp/tools/agent/how-it-works.ts +1 -1
- package/src/mcp/tools/agent/publish.ts +64 -1
- package/src/mcp/tools/agent/session.ts +29 -8
- package/src/mcp/tools/agent/shared.ts +5 -10
- package/src/mcp/tools/authenticate/authenticate.ts +14 -4
- package/src/mcp/tools/authenticate/builder-access.ts +42 -0
- package/src/mcp/tools/authenticate/index.ts +4 -0
- package/src/mcp/tools/mode/mode.ts +28 -4
- package/src/old/login-cdp.ts +13 -3
- package/src/old/to-register.ts +50 -27
- package/src/old/tools/publish.ts +131 -9
- package/src/provider/draft.ts +98 -56
- package/src/provider/index.ts +9 -1
- package/src/provider/injection-doc.ts +4 -3
- package/src/provider/schema.ts +27 -1
- package/src/provider/secrets.ts +171 -6
- package/src/provider/to-version.ts +53 -7
- package/lib/authoring/find.d.ts +0 -3
- package/lib/authoring/find.js +0 -54
- package/lib/authoring/propose.d.ts +0 -3
- package/lib/authoring/propose.js +0 -109
- package/lib/authoring/replay.d.ts +0 -10
- package/lib/authoring/replay.js +0 -34
- package/lib/cli/args.d.ts +0 -14
- package/lib/cli/args.js +0 -19
- package/lib/cli/commands.d.ts +0 -1
- package/lib/cli/commands.js +0 -19
- package/lib/cli.d.ts +0 -1
- package/lib/cli.js +0 -7
- package/lib/mcp/credentials/authenticate.d.ts +0 -3
- package/lib/mcp/credentials/authenticate.js +0 -108
- package/lib/mcp/credentials/build.d.ts +0 -5
- package/lib/mcp/credentials/build.js +0 -18
- package/lib/mcp/credentials/cache.d.ts +0 -70
- package/lib/mcp/credentials/cache.js +0 -131
- package/lib/mcp/credentials/env-file.d.ts +0 -25
- package/lib/mcp/credentials/env-file.js +0 -71
- package/lib/mcp/credentials/import.d.ts +0 -2
- package/lib/mcp/credentials/import.js +0 -52
- package/lib/mcp/credentials/issue.d.ts +0 -2
- package/lib/mcp/credentials/issue.js +0 -84
- package/lib/mcp/credentials/keypair.d.ts +0 -17
- package/lib/mcp/credentials/keypair.js +0 -48
- package/lib/mcp/credentials/link.d.ts +0 -3
- package/lib/mcp/credentials/link.js +0 -107
- package/lib/mcp/credentials/list-cached.d.ts +0 -2
- package/lib/mcp/credentials/list-cached.js +0 -55
- package/lib/mcp/credentials/owner-key.d.ts +0 -23
- package/lib/mcp/credentials/owner-key.js +0 -58
- package/lib/mcp/credentials/paths.d.ts +0 -12
- package/lib/mcp/credentials/paths.js +0 -35
- package/lib/mcp/credentials/poll.d.ts +0 -27
- package/lib/mcp/credentials/poll.js +0 -61
- package/lib/mcp/credentials/register.d.ts +0 -18
- package/lib/mcp/credentials/register.js +0 -30
- package/lib/mcp/credentials/resolve-owner-key.d.ts +0 -12
- package/lib/mcp/credentials/resolve-owner-key.js +0 -68
- package/lib/mcp/credentials/session-cache.d.ts +0 -14
- package/lib/mcp/credentials/session-cache.js +0 -42
- package/lib/mcp/credentials/status.d.ts +0 -3
- package/lib/mcp/credentials/status.js +0 -76
- package/lib/mcp/register.d.ts +0 -38
- package/lib/mcp/register.js +0 -80
- package/lib/mcp/tools/analyze.d.ts +0 -3
- package/lib/mcp/tools/analyze.js +0 -26
- package/lib/mcp/tools/attach.d.ts +0 -11
- package/lib/mcp/tools/attach.js +0 -72
- package/lib/mcp/tools/capture.d.ts +0 -10
- package/lib/mcp/tools/capture.js +0 -142
- package/lib/mcp/tools/eval.d.ts +0 -5
- package/lib/mcp/tools/eval.js +0 -127
- package/lib/mcp/tools/find.d.ts +0 -3
- package/lib/mcp/tools/find.js +0 -60
- package/lib/mcp/tools/owner-key.d.ts +0 -2
- package/lib/mcp/tools/owner-key.js +0 -63
- package/lib/mcp/tools/proof.d.ts +0 -3
- package/lib/mcp/tools/proof.js +0 -214
- package/lib/mcp/tools/propose.d.ts +0 -9
- package/lib/mcp/tools/propose.js +0 -75
- package/lib/mcp/tools/publish.d.ts +0 -3
- package/lib/mcp/tools/publish.js +0 -59
- package/lib/mcp/tools/replay.d.ts +0 -3
- package/lib/mcp/tools/replay.js +0 -46
- package/lib/mcp/tools/session.d.ts +0 -10
- package/lib/mcp/tools/session.js +0 -29
- package/lib/mcp/tools/verification/diagnose.d.ts +0 -11
- package/lib/mcp/tools/verification/diagnose.js +0 -132
- package/lib/mcp/verifications/build.d.ts +0 -5
- package/lib/mcp/verifications/build.js +0 -14
- package/lib/mcp/verifications/create.d.ts +0 -3
- package/lib/mcp/verifications/create.js +0 -57
- package/lib/mcp/verifications/find.d.ts +0 -3
- package/lib/mcp/verifications/find.js +0 -37
- package/lib/mcp/verifications/get.d.ts +0 -3
- package/lib/mcp/verifications/get.js +0 -26
- package/lib/mcp/verifications/update.d.ts +0 -3
- package/lib/mcp/verifications/update.js +0 -78
- package/lib/old/tools/get-version-config.d.ts +0 -17
- package/lib/old/tools/get-version-config.js +0 -47
- package/lib/routes.gen.d.ts +0 -271
- package/lib/routes.gen.js +0 -73
- package/lib/types/openapi.d.ts +0 -4
- package/lib/types/openapi.gen.d.ts +0 -3967
- package/lib/types/openapi.gen.js +0 -5
- package/lib/types/openapi.js +0 -1
package/src/mcp/start.ts
CHANGED
|
@@ -20,6 +20,8 @@ import { NAME, VERSION } from '../pkg.ts'
|
|
|
20
20
|
import { registerTools } from './api/tools.ts'
|
|
21
21
|
import {
|
|
22
22
|
buildAgentTools,
|
|
23
|
+
buildBuilderAuthenticationTools,
|
|
24
|
+
buildBuilderOrganizationTools,
|
|
23
25
|
buildCredentialTools,
|
|
24
26
|
buildLocalCredentialTools,
|
|
25
27
|
buildModeTools,
|
|
@@ -49,7 +51,13 @@ export async function startMcpServer() {
|
|
|
49
51
|
// deployments — set RECLAIM_AGENT_DISABLED=1 to register only the API
|
|
50
52
|
// surface.
|
|
51
53
|
const agentToolsEnabled = !agentDisabled()
|
|
52
|
-
|
|
54
|
+
const builderClient = new ReclaimClient({
|
|
55
|
+
baseUrl: apiUrl(),
|
|
56
|
+
// Builder access is orthogonal to publishing mode. A legacy-devtools
|
|
57
|
+
// session can authenticate here solely to allocate a remote browser.
|
|
58
|
+
token: apiToken() || readSession()?.token,
|
|
59
|
+
headers: { 'User-Agent': `${NAME}/${VERSION}` },
|
|
60
|
+
})
|
|
53
61
|
// Backend-mode toggle — available in both modes and regardless of whether
|
|
54
62
|
// the agent tools are enabled, so a user can always switch either direction.
|
|
55
63
|
registerToolList(server, buildModeTools(config, oldMode))
|
|
@@ -66,20 +74,17 @@ export async function startMcpServer() {
|
|
|
66
74
|
})
|
|
67
75
|
const sdkClient = new ReclaimSdkClient({ baseUrl: sdkApiUrl() })
|
|
68
76
|
|
|
77
|
+
registerToolList(server, buildBuilderAuthenticationTools(builderClient))
|
|
78
|
+
registerToolList(server, buildBuilderOrganizationTools(builderClient))
|
|
69
79
|
registerToolList(server, buildLocalCredentialTools())
|
|
70
80
|
registerToolList(server, buildOldBackendTools(oldClient, sdkClient))
|
|
71
81
|
if(agentToolsEnabled) {
|
|
72
82
|
// No builder client → no builder publish tool (old mode has its own).
|
|
73
|
-
registerToolList(server, buildAgentTools())
|
|
83
|
+
registerToolList(server, buildAgentTools(undefined, builderClient))
|
|
74
84
|
}
|
|
75
85
|
// NOTE: registerTools() is intentionally NOT called — generated tools off.
|
|
76
86
|
} else {
|
|
77
|
-
const client =
|
|
78
|
-
baseUrl: apiUrl(),
|
|
79
|
-
// Priority: explicit env var → cached session token → unauthenticated
|
|
80
|
-
token: apiToken() || readSession()?.token,
|
|
81
|
-
headers: { 'User-Agent': `${NAME}/${VERSION}` },
|
|
82
|
-
})
|
|
87
|
+
const client = builderClient
|
|
83
88
|
|
|
84
89
|
// Tool names must stay disjoint across these three groups: hand-written
|
|
85
90
|
// tools are higher-level orchestrations (multi-call flows, ReclaimProvider
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { badRequest } from '@hapi/boom'
|
|
2
|
+
import type { ReclaimClient } from '@reclaimprotocol/client'
|
|
3
|
+
import assert from 'node:assert'
|
|
4
|
+
import {
|
|
5
|
+
type CdpHandles,
|
|
6
|
+
connectCdp,
|
|
7
|
+
connectCdpUrl,
|
|
8
|
+
} from '../../../cdp/client.ts'
|
|
2
9
|
import {
|
|
3
10
|
defaultProfileDir,
|
|
4
11
|
launchChrome,
|
|
@@ -8,88 +15,222 @@ import type { ConfigStore } from '../../../config/store.ts'
|
|
|
8
15
|
import { defaultConfigPath } from '../../../config/store.ts'
|
|
9
16
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
10
17
|
|
|
18
|
+
export type BrowserMode = 'dedicated' | 'attach' | 'builder' | 'custom'
|
|
19
|
+
|
|
11
20
|
export interface AttachState {
|
|
12
21
|
handles: CdpHandles
|
|
13
|
-
|
|
22
|
+
mode: BrowserMode
|
|
23
|
+
port?: number
|
|
24
|
+
liveViewUrl?: string
|
|
25
|
+
browserSessionId?: string
|
|
26
|
+
orgId?: string
|
|
27
|
+
disposeRemote?: () => Promise<void>
|
|
14
28
|
}
|
|
15
29
|
|
|
16
|
-
export
|
|
30
|
+
export interface AttachRef {
|
|
17
31
|
current?: AttachState
|
|
18
32
|
config: ConfigStore
|
|
19
|
-
}
|
|
20
|
-
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AttachArgs {
|
|
36
|
+
mode?: BrowserMode
|
|
37
|
+
/** Backward-compatible local-mode spelling. */
|
|
38
|
+
attachMode?: 'dedicated' | 'attach'
|
|
39
|
+
port?: number
|
|
40
|
+
orgId?: string
|
|
41
|
+
cdpUrl?: string
|
|
42
|
+
liveViewUrl?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface AttachDependencies {
|
|
46
|
+
connectLocal: typeof connectCdp
|
|
47
|
+
connectRemote: typeof connectCdpUrl
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const DEFAULT_DEPENDENCIES: AttachDependencies = {
|
|
51
|
+
connectLocal: connectCdp,
|
|
52
|
+
connectRemote: connectCdpUrl,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function attachTool(
|
|
56
|
+
state: AttachRef,
|
|
57
|
+
builderClient?: ReclaimClient,
|
|
58
|
+
name: 'attach_browser' | 'attach_chrome' = 'attach_browser',
|
|
59
|
+
beforeReplace?: () => Promise<void>,
|
|
60
|
+
dependencies: AttachDependencies = DEFAULT_DEPENDENCIES,
|
|
61
|
+
): RegisteredTool {
|
|
62
|
+
return defineTool<AttachArgs>(
|
|
21
63
|
{
|
|
22
|
-
name
|
|
64
|
+
name,
|
|
23
65
|
description:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
66
|
+
name === 'attach_chrome'
|
|
67
|
+
? 'Backward-compatible alias for attach_browser.'
|
|
68
|
+
: 'Connect the provider-authoring tools to a browser. Choose '
|
|
69
|
+
+ 'dedicated (launch local Chrome), attach (existing local CDP '
|
|
70
|
+
+ 'port), builder (quota-accounted Popcorn browser with a '
|
|
71
|
+
+ 'shareable live view), or custom (caller-supplied CDP '
|
|
72
|
+
+ 'websocket). '
|
|
73
|
+
+ 'Builder and custom CDP credentials are held in memory and never '
|
|
74
|
+
+ 'returned.',
|
|
28
75
|
inputSchema: {
|
|
29
76
|
type: 'object',
|
|
30
77
|
properties: {
|
|
31
|
-
|
|
32
|
-
|
|
78
|
+
mode: {
|
|
79
|
+
type: 'string',
|
|
80
|
+
enum: ['dedicated', 'attach', 'builder', 'custom'],
|
|
81
|
+
},
|
|
82
|
+
attachMode: {
|
|
83
|
+
type: 'string',
|
|
84
|
+
enum: ['dedicated', 'attach'],
|
|
85
|
+
description: 'Deprecated local-mode alias for mode.',
|
|
86
|
+
},
|
|
87
|
+
port: { type: 'number', minimum: 1, maximum: 65535 },
|
|
88
|
+
orgId: { type: 'string', format: 'uuid' },
|
|
89
|
+
cdpUrl: { type: 'string', maxLength: 4000 },
|
|
90
|
+
liveViewUrl: { type: 'string', format: 'uri', maxLength: 4000 },
|
|
33
91
|
},
|
|
34
92
|
},
|
|
35
93
|
},
|
|
36
94
|
async(parsed) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
attachMode: parsed.attachMode ?? cfg?.attachMode ?? 'dedicated',
|
|
41
|
-
port: parsed.port ?? cfg?.port ?? 9222,
|
|
42
|
-
}
|
|
43
|
-
state.config.write({ browser_agent: cfg })
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if(!cfg) {
|
|
95
|
+
const persisted = state.config.read()?.browser_agent
|
|
96
|
+
const mode = parsed.mode ?? parsed.attachMode ?? persisted?.attachMode
|
|
97
|
+
if(!mode) {
|
|
47
98
|
return {
|
|
48
99
|
needsChoice: true,
|
|
49
100
|
message:
|
|
50
|
-
'
|
|
51
|
-
'
|
|
52
|
-
'
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'BOTH --remote-debugging-port=9222 AND ' +
|
|
56
|
-
'--user-data-dir=<some-non-default-path>; ' +
|
|
57
|
-
'since Chrome 136 the debug-port flag is silently ' +
|
|
58
|
-
'ignored if the default profile is in use).',
|
|
101
|
+
'Choose a browser mode and call attach_browser again: '
|
|
102
|
+
+ '"dedicated" launches a local dedicated Chrome profile; '
|
|
103
|
+
+ '"attach" connects to a local Chrome debugging port; '
|
|
104
|
+
+ '"builder" creates a quota-accounted remote browser and '
|
|
105
|
+
+ 'requires orgId; "custom" requires a CDP websocket URL.',
|
|
59
106
|
configPath: defaultConfigPath(),
|
|
60
107
|
}
|
|
61
108
|
}
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new Error(
|
|
73
|
-
`attach mode: nothing answering CDP on port ${cfg.port}. ` +
|
|
74
|
-
'Chrome must be launched with BOTH ' +
|
|
75
|
-
`--remote-debugging-port=${cfg.port} AND ` +
|
|
76
|
-
'--user-data-dir=<some-non-default-path>. ' +
|
|
77
|
-
'Since Chrome 136 (May 2025), the debug-port flag is ' +
|
|
78
|
-
'silently ignored when the default ' +
|
|
79
|
-
"user profile is in use — that's why nothing opens the port. " +
|
|
80
|
-
'Either re-launch Chrome with ' +
|
|
81
|
-
'a custom --user-data-dir, or switch to attachMode="dedicated".',
|
|
82
|
-
)
|
|
83
|
-
})
|
|
110
|
+
const next = await connect(
|
|
111
|
+
mode, parsed, persisted, builderClient, dependencies,
|
|
112
|
+
)
|
|
113
|
+
try {
|
|
114
|
+
await beforeReplace?.()
|
|
115
|
+
await disposeAttached(state)
|
|
116
|
+
} catch(err) {
|
|
117
|
+
await disposeState(next).catch(() => {})
|
|
118
|
+
throw err
|
|
84
119
|
}
|
|
85
120
|
|
|
86
|
-
|
|
87
|
-
|
|
121
|
+
state.current = next
|
|
122
|
+
if(mode === 'dedicated' || mode === 'attach') {
|
|
123
|
+
state.config.write({
|
|
124
|
+
browser_agent: { attachMode: mode, port: next.port! },
|
|
125
|
+
})
|
|
88
126
|
}
|
|
89
127
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
128
|
+
return {
|
|
129
|
+
attached: true,
|
|
130
|
+
mode,
|
|
131
|
+
...(next.port ? { port: next.port } : {}),
|
|
132
|
+
...(next.browserSessionId
|
|
133
|
+
? { browserSessionId: next.browserSessionId }
|
|
134
|
+
: {}),
|
|
135
|
+
...(next.liveViewUrl ? { liveViewUrl: next.liveViewUrl } : {}),
|
|
136
|
+
}
|
|
93
137
|
},
|
|
94
138
|
)
|
|
95
139
|
}
|
|
140
|
+
|
|
141
|
+
export async function disposeAttached(state: AttachRef) {
|
|
142
|
+
const current = state.current
|
|
143
|
+
if(!current) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
delete state.current
|
|
148
|
+
await disposeState(current)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function connect(
|
|
152
|
+
mode: BrowserMode,
|
|
153
|
+
parsed: AttachArgs,
|
|
154
|
+
persisted: { attachMode: 'dedicated' | 'attach', port: number } | undefined,
|
|
155
|
+
builderClient: ReclaimClient | undefined,
|
|
156
|
+
dependencies: AttachDependencies,
|
|
157
|
+
): Promise<AttachState> {
|
|
158
|
+
if(mode === 'builder') {
|
|
159
|
+
assert(builderClient, badRequest('Builder browser service is unavailable'))
|
|
160
|
+
assert(parsed.orgId, badRequest('orgId is required for builder mode'))
|
|
161
|
+
const { data } = await builderClient.call('CreateAgentBrowserSession', {
|
|
162
|
+
params: { orgId: parsed.orgId },
|
|
163
|
+
})
|
|
164
|
+
let handles: CdpHandles
|
|
165
|
+
try {
|
|
166
|
+
handles = await dependencies.connectRemote(data.cdpUrl)
|
|
167
|
+
} catch(err) {
|
|
168
|
+
await builderClient.call('DisposeAgentBrowserSession', {
|
|
169
|
+
params: {
|
|
170
|
+
orgId: parsed.orgId,
|
|
171
|
+
browserSessionId: data.id,
|
|
172
|
+
},
|
|
173
|
+
}).catch(() => {})
|
|
174
|
+
throw err
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
handles,
|
|
179
|
+
mode,
|
|
180
|
+
liveViewUrl: data.liveViewUrl,
|
|
181
|
+
browserSessionId: data.id,
|
|
182
|
+
orgId: parsed.orgId,
|
|
183
|
+
disposeRemote: async() => {
|
|
184
|
+
await builderClient.call('DisposeAgentBrowserSession', {
|
|
185
|
+
params: {
|
|
186
|
+
orgId: parsed.orgId!,
|
|
187
|
+
browserSessionId: data.id,
|
|
188
|
+
},
|
|
189
|
+
})
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if(mode === 'custom') {
|
|
195
|
+
assert(parsed.cdpUrl, badRequest('cdpUrl is required for custom mode'))
|
|
196
|
+
const url = new URL(parsed.cdpUrl)
|
|
197
|
+
assert(
|
|
198
|
+
url.protocol === 'ws:' || url.protocol === 'wss:',
|
|
199
|
+
badRequest('cdpUrl must use ws:// or wss://'),
|
|
200
|
+
)
|
|
201
|
+
return {
|
|
202
|
+
handles: await dependencies.connectRemote(parsed.cdpUrl),
|
|
203
|
+
mode,
|
|
204
|
+
...(parsed.liveViewUrl ? { liveViewUrl: parsed.liveViewUrl } : {}),
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const port = parsed.port ?? persisted?.port ?? 9222
|
|
209
|
+
if(mode === 'dedicated') {
|
|
210
|
+
try {
|
|
211
|
+
await waitForCdpReady(port, 500)
|
|
212
|
+
} catch{
|
|
213
|
+
launchChrome({ port, profileDir: defaultProfileDir() })
|
|
214
|
+
await waitForCdpReady(port, 10_000)
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
await waitForCdpReady(port, 2_000).catch(() => {
|
|
218
|
+
throw new Error(
|
|
219
|
+
`attach mode: nothing answering CDP on port ${port}. Chrome must `
|
|
220
|
+
+ `be launched with --remote-debugging-port=${port} AND `
|
|
221
|
+
+ '--user-data-dir=<some-non-default-path>. Chrome 136+ ignores '
|
|
222
|
+
+ 'the debugging flag for the default profile.',
|
|
223
|
+
)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { handles: await dependencies.connectLocal(port), mode, port }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function disposeState(state: AttachState) {
|
|
231
|
+
try {
|
|
232
|
+
await state.handles.close()
|
|
233
|
+
} finally {
|
|
234
|
+
await state.disposeRemote?.()
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -16,7 +16,7 @@ import { ensureAttestorCrypto } from '../../../proof/proof-core.ts'
|
|
|
16
16
|
import { type RegisteredTool } from '../../server.ts'
|
|
17
17
|
import { analyzeTool } from './analyze.ts'
|
|
18
18
|
import { type AttachState, attachTool } from './attach.ts'
|
|
19
|
-
import { type CaptureStore, captureTools } from './capture.ts'
|
|
19
|
+
import { type CaptureStore, captureTools, resetCaptures } from './capture.ts'
|
|
20
20
|
import { listTabsTool } from './eval.ts'
|
|
21
21
|
import { howItWorksTool } from './how-it-works.ts'
|
|
22
22
|
import { createVersionFromCaptureTool } from './publish.ts'
|
|
@@ -42,6 +42,7 @@ function buildAgentState(): AgentState {
|
|
|
42
42
|
* capture/proof tools — old mode supplies its own publish tool. */
|
|
43
43
|
export function buildAgentTools(
|
|
44
44
|
client?: ReclaimClient,
|
|
45
|
+
builderClient: ReclaimClient | undefined = client,
|
|
45
46
|
): RegisteredTool[] {
|
|
46
47
|
const state = buildAgentState()
|
|
47
48
|
ensureAttestorCrypto()
|
|
@@ -49,7 +50,13 @@ export function buildAgentTools(
|
|
|
49
50
|
// Stateless docs tool — serve the mode-specific authoring guide to any
|
|
50
51
|
// client. No `client` ⇒ old-devtools mode (see `mcp/start.ts`).
|
|
51
52
|
tools.push(howItWorksTool(!client))
|
|
52
|
-
|
|
53
|
+
const beforeReplace = () => resetCaptures(state.captures)
|
|
54
|
+
tools.push(attachTool(
|
|
55
|
+
state.attach, builderClient, 'attach_browser', beforeReplace,
|
|
56
|
+
))
|
|
57
|
+
tools.push(attachTool(
|
|
58
|
+
state.attach, builderClient, 'attach_chrome', beforeReplace,
|
|
59
|
+
))
|
|
53
60
|
tools.push(listTabsTool(state.attach))
|
|
54
61
|
for(const t of captureTools(state.attach, state.captures)) {
|
|
55
62
|
tools.push(t)
|
|
@@ -20,13 +20,22 @@ export interface CaptureStore {
|
|
|
20
20
|
activeTab?: string
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
export async function resetCaptures(captures: CaptureStore) {
|
|
24
|
+
for(const entry of captures.sessions.values()) {
|
|
25
|
+
await entry.session.stop().catch(() => {})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
captures.sessions.clear()
|
|
29
|
+
delete captures.activeTab
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export function captureTools(
|
|
24
33
|
attachRef: { current?: AttachState },
|
|
25
34
|
captures: CaptureStore,
|
|
26
35
|
): RegisteredTool[] {
|
|
27
36
|
const requireAttached = (): AttachState => {
|
|
28
37
|
if(!attachRef.current) {
|
|
29
|
-
throw new Error('Not attached. Call
|
|
38
|
+
throw new Error('Not attached. Call attach_browser first.')
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
return attachRef.current
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { listTabs } from '../../../cdp/tabs.ts'
|
|
2
1
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
3
2
|
import type { AttachState } from './attach.ts'
|
|
4
3
|
|
|
@@ -16,10 +15,10 @@ export function listTabsTool(attachRef: {
|
|
|
16
15
|
},
|
|
17
16
|
async() => {
|
|
18
17
|
if(!attachRef.current) {
|
|
19
|
-
throw new Error('Not attached. Call
|
|
18
|
+
throw new Error('Not attached. Call attach_browser first.')
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
return { tabs: await
|
|
21
|
+
return { tabs: await attachRef.current.handles.listTabs() }
|
|
23
22
|
},
|
|
24
23
|
)
|
|
25
24
|
}
|
|
@@ -11,7 +11,7 @@ import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
|
11
11
|
* so any MCP client — not just Claude Code, which alone auto-loads skills — can
|
|
12
12
|
* pull it. `oldMode` selects the backend-specific guide so the agent is never
|
|
13
13
|
* told to call auth/publish tools that aren't registered in its mode.
|
|
14
|
-
* Stateless: callable before `
|
|
14
|
+
* Stateless: callable before `attach_browser`.
|
|
15
15
|
*/
|
|
16
16
|
export function howItWorksTool(oldMode: boolean): RegisteredTool {
|
|
17
17
|
const guide = oldMode ? PROVIDER_GUIDE_OLD : PROVIDER_GUIDE_BUILDER
|
|
@@ -24,6 +24,12 @@ const RESPONSE_MATCHES_SCHEMA: JSONSchema.Schema = {
|
|
|
24
24
|
properties: {
|
|
25
25
|
type: { type: 'string', enum: ['regex', 'contains'] },
|
|
26
26
|
value: { type: 'string' },
|
|
27
|
+
isOptional: {
|
|
28
|
+
type: 'boolean',
|
|
29
|
+
description:
|
|
30
|
+
'Mark this match non-fatal (verification continues when it ' +
|
|
31
|
+
'fails). Default false.',
|
|
32
|
+
},
|
|
27
33
|
},
|
|
28
34
|
required: ['type', 'value'],
|
|
29
35
|
},
|
|
@@ -59,6 +65,13 @@ const REQUEST_DRAFT_PROPERTIES: Record<string, JSONSchema.Schema> = {
|
|
|
59
65
|
},
|
|
60
66
|
method: { type: 'string', enum: ['GET', 'POST', 'PUT', 'PATCH'] },
|
|
61
67
|
body: { type: 'string', description: 'Request body template, if any.' },
|
|
68
|
+
credentials: {
|
|
69
|
+
type: 'string',
|
|
70
|
+
enum: ['omit', 'same-origin', 'include'],
|
|
71
|
+
description:
|
|
72
|
+
'How the verification client supplies cookies/auth when replaying ' +
|
|
73
|
+
'this request. Default `include`.',
|
|
74
|
+
},
|
|
62
75
|
responseMatches: RESPONSE_MATCHES_SCHEMA,
|
|
63
76
|
responseRedactions: RESPONSE_REDACTIONS_SCHEMA,
|
|
64
77
|
}
|
|
@@ -120,6 +133,11 @@ interface PublishArgs {
|
|
|
120
133
|
version?: { major: number, minor: number, patch: number }
|
|
121
134
|
jsUserScripts?: string
|
|
122
135
|
allowedJsRequests?: AllowedJsRequest[]
|
|
136
|
+
interceptorOptions?: {
|
|
137
|
+
interceptorType: 'HAWKEYE' | 'MSWJS' | 'CDP'
|
|
138
|
+
isDocumentRequestReplayEnabled?: boolean
|
|
139
|
+
interceptorSettings?: string
|
|
140
|
+
}
|
|
123
141
|
}
|
|
124
142
|
|
|
125
143
|
export function createVersionFromCaptureTool(
|
|
@@ -216,9 +234,53 @@ export function createVersionFromCaptureTool(
|
|
|
216
234
|
description:
|
|
217
235
|
'Extra requests the injection script is allowed to fire ' +
|
|
218
236
|
'(e.g. paginated API calls), as RequestSelectionTemplates. ' +
|
|
219
|
-
'`multiple`/`required` default to true.'
|
|
237
|
+
'`multiple`/`required` default to true. ' +
|
|
238
|
+
'⚠️ BEFORE populating this to gate ' +
|
|
239
|
+
'`verifyProof(proof, { providerId })` (old-devtools calls the ' +
|
|
240
|
+
'same concept `allowedInjectedRequestData` — same mechanics, ' +
|
|
241
|
+
'different field name): this whole entry (url, method, body, ' +
|
|
242
|
+
'responseMatches, responseRedactions) feeds a client-side ' +
|
|
243
|
+
'hash-equality check, and `url` is hashed VERBATIM — no regex/ ' +
|
|
244
|
+
'template resolution, even with `urlType: REGEX` or ' +
|
|
245
|
+
'`TEMPLATE`. If the real captured URL contains ANY per-session ' +
|
|
246
|
+
'part (a tracking nonce, CSRF token, cache-busting timestamp), ' +
|
|
247
|
+
'no declared spec can EVER hash-match a real proof — populating' +
|
|
248
|
+
' this for such a provider makes verifyProof reject every real ' +
|
|
249
|
+
'proof, which is worse than leaving it empty. Diff the same ' +
|
|
250
|
+
'endpoint\'s captured URL across two separate sessions before ' +
|
|
251
|
+
'wiring this up. If provider-hash validation keeps causing ' +
|
|
252
|
+
'problems (unstable url, or any other persistent mismatch), ' +
|
|
253
|
+
'the fallback on the verifying side is `verifyProof(proof, { ' +
|
|
254
|
+
'dangerouslyDisableContentValidation: true })` — trusts only ' +
|
|
255
|
+
'the attestor\'s signature, skips the hash-equality check ' +
|
|
256
|
+
'entirely; see the "Provider-hash validation" section of the ' +
|
|
257
|
+
'injection script\'s guide for the full mechanics and when to ' +
|
|
258
|
+
'reach for it.',
|
|
220
259
|
items: ALLOWED_JS_REQUEST_SCHEMA,
|
|
221
260
|
},
|
|
261
|
+
interceptorOptions: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
description:
|
|
264
|
+
'In-app interception settings, stored as ' +
|
|
265
|
+
'webSettings.clientOptions.inapp.interceptorOptions. ' +
|
|
266
|
+
'`isDocumentRequestReplayEnabled` is the INVERSE of ' +
|
|
267
|
+
'old-devtools\' `disableRequestReplay`: true replays the ' +
|
|
268
|
+
'main document request via the attestor (default false).',
|
|
269
|
+
properties: {
|
|
270
|
+
interceptorType: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
enum: ['HAWKEYE', 'MSWJS', 'CDP'],
|
|
273
|
+
description: 'How traffic interception is implemented.',
|
|
274
|
+
},
|
|
275
|
+
isDocumentRequestReplayEnabled: { type: 'boolean' },
|
|
276
|
+
interceptorSettings: {
|
|
277
|
+
type: 'string',
|
|
278
|
+
description:
|
|
279
|
+
'Free-form JSON config for the chosen interceptor.',
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
required: ['interceptorType'],
|
|
283
|
+
},
|
|
222
284
|
},
|
|
223
285
|
required: ['providerId', 'initialUrl'],
|
|
224
286
|
},
|
|
@@ -244,6 +306,7 @@ export function createVersionFromCaptureTool(
|
|
|
244
306
|
version: ver,
|
|
245
307
|
jsUserScripts: args.jsUserScripts,
|
|
246
308
|
allowedJsRequests: args.allowedJsRequests,
|
|
309
|
+
interceptorOptions: args.interceptorOptions,
|
|
247
310
|
})
|
|
248
311
|
const result = await client.call('CreateProviderVersion', {
|
|
249
312
|
params: { providerId: args.providerId },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ConfigStore } from '../../../config/store.ts'
|
|
2
2
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
3
|
-
import type
|
|
4
|
-
import type
|
|
3
|
+
import { type AttachState, disposeAttached } from './attach.ts'
|
|
4
|
+
import { type CaptureStore, resetCaptures } from './capture.ts'
|
|
5
5
|
|
|
6
6
|
export function sessionTools(
|
|
7
7
|
attachRef: { current?: AttachState, config: ConfigStore },
|
|
@@ -18,11 +18,29 @@ export function sessionTools(
|
|
|
18
18
|
async() => ({
|
|
19
19
|
config: attachRef.config.read()?.browser_agent,
|
|
20
20
|
attached: Boolean(attachRef.current),
|
|
21
|
+
mode: attachRef.current?.mode,
|
|
21
22
|
port: attachRef.current?.port,
|
|
23
|
+
liveViewUrl: attachRef.current?.liveViewUrl,
|
|
24
|
+
browserSessionId: attachRef.current?.browserSessionId,
|
|
25
|
+
orgId: attachRef.current?.orgId,
|
|
22
26
|
captures: [...captures.sessions.keys()],
|
|
23
27
|
activeTab: captures.activeTab,
|
|
24
28
|
}),
|
|
25
29
|
),
|
|
30
|
+
defineTool<Record<string, never>>(
|
|
31
|
+
{
|
|
32
|
+
name: 'dispose_browser',
|
|
33
|
+
description:
|
|
34
|
+
'Stop captures, disconnect the current browser, and immediately '
|
|
35
|
+
+ 'delete a Builder-hosted Popcorn session. Custom CDP sessions '
|
|
36
|
+
+ 'are disconnected but never deleted by Reclaim.',
|
|
37
|
+
inputSchema: { type: 'object', properties: {} },
|
|
38
|
+
},
|
|
39
|
+
async() => {
|
|
40
|
+
await resetAll(attachRef, captures)
|
|
41
|
+
return { disposed: true }
|
|
42
|
+
},
|
|
43
|
+
),
|
|
26
44
|
defineTool<Record<string, never>>(
|
|
27
45
|
{
|
|
28
46
|
name: 'reset_session',
|
|
@@ -32,14 +50,17 @@ export function sessionTools(
|
|
|
32
50
|
inputSchema: { type: 'object', properties: {} },
|
|
33
51
|
},
|
|
34
52
|
async() => {
|
|
35
|
-
|
|
36
|
-
await entry.session.stop().catch(() => {})
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
captures.sessions.clear()
|
|
40
|
-
delete captures.activeTab
|
|
53
|
+
await resetAll(attachRef, captures)
|
|
41
54
|
return { reset: true }
|
|
42
55
|
},
|
|
43
56
|
),
|
|
44
57
|
]
|
|
45
58
|
}
|
|
59
|
+
|
|
60
|
+
async function resetAll(
|
|
61
|
+
attachRef: { current?: AttachState, config: ConfigStore },
|
|
62
|
+
captures: CaptureStore,
|
|
63
|
+
) {
|
|
64
|
+
await resetCaptures(captures)
|
|
65
|
+
await disposeAttached(attachRef)
|
|
66
|
+
}
|
|
@@ -54,19 +54,14 @@ function withLocalOprfMode(provider: ReclaimProvider): ReclaimProvider {
|
|
|
54
54
|
* captured requests, prove via the in-process attestor, replay by re-fetch. */
|
|
55
55
|
function localBackend(entry: CaptureEntry): AuthoringBackend {
|
|
56
56
|
const { tabClient, session, drafts } = entry
|
|
57
|
-
// The per-tab CDP client is strongly typed to `keyof Commands`; the shared
|
|
58
|
-
// tools drive it with dynamic method/event names, so view it as string-keyed.
|
|
59
|
-
const client = tabClient as unknown as {
|
|
60
|
-
send: (method: string, params: object) => Promise<unknown>
|
|
61
|
-
once: (e: string, cb: () => void) => void
|
|
62
|
-
removeListener: (e: string, cb: () => void) => void
|
|
63
|
-
}
|
|
64
57
|
|
|
65
58
|
return {
|
|
66
59
|
capture: session,
|
|
67
60
|
drafts,
|
|
68
|
-
sendCdp: (method, params) =>
|
|
61
|
+
sendCdp: (method, params) => tabClient.send(method, params ?? {}),
|
|
69
62
|
waitForEvent: (method, timeoutMs) => new Promise((resolve) => {
|
|
63
|
+
let stopListening = () => {}
|
|
64
|
+
|
|
70
65
|
const onEvent = () => {
|
|
71
66
|
cleanup()
|
|
72
67
|
resolve('fired')
|
|
@@ -78,10 +73,10 @@ function localBackend(entry: CaptureEntry): AuthoringBackend {
|
|
|
78
73
|
}, timeoutMs)
|
|
79
74
|
const cleanup = () => {
|
|
80
75
|
clearTimeout(timer)
|
|
81
|
-
|
|
76
|
+
stopListening()
|
|
82
77
|
}
|
|
83
78
|
|
|
84
|
-
|
|
79
|
+
stopListening = tabClient.subscribeOnce(method, onEvent)
|
|
85
80
|
}),
|
|
86
81
|
replay: async(draft, opts) => {
|
|
87
82
|
// Auth-bound check: re-fetch with the secrets dropped. replayProvider
|
|
@@ -9,13 +9,23 @@ import { writeSession } from './session-cache.ts'
|
|
|
9
9
|
const SCOPE = 'openid profile email'
|
|
10
10
|
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000
|
|
11
11
|
|
|
12
|
-
export function authenticateTool(
|
|
12
|
+
export function authenticateTool(
|
|
13
|
+
client: ReclaimClient,
|
|
14
|
+
name: 'reclaim_authenticate' | 'authenticate_builder' =
|
|
15
|
+
'reclaim_authenticate',
|
|
16
|
+
): RegisteredTool {
|
|
13
17
|
return defineTool<Record<string, never>>(
|
|
14
18
|
{
|
|
15
|
-
name
|
|
19
|
+
name,
|
|
16
20
|
description:
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
(name === 'authenticate_builder'
|
|
22
|
+
? 'Authenticate specifically with Builder. Available regardless '
|
|
23
|
+
+ 'of '
|
|
24
|
+
+ 'the selected publishing backend. '
|
|
25
|
+
: '')
|
|
26
|
+
+ 'Authorize the CLI / MCP to make authenticated API calls. Opens '
|
|
27
|
+
+ 'the '
|
|
28
|
+
+ 'browser to the builder sign-in page (standard OAuth 2.0 ' +
|
|
19
29
|
'authorization-code + PKCE with a localhost redirect) and waits for ' +
|
|
20
30
|
'the user to sign in and approve. On success the access token is ' +
|
|
21
31
|
'cached in ~/.reclaim/config.json and activated for this session.\n\n' +
|