@thinkrun/mcp 0.3.5
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 +356 -0
- package/dist/bin/thinkrun-mcp.d.ts +3 -0
- package/dist/bin/thinkrun-mcp.d.ts.map +1 -0
- package/dist/bin/thinkrun-mcp.js +305 -0
- package/dist/bin/thinkrun-mcp.js.map +1 -0
- package/dist/src/client.d.ts +422 -0
- package/dist/src/client.d.ts.map +1 -0
- package/dist/src/client.js +2 -0
- package/dist/src/client.js.map +1 -0
- package/dist/src/cloud-client.d.ts +69 -0
- package/dist/src/cloud-client.d.ts.map +1 -0
- package/dist/src/cloud-client.js +291 -0
- package/dist/src/cloud-client.js.map +1 -0
- package/dist/src/extension-proxy-client.d.ts +139 -0
- package/dist/src/extension-proxy-client.d.ts.map +1 -0
- package/dist/src/extension-proxy-client.js +451 -0
- package/dist/src/extension-proxy-client.js.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/local-client.d.ts +120 -0
- package/dist/src/local-client.d.ts.map +1 -0
- package/dist/src/local-client.js +947 -0
- package/dist/src/local-client.js.map +1 -0
- package/dist/src/local-locks.d.ts +26 -0
- package/dist/src/local-locks.d.ts.map +1 -0
- package/dist/src/local-locks.js +315 -0
- package/dist/src/local-locks.js.map +1 -0
- package/dist/src/package-version.d.ts +2 -0
- package/dist/src/package-version.d.ts.map +1 -0
- package/dist/src/package-version.js +13 -0
- package/dist/src/package-version.js.map +1 -0
- package/dist/src/page-cache-http.d.ts +15 -0
- package/dist/src/page-cache-http.d.ts.map +1 -0
- package/dist/src/page-cache-http.js +44 -0
- package/dist/src/page-cache-http.js.map +1 -0
- package/dist/src/server.d.ts +135 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +1454 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/test-utils/lock-fixtures.d.ts +18 -0
- package/dist/src/test-utils/lock-fixtures.d.ts.map +1 -0
- package/dist/src/test-utils/lock-fixtures.js +33 -0
- package/dist/src/test-utils/lock-fixtures.js.map +1 -0
- package/dist/src/unwrap-evaluate-payload.d.ts +8 -0
- package/dist/src/unwrap-evaluate-payload.d.ts.map +1 -0
- package/dist/src/unwrap-evaluate-payload.js +19 -0
- package/dist/src/unwrap-evaluate-payload.js.map +1 -0
- package/package.json +65 -0
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ThinkRun MCP Server — exposes browser automation as MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Security: Selectors are validated via cssSelector() zod refinement to reject
|
|
5
|
+
* script injection attempts. The server-side also applies JSON.stringify()
|
|
6
|
+
* escaping when embedding selectors in error messages (defense-in-depth).
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
/** Zod schema for CSS selectors — rejects obvious script injection patterns. */
|
|
11
|
+
const cssSelector = z
|
|
12
|
+
.string()
|
|
13
|
+
.refine((s) => !/<script/i.test(s) && !/javascript:/i.test(s) && !/on\w+\s*=/i.test(s), { message: 'Selector contains potentially unsafe content' });
|
|
14
|
+
/** Absolute http(s) URL — matches service expectations for page-cache targets. */
|
|
15
|
+
const absoluteHttpUrl = z.string().refine((s) => {
|
|
16
|
+
try {
|
|
17
|
+
const u = new URL(s);
|
|
18
|
+
return u.protocol === 'http:' || u.protocol === 'https:';
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}, { message: 'Must be an absolute http(s) URL' });
|
|
24
|
+
/**
|
|
25
|
+
* MCP tool input for page-cache `options` — keep aligned with
|
|
26
|
+
* `mech-browser-service/src/routes/cache.ts` (`pageCacheOptionsSchema`).
|
|
27
|
+
*/
|
|
28
|
+
const pageCacheToolOptionsSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
provider: z.enum(['cloud', 'local']).optional(),
|
|
31
|
+
sensitive: z.boolean().optional(),
|
|
32
|
+
fullPage: z.boolean().optional(),
|
|
33
|
+
waitFor: z
|
|
34
|
+
.union([z.enum(['load', 'domcontentloaded', 'networkidle']), z.number().positive()])
|
|
35
|
+
.optional(),
|
|
36
|
+
maxDimension: z.number().positive().optional(),
|
|
37
|
+
viewport: z
|
|
38
|
+
.object({
|
|
39
|
+
width: z.number().positive(),
|
|
40
|
+
height: z.number().positive(),
|
|
41
|
+
})
|
|
42
|
+
.optional(),
|
|
43
|
+
timeout: z.number().positive().optional(),
|
|
44
|
+
proxy: z
|
|
45
|
+
.object({
|
|
46
|
+
server: absoluteHttpUrl.describe('Proxy URL (http or https)'),
|
|
47
|
+
username: z.string().optional(),
|
|
48
|
+
password: z.string().optional(),
|
|
49
|
+
})
|
|
50
|
+
.optional(),
|
|
51
|
+
userAgent: z.string().max(2000).optional(),
|
|
52
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
53
|
+
javascript: z.boolean().optional(),
|
|
54
|
+
adBlocking: z.literal(true).optional(),
|
|
55
|
+
textFormat: z.literal('plain').optional(),
|
|
56
|
+
})
|
|
57
|
+
.optional();
|
|
58
|
+
export const MAX_GET_HTML_CHARS = 200_000;
|
|
59
|
+
export const MAX_SLEEP_MS = 30_000;
|
|
60
|
+
const LOCAL_TOOL_ENRICH_TIMEOUT_MS = 300;
|
|
61
|
+
const LOCAL_TOOL_ENRICH_BRIDGE_TIMEOUT_MS = 150;
|
|
62
|
+
function withTimeout(promise, timeoutMs, label) {
|
|
63
|
+
let timer;
|
|
64
|
+
const timeout = new Promise((_, reject) => {
|
|
65
|
+
timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
66
|
+
});
|
|
67
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
68
|
+
if (timer)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function buildLocalToolErrorResult(client, err) {
|
|
73
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
+
const payload = {
|
|
75
|
+
success: false,
|
|
76
|
+
error: message,
|
|
77
|
+
};
|
|
78
|
+
if (typeof err === 'object' && err !== null) {
|
|
79
|
+
const maybeCode = 'code' in err ? err.code : undefined;
|
|
80
|
+
const maybeHint = 'hint' in err ? err.hint : undefined;
|
|
81
|
+
if (typeof maybeCode === 'string' && maybeCode.length > 0)
|
|
82
|
+
payload.code = maybeCode;
|
|
83
|
+
if (typeof maybeHint === 'string' && maybeHint.length > 0)
|
|
84
|
+
payload.hint = maybeHint;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const diagnostics = await withTimeout(client.getLocalDiagnostics({ bridgeTimeoutMs: LOCAL_TOOL_ENRICH_BRIDGE_TIMEOUT_MS }), LOCAL_TOOL_ENRICH_TIMEOUT_MS, 'local diagnostics enrichment');
|
|
88
|
+
if (typeof diagnostics.code === 'string' && diagnostics.code.length > 0 && !payload.code) {
|
|
89
|
+
payload.code = diagnostics.code;
|
|
90
|
+
}
|
|
91
|
+
if (typeof diagnostics.hint === 'string' && diagnostics.hint.length > 0 && !payload.hint) {
|
|
92
|
+
payload.hint = diagnostics.hint;
|
|
93
|
+
}
|
|
94
|
+
if (diagnostics.success) {
|
|
95
|
+
const continuityState = diagnostics.continuity?.state ?? null;
|
|
96
|
+
const recoveryState = diagnostics.bridge?.recoveryState ?? null;
|
|
97
|
+
const extensionConnected = diagnostics.bridge?.extensionConnected ?? null;
|
|
98
|
+
payload.continuityState = continuityState;
|
|
99
|
+
payload.recoveryState = recoveryState;
|
|
100
|
+
payload.extensionConnected = extensionConnected;
|
|
101
|
+
if (!payload.code) {
|
|
102
|
+
if (recoveryState === 'recovering') {
|
|
103
|
+
payload.code = 'BRIDGE_RECOVERING';
|
|
104
|
+
}
|
|
105
|
+
else if (recoveryState === 'flapping' || recoveryState === 'recently_recovered') {
|
|
106
|
+
payload.code = 'BRIDGE_UNSTABLE';
|
|
107
|
+
}
|
|
108
|
+
else if (extensionConnected === false) {
|
|
109
|
+
payload.code = 'LOCAL_BRIDGE_DISCONNECTED';
|
|
110
|
+
}
|
|
111
|
+
else if (continuityState === 'stale_reclaimable') {
|
|
112
|
+
payload.code = 'STALE_RECLAIMABLE';
|
|
113
|
+
}
|
|
114
|
+
else if (continuityState === 'foreign_controller_live') {
|
|
115
|
+
payload.code = 'FOREIGN_CONTROLLER_LIVE';
|
|
116
|
+
}
|
|
117
|
+
else if (continuityState === 'same_controller_read_resumable'
|
|
118
|
+
|| continuityState === 'same_controller_missing_runtime_registration'
|
|
119
|
+
|| continuityState === 'same_controller_missing_mutating_authority_state') {
|
|
120
|
+
payload.code = 'SAME_CONTROLLER_CONTINUITY_REPAIRABLE';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!payload.hint) {
|
|
124
|
+
if (payload.code === 'STALE_RECLAIMABLE') {
|
|
125
|
+
payload.hint = 'Run local_diagnostics, confirm the stale lock, then re-attach to reclaim the tab safely.';
|
|
126
|
+
}
|
|
127
|
+
else if (payload.code === 'FOREIGN_CONTROLLER_LIVE') {
|
|
128
|
+
payload.hint = 'Another local controller still owns this tab. Use a different tab/window or wait for release.';
|
|
129
|
+
}
|
|
130
|
+
else if (payload.code === 'SAME_CONTROLLER_CONTINUITY_REPAIRABLE') {
|
|
131
|
+
payload.hint = 'Same-controller continuity still exists. Run local_diagnostics, then re-attach to repair the runtime binding.';
|
|
132
|
+
}
|
|
133
|
+
else if (payload.code === 'BRIDGE_RECOVERING') {
|
|
134
|
+
payload.hint = 'The local bridge is reconnecting automatically. Wait a few seconds, then retry.';
|
|
135
|
+
}
|
|
136
|
+
else if (payload.code === 'BRIDGE_UNSTABLE') {
|
|
137
|
+
payload.hint = 'The local bridge is unstable after repeated disconnects. Stabilize the browser or reload the extension before retrying.';
|
|
138
|
+
}
|
|
139
|
+
else if (payload.code === 'LOCAL_BRIDGE_DISCONNECTED') {
|
|
140
|
+
payload.hint = 'Reload the ThinkRun extension in your Chromium-based browser, then retry.';
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// best-effort enrichment only
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Apply the PRD's local-session mutation policy without requiring tool registration.
|
|
155
|
+
* This is exported so tests can lock the behavior down before the MCP tools are added.
|
|
156
|
+
*/
|
|
157
|
+
export function applyLifecycleDefaultSession(args) {
|
|
158
|
+
switch (args.operation) {
|
|
159
|
+
case 'tab_attach':
|
|
160
|
+
case 'window_new':
|
|
161
|
+
return args.resultSessionId ?? args.currentDefaultSessionId;
|
|
162
|
+
case 'tab_switch':
|
|
163
|
+
case 'tab_focus':
|
|
164
|
+
return args.currentDefaultSessionId;
|
|
165
|
+
case 'session_release':
|
|
166
|
+
if (!args.currentDefaultSessionId)
|
|
167
|
+
return undefined;
|
|
168
|
+
if (!args.releasedSessionId)
|
|
169
|
+
return args.currentDefaultSessionId;
|
|
170
|
+
return args.currentDefaultSessionId === args.releasedSessionId
|
|
171
|
+
? undefined
|
|
172
|
+
: args.currentDefaultSessionId;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* clear_logs tool handler logic. Exported for unit tests so we can assert
|
|
177
|
+
* on content/isError without going through the MCP transport.
|
|
178
|
+
* Catches rejections from client.clearLogs (e.g. network or unexpected API errors)
|
|
179
|
+
* and returns a structured ToolResult so the MCP tool always responds with content/isError.
|
|
180
|
+
*/
|
|
181
|
+
export async function handleClearLogs(client, args, sidResolver) {
|
|
182
|
+
try {
|
|
183
|
+
const result = await client.clearLogs(sidResolver(args));
|
|
184
|
+
return {
|
|
185
|
+
content: [{
|
|
186
|
+
type: 'text',
|
|
187
|
+
text: result.success ? 'Logs cleared.' : (result.error || 'Failed to clear logs'),
|
|
188
|
+
}],
|
|
189
|
+
isError: !result.success,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
194
|
+
return {
|
|
195
|
+
content: [{ type: 'text', text: message }],
|
|
196
|
+
isError: true,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export async function handleTabAttach(client, args, defaultSessionRef) {
|
|
201
|
+
try {
|
|
202
|
+
const result = await client.attachToTab(args.tabId);
|
|
203
|
+
defaultSessionRef.current = applyLifecycleDefaultSession({
|
|
204
|
+
currentDefaultSessionId: defaultSessionRef.current,
|
|
205
|
+
operation: 'tab_attach',
|
|
206
|
+
resultSessionId: result.sessionId,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
210
|
+
isError: false,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
return buildLocalToolErrorResult(client, err);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export async function handleTabSwitch(client, args, defaultSessionRef) {
|
|
218
|
+
try {
|
|
219
|
+
const result = await client.switchToTab(args.tabId);
|
|
220
|
+
defaultSessionRef.current = applyLifecycleDefaultSession({
|
|
221
|
+
currentDefaultSessionId: defaultSessionRef.current,
|
|
222
|
+
operation: 'tab_switch',
|
|
223
|
+
});
|
|
224
|
+
return {
|
|
225
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, tabId: args.tabId }, null, 2) }],
|
|
226
|
+
isError: !result.success,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
return buildLocalToolErrorResult(client, err);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
export async function handleWindowNew(client, args, defaultSessionRef) {
|
|
234
|
+
try {
|
|
235
|
+
const result = await client.openNewWindow(args.url);
|
|
236
|
+
defaultSessionRef.current = applyLifecycleDefaultSession({
|
|
237
|
+
currentDefaultSessionId: defaultSessionRef.current,
|
|
238
|
+
operation: 'window_new',
|
|
239
|
+
resultSessionId: result.sessionId,
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
243
|
+
isError: false,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
catch (err) {
|
|
247
|
+
return buildLocalToolErrorResult(client, err);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Matches the full local session ID format `local-<tabId>` (e.g. `local-656856474`).
|
|
251
|
+
// Anchored to prevent false positives on cloud session IDs that end in digits
|
|
252
|
+
// (e.g. `remote-session-42` would match the unanchored form and produce a tab ID).
|
|
253
|
+
const LOCAL_SESSION_TAB_ID_RE = /^local-(\d+)$/;
|
|
254
|
+
function extractLocalTabIdFromSessionId(sessionId) {
|
|
255
|
+
// Local MCP session IDs are expected to end in the underlying tab ID.
|
|
256
|
+
// `focus` depends on this convention to foreground the bound local tab
|
|
257
|
+
// without rebinding default session state.
|
|
258
|
+
const match = sessionId.match(LOCAL_SESSION_TAB_ID_RE);
|
|
259
|
+
if (!match)
|
|
260
|
+
return undefined;
|
|
261
|
+
const parsed = Number(match[1]);
|
|
262
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
263
|
+
}
|
|
264
|
+
export async function handleTabFocus(client, args, sidResolver, defaultSessionRef) {
|
|
265
|
+
try {
|
|
266
|
+
const sessionId = sidResolver(args);
|
|
267
|
+
const tabId = extractLocalTabIdFromSessionId(sessionId);
|
|
268
|
+
if (!tabId) {
|
|
269
|
+
return {
|
|
270
|
+
content: [{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: JSON.stringify({ success: false, error: `Cannot derive local tab ID from session ${sessionId}` }, null, 2),
|
|
273
|
+
}],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const result = await client.switchToTab(tabId);
|
|
278
|
+
defaultSessionRef.current = applyLifecycleDefaultSession({
|
|
279
|
+
currentDefaultSessionId: defaultSessionRef.current,
|
|
280
|
+
operation: 'tab_focus',
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, tabId }, null, 2) }],
|
|
284
|
+
isError: !result.success,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
return buildLocalToolErrorResult(client, err);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
export async function handleSessionRelease(client, args, defaultSessionRef) {
|
|
292
|
+
try {
|
|
293
|
+
const releasedSessionId = args.sessionId ?? defaultSessionRef.current;
|
|
294
|
+
const result = await client.releaseSession(releasedSessionId);
|
|
295
|
+
if (result.success) {
|
|
296
|
+
defaultSessionRef.current = applyLifecycleDefaultSession({
|
|
297
|
+
currentDefaultSessionId: defaultSessionRef.current,
|
|
298
|
+
operation: 'session_release',
|
|
299
|
+
releasedSessionId,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: 'text', text: JSON.stringify({ ...result, releasedSessionId: releasedSessionId ?? null }, null, 2) }],
|
|
304
|
+
isError: !result.success,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
return buildLocalToolErrorResult(client, err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
export async function handleLocalActionRun(client, args) {
|
|
312
|
+
try {
|
|
313
|
+
const result = await client.runLocalAction({
|
|
314
|
+
instruction: args.instruction,
|
|
315
|
+
maxIterations: args.maxIterations,
|
|
316
|
+
});
|
|
317
|
+
return {
|
|
318
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
319
|
+
isError: !result.success,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
return buildLocalToolErrorResult(client, err);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
export async function handleLocalActionStatus(client, args) {
|
|
327
|
+
try {
|
|
328
|
+
const result = await client.getLocalActionStatus(args.actionId);
|
|
329
|
+
return {
|
|
330
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
331
|
+
isError: !result.success,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return buildLocalToolErrorResult(client, err);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
export async function handleLocalActionCancel(client, args) {
|
|
339
|
+
try {
|
|
340
|
+
const result = await client.cancelLocalAction(args.actionId);
|
|
341
|
+
return {
|
|
342
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
343
|
+
isError: !result.success,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
return buildLocalToolErrorResult(client, err);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export async function handleLocalDiagnostics(client) {
|
|
351
|
+
try {
|
|
352
|
+
const result = await client.getLocalDiagnostics();
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
355
|
+
isError: !result.success,
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
// Intentionally avoid buildLocalToolErrorResult() here to prevent recursive diagnostics calls.
|
|
360
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: 'text', text: JSON.stringify({ success: false, error: message }, null, 2) }],
|
|
363
|
+
isError: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
export async function handleLocalResetConnection(client) {
|
|
368
|
+
try {
|
|
369
|
+
const result = await client.resetLocalConnection();
|
|
370
|
+
return {
|
|
371
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
372
|
+
isError: !result.success,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
// Intentionally avoid buildLocalToolErrorResult() here to prevent recursive diagnostics calls.
|
|
377
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: 'text', text: JSON.stringify({ success: false, error: message }, null, 2) }],
|
|
380
|
+
isError: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
export async function handleGetUrl(client, args, sidResolver) {
|
|
385
|
+
try {
|
|
386
|
+
const result = await client.evaluate(sidResolver(args), { script: 'window.location.href' });
|
|
387
|
+
const hasValue = result.result !== undefined && result.result !== null;
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: 'text', text: String(result.result ?? '') }],
|
|
390
|
+
isError: !result.success || !hasValue,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (err) {
|
|
394
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
395
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
export async function handleGetTitle(client, args, sidResolver) {
|
|
399
|
+
try {
|
|
400
|
+
const result = await client.evaluate(sidResolver(args), { script: 'document.title' });
|
|
401
|
+
const hasValue = result.result !== undefined && result.result !== null;
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: 'text', text: String(result.result ?? '') }],
|
|
404
|
+
isError: !result.success || !hasValue,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
catch (err) {
|
|
408
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
409
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
export async function handleGetHtml(client, args, sidResolver) {
|
|
413
|
+
try {
|
|
414
|
+
const result = await client.evaluate(sidResolver(args), { script: 'document.documentElement.outerHTML' });
|
|
415
|
+
const hasValue = result.result !== undefined && result.result !== null;
|
|
416
|
+
const html = String(result.result ?? '');
|
|
417
|
+
const truncated = html.length > MAX_GET_HTML_CHARS;
|
|
418
|
+
const payload = truncated
|
|
419
|
+
? {
|
|
420
|
+
html: html.slice(0, MAX_GET_HTML_CHARS),
|
|
421
|
+
truncated: true,
|
|
422
|
+
originalLength: html.length,
|
|
423
|
+
returnedLength: MAX_GET_HTML_CHARS,
|
|
424
|
+
}
|
|
425
|
+
: {
|
|
426
|
+
html,
|
|
427
|
+
truncated: false,
|
|
428
|
+
originalLength: html.length,
|
|
429
|
+
returnedLength: html.length,
|
|
430
|
+
};
|
|
431
|
+
return {
|
|
432
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
433
|
+
isError: !result.success || !hasValue,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
438
|
+
return { content: [{ type: 'text', text: message }], isError: true };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
export async function handleSleep(args) {
|
|
442
|
+
if (args.durationMs < 0 || args.durationMs > MAX_SLEEP_MS) {
|
|
443
|
+
return {
|
|
444
|
+
content: [{
|
|
445
|
+
type: 'text',
|
|
446
|
+
text: JSON.stringify({
|
|
447
|
+
success: false,
|
|
448
|
+
error: `durationMs must be between 0 and ${MAX_SLEEP_MS}`,
|
|
449
|
+
}, null, 2),
|
|
450
|
+
}],
|
|
451
|
+
isError: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
await new Promise((resolve) => setTimeout(resolve, args.durationMs));
|
|
455
|
+
return {
|
|
456
|
+
content: [{
|
|
457
|
+
type: 'text',
|
|
458
|
+
text: JSON.stringify({ success: true, durationMs: args.durationMs }, null, 2),
|
|
459
|
+
}],
|
|
460
|
+
isError: false,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const READY_STATUSES = new Set(['ready', 'running', 'active']);
|
|
464
|
+
export function createServer(client, options) {
|
|
465
|
+
const defaultSid = options?.defaultSessionId;
|
|
466
|
+
const defaultSessionRef = options?.defaultSessionRef;
|
|
467
|
+
const sessionStateRef = defaultSessionRef ?? { current: defaultSid };
|
|
468
|
+
const clientRef = options?.clientRef;
|
|
469
|
+
const onSetMode = options?.onSetMode;
|
|
470
|
+
// Both clientRef and onSetMode must be provided together — set_mode is skipped otherwise
|
|
471
|
+
if (onSetMode && !clientRef) {
|
|
472
|
+
console.error('[thinkrun-mcp] Warning: onSetMode provided without clientRef — set_mode will not be registered');
|
|
473
|
+
}
|
|
474
|
+
// All tool handlers use c() to get the current client — supports runtime switching via set_mode.
|
|
475
|
+
// The swap is not atomic with in-flight tool calls. MCP stdio uses sequential request/response,
|
|
476
|
+
// so concurrent calls are not expected, but future transports may allow pipelining.
|
|
477
|
+
const c = clientRef ? () => clientRef.current : () => client;
|
|
478
|
+
const requiredSessionIdSchema = z.string().describe('The session ID');
|
|
479
|
+
const defaultableSessionIdSchema = z.string().optional().describe('The session ID (auto-injected if omitted when a default session exists)');
|
|
480
|
+
/** Resolve session ID: use provided value, then ref, then initial default. */
|
|
481
|
+
function sid(args) {
|
|
482
|
+
const id = args.sessionId ?? sessionStateRef.current;
|
|
483
|
+
if (!id)
|
|
484
|
+
throw new Error('sessionId is required (no default session configured)');
|
|
485
|
+
return id;
|
|
486
|
+
}
|
|
487
|
+
const effectiveDefault = sessionStateRef.current;
|
|
488
|
+
const server = new McpServer({ name: 'thinkrun', version: '0.1.0' }, {
|
|
489
|
+
capabilities: { tools: {} },
|
|
490
|
+
instructions: effectiveDefault
|
|
491
|
+
? 'ThinkRun MCP server provides browser automation tools. ' +
|
|
492
|
+
`A browser session is already active (ID: ${effectiveDefault}). ` +
|
|
493
|
+
'Use navigation and interaction tools directly — no need to create a session. ' +
|
|
494
|
+
'Always include a "thought" parameter to document your reasoning.'
|
|
495
|
+
: 'ThinkRun MCP server provides browser automation tools. ' +
|
|
496
|
+
'Create a session first, then use navigation and interaction tools. ' +
|
|
497
|
+
'Always include a "thought" parameter to document your reasoning.',
|
|
498
|
+
});
|
|
499
|
+
// ================================================================
|
|
500
|
+
// Session lifecycle
|
|
501
|
+
// ================================================================
|
|
502
|
+
server.registerTool('session_create', {
|
|
503
|
+
title: 'Create Browser Session',
|
|
504
|
+
description: 'Create a new browser session. Returns a sessionId to use with all other tools. ' +
|
|
505
|
+
'In cloud mode, this provisions a dedicated browser machine.',
|
|
506
|
+
inputSchema: {
|
|
507
|
+
initialPrompt: z.string().optional().describe('Describe what you plan to do in this session'),
|
|
508
|
+
viewportWidth: z.number().optional().describe('Viewport width in pixels (default: 1280)'),
|
|
509
|
+
viewportHeight: z.number().optional().describe('Viewport height in pixels (default: 720)'),
|
|
510
|
+
},
|
|
511
|
+
}, async (args) => {
|
|
512
|
+
const options = {
|
|
513
|
+
initialPrompt: args.initialPrompt,
|
|
514
|
+
...(args.viewportWidth || args.viewportHeight ? {
|
|
515
|
+
options: {
|
|
516
|
+
viewport: {
|
|
517
|
+
width: args.viewportWidth || 1280,
|
|
518
|
+
height: args.viewportHeight || 720,
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
} : {}),
|
|
522
|
+
};
|
|
523
|
+
const result = await c().createSession(options);
|
|
524
|
+
return {
|
|
525
|
+
content: [{
|
|
526
|
+
type: 'text',
|
|
527
|
+
text: JSON.stringify(result, null, 2),
|
|
528
|
+
}],
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
server.registerTool('session_status', {
|
|
532
|
+
title: 'Get Session Status',
|
|
533
|
+
description: 'Get the current status of a browser session including URL, title, and action count.',
|
|
534
|
+
inputSchema: {
|
|
535
|
+
sessionId: defaultableSessionIdSchema,
|
|
536
|
+
},
|
|
537
|
+
}, async (args) => {
|
|
538
|
+
const result = await c().getSession(sid(args));
|
|
539
|
+
return {
|
|
540
|
+
content: [{
|
|
541
|
+
type: 'text',
|
|
542
|
+
text: JSON.stringify(result, null, 2),
|
|
543
|
+
}],
|
|
544
|
+
};
|
|
545
|
+
});
|
|
546
|
+
server.registerTool('session_delete', {
|
|
547
|
+
title: 'Delete Browser Session',
|
|
548
|
+
description: 'Terminate a browser session and clean up resources. Artifacts are preserved.',
|
|
549
|
+
inputSchema: {
|
|
550
|
+
sessionId: defaultableSessionIdSchema,
|
|
551
|
+
},
|
|
552
|
+
}, async (args) => {
|
|
553
|
+
const result = await c().deleteSession(sid(args));
|
|
554
|
+
return {
|
|
555
|
+
content: [{
|
|
556
|
+
type: 'text',
|
|
557
|
+
text: JSON.stringify(result, null, 2),
|
|
558
|
+
}],
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
server.registerTool('session_list', {
|
|
562
|
+
title: 'List Browser Sessions',
|
|
563
|
+
description: 'List all active browser sessions for this user.',
|
|
564
|
+
}, async () => {
|
|
565
|
+
const result = await c().listSessions();
|
|
566
|
+
return {
|
|
567
|
+
content: [{
|
|
568
|
+
type: 'text',
|
|
569
|
+
text: JSON.stringify(result, null, 2),
|
|
570
|
+
}],
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
server.registerTool('session_artifacts', {
|
|
574
|
+
title: 'List Session Artifacts',
|
|
575
|
+
description: 'List artifacts (screenshots, recording) for a session. ' +
|
|
576
|
+
'Cloud mode only; returns presigned URLs when available.',
|
|
577
|
+
inputSchema: {
|
|
578
|
+
sessionId: defaultableSessionIdSchema,
|
|
579
|
+
},
|
|
580
|
+
}, async (args) => {
|
|
581
|
+
const result = await c().listArtifacts(sid(args));
|
|
582
|
+
return {
|
|
583
|
+
content: [{
|
|
584
|
+
type: 'text',
|
|
585
|
+
text: JSON.stringify(result, null, 2),
|
|
586
|
+
}],
|
|
587
|
+
isError: !result.success,
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
server.registerTool('session_wait_ready', {
|
|
591
|
+
title: 'Wait for Session Ready',
|
|
592
|
+
description: 'Poll session status until the session is ready (running/active) or timeout. ' +
|
|
593
|
+
'Use after session_create in cloud mode when the session may still be provisioning.',
|
|
594
|
+
inputSchema: {
|
|
595
|
+
sessionId: defaultableSessionIdSchema,
|
|
596
|
+
timeoutMs: z.number().min(1000).max(300000).optional()
|
|
597
|
+
.describe('Max wait in milliseconds (default: 60000, max: 300000)'),
|
|
598
|
+
pollIntervalMs: z.number().min(500).max(10000).optional()
|
|
599
|
+
.describe('Poll interval in ms (default: 2000)'),
|
|
600
|
+
},
|
|
601
|
+
}, async (args) => {
|
|
602
|
+
const sessionId = sid(args);
|
|
603
|
+
const timeoutMs = args.timeoutMs ?? 60000;
|
|
604
|
+
const pollIntervalMs = args.pollIntervalMs ?? 2000;
|
|
605
|
+
const deadline = Date.now() + timeoutMs;
|
|
606
|
+
let last = { status: '' };
|
|
607
|
+
while (Date.now() < deadline) {
|
|
608
|
+
const result = await c().getSession(sessionId);
|
|
609
|
+
last = result;
|
|
610
|
+
if (READY_STATUSES.has(String(result.status).toLowerCase())) {
|
|
611
|
+
return {
|
|
612
|
+
content: [{
|
|
613
|
+
type: 'text',
|
|
614
|
+
text: JSON.stringify({ success: true, status: result.status, url: result.url, title: result.title }, null, 2),
|
|
615
|
+
}],
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
content: [{
|
|
622
|
+
type: 'text',
|
|
623
|
+
text: JSON.stringify({
|
|
624
|
+
success: false,
|
|
625
|
+
error: 'Session did not become ready before timeout',
|
|
626
|
+
lastStatus: last.status,
|
|
627
|
+
url: last.url,
|
|
628
|
+
title: last.title,
|
|
629
|
+
}, null, 2),
|
|
630
|
+
}],
|
|
631
|
+
isError: true,
|
|
632
|
+
};
|
|
633
|
+
});
|
|
634
|
+
server.registerTool('session_use', {
|
|
635
|
+
title: 'Set Default Session',
|
|
636
|
+
description: 'Set the default session for subsequent tool calls so you do not need to pass sessionId every time. ' +
|
|
637
|
+
'Only available when the server was started with a default-session ref (e.g. --session-id).',
|
|
638
|
+
inputSchema: {
|
|
639
|
+
sessionId: z.string().describe('The session ID to use as default'),
|
|
640
|
+
},
|
|
641
|
+
}, async (args) => {
|
|
642
|
+
if (!defaultSessionRef) {
|
|
643
|
+
return {
|
|
644
|
+
content: [{
|
|
645
|
+
type: 'text',
|
|
646
|
+
text: JSON.stringify({
|
|
647
|
+
success: false,
|
|
648
|
+
error: 'Set-default session is not supported (server was not started with a mutable default session)',
|
|
649
|
+
}, null, 2),
|
|
650
|
+
}],
|
|
651
|
+
isError: true,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
sessionStateRef.current = args.sessionId;
|
|
655
|
+
return {
|
|
656
|
+
content: [{
|
|
657
|
+
type: 'text',
|
|
658
|
+
text: JSON.stringify({ success: true, defaultSessionId: args.sessionId }, null, 2),
|
|
659
|
+
}],
|
|
660
|
+
};
|
|
661
|
+
});
|
|
662
|
+
// ================================================================
|
|
663
|
+
// Mode switching (only registered when clientRef + onSetMode provided)
|
|
664
|
+
// ================================================================
|
|
665
|
+
if (clientRef && onSetMode) {
|
|
666
|
+
server.registerTool('set_mode', {
|
|
667
|
+
title: 'Switch Connection Mode',
|
|
668
|
+
description: 'Switch the MCP server between local (Chrome extension) and cloud (ThinkRun API) modes at runtime. ' +
|
|
669
|
+
'Avoids restarting the CLI. Active sessions from the previous mode are not automatically cleaned up. ' +
|
|
670
|
+
'The previous client connection is replaced — ensure any active sessions are deleted first.',
|
|
671
|
+
inputSchema: {
|
|
672
|
+
mode: z.enum(['local', 'cloud', 'auto']).describe('Target mode: "local" for Chrome extension, "cloud" for ThinkRun API, "auto" to detect'),
|
|
673
|
+
},
|
|
674
|
+
}, async (args) => {
|
|
675
|
+
try {
|
|
676
|
+
const result = await onSetMode(args.mode);
|
|
677
|
+
clientRef.current = result.client;
|
|
678
|
+
return {
|
|
679
|
+
content: [{
|
|
680
|
+
type: 'text',
|
|
681
|
+
text: JSON.stringify({ success: true, mode: args.mode, resolvedMode: result.resolvedMode }, null, 2),
|
|
682
|
+
}],
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
687
|
+
return {
|
|
688
|
+
content: [{
|
|
689
|
+
type: 'text',
|
|
690
|
+
text: JSON.stringify({ success: false, error: message, mode: args.mode }, null, 2),
|
|
691
|
+
}],
|
|
692
|
+
isError: true,
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
// ================================================================
|
|
698
|
+
// Stateless page cache (no sessionId)
|
|
699
|
+
// ================================================================
|
|
700
|
+
async function pageCacheToolResult(fn) {
|
|
701
|
+
try {
|
|
702
|
+
const result = await fn();
|
|
703
|
+
const err = !result.success;
|
|
704
|
+
return {
|
|
705
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
706
|
+
isError: err,
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
711
|
+
return {
|
|
712
|
+
content: [{ type: 'text', text: JSON.stringify({ success: false, error: message }, null, 2) }],
|
|
713
|
+
isError: true,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
server.registerTool('page_cache_html', {
|
|
718
|
+
title: 'Page cache — HTML',
|
|
719
|
+
description: 'Stateless: fetch rendered HTML for a URL without a browser session (no sessionId). ' +
|
|
720
|
+
'Calls POST /api/cache/html. Cloud usage may consume credits when the service is configured for billing. ' +
|
|
721
|
+
'Use options.provider "local" or options.sensitive true to avoid third-party fetch when supported.',
|
|
722
|
+
inputSchema: {
|
|
723
|
+
url: absoluteHttpUrl.describe('Page URL'),
|
|
724
|
+
options: pageCacheToolOptionsSchema,
|
|
725
|
+
},
|
|
726
|
+
}, async (args) => pageCacheToolResult(() => c().pageCacheHtml({ url: args.url, options: args.options ?? undefined })));
|
|
727
|
+
server.registerTool('page_cache_text', {
|
|
728
|
+
title: 'Page cache — text',
|
|
729
|
+
description: 'Stateless: extract plain text for a URL without a browser session. POST /api/cache/text. ' +
|
|
730
|
+
'May consume credits in cloud mode.',
|
|
731
|
+
inputSchema: {
|
|
732
|
+
url: absoluteHttpUrl.describe('Page URL'),
|
|
733
|
+
options: pageCacheToolOptionsSchema,
|
|
734
|
+
},
|
|
735
|
+
}, async (args) => pageCacheToolResult(() => c().pageCacheText({ url: args.url, options: args.options ?? undefined })));
|
|
736
|
+
server.registerTool('page_cache_screenshot', {
|
|
737
|
+
title: 'Page cache — screenshot',
|
|
738
|
+
description: 'Stateless: capture a PNG for a URL without a session. Returns JSON with artifactUrl when Accept is application/json. ' +
|
|
739
|
+
'Screenshot path may use more credits than HTML/text.',
|
|
740
|
+
inputSchema: {
|
|
741
|
+
url: absoluteHttpUrl.describe('Page URL'),
|
|
742
|
+
options: pageCacheToolOptionsSchema,
|
|
743
|
+
},
|
|
744
|
+
}, async (args) => pageCacheToolResult(() => c().pageCacheScreenshot({ url: args.url, options: args.options ?? undefined })));
|
|
745
|
+
// ================================================================
|
|
746
|
+
// Navigation
|
|
747
|
+
// ================================================================
|
|
748
|
+
server.registerTool('navigate', {
|
|
749
|
+
title: 'Navigate to URL',
|
|
750
|
+
description: 'Navigate the browser to a URL. Waits for page load before returning. ' +
|
|
751
|
+
'Use captureHtml to get the page HTML in the response.',
|
|
752
|
+
inputSchema: {
|
|
753
|
+
sessionId: defaultableSessionIdSchema,
|
|
754
|
+
url: z.string().describe('The URL to navigate to'),
|
|
755
|
+
thought: z.string().optional().describe('Why you are navigating here'),
|
|
756
|
+
waitUntil: z
|
|
757
|
+
.enum(['load', 'domcontentloaded', 'networkidle'])
|
|
758
|
+
.optional()
|
|
759
|
+
.describe('When to consider navigation complete (default: load)'),
|
|
760
|
+
timeout: z.number().optional().describe('Max wait time in milliseconds'),
|
|
761
|
+
captureHtml: z.boolean().optional().describe('Capture page HTML after navigation'),
|
|
762
|
+
},
|
|
763
|
+
}, async (args) => {
|
|
764
|
+
const { sessionId: _sid, ...params } = args;
|
|
765
|
+
const result = await c().navigate(sid(args), params);
|
|
766
|
+
return {
|
|
767
|
+
content: [{
|
|
768
|
+
type: 'text',
|
|
769
|
+
text: JSON.stringify(result, null, 2),
|
|
770
|
+
}],
|
|
771
|
+
};
|
|
772
|
+
});
|
|
773
|
+
server.registerTool('go_back', {
|
|
774
|
+
title: 'Go Back',
|
|
775
|
+
description: 'Navigate back to the previous page in browser history.',
|
|
776
|
+
inputSchema: {
|
|
777
|
+
sessionId: defaultableSessionIdSchema,
|
|
778
|
+
},
|
|
779
|
+
}, async (args) => {
|
|
780
|
+
const result = await c().goBack(sid(args));
|
|
781
|
+
return {
|
|
782
|
+
content: [{
|
|
783
|
+
type: 'text',
|
|
784
|
+
text: JSON.stringify(result, null, 2),
|
|
785
|
+
}],
|
|
786
|
+
isError: !result.success,
|
|
787
|
+
};
|
|
788
|
+
});
|
|
789
|
+
server.registerTool('go_forward', {
|
|
790
|
+
title: 'Go Forward',
|
|
791
|
+
description: 'Navigate forward to the next page in browser history.',
|
|
792
|
+
inputSchema: {
|
|
793
|
+
sessionId: defaultableSessionIdSchema,
|
|
794
|
+
},
|
|
795
|
+
}, async (args) => {
|
|
796
|
+
const result = await c().goForward(sid(args));
|
|
797
|
+
return {
|
|
798
|
+
content: [{
|
|
799
|
+
type: 'text',
|
|
800
|
+
text: JSON.stringify(result, null, 2),
|
|
801
|
+
}],
|
|
802
|
+
isError: !result.success,
|
|
803
|
+
};
|
|
804
|
+
});
|
|
805
|
+
server.registerTool('wait_for_element', {
|
|
806
|
+
title: 'Wait for Element',
|
|
807
|
+
description: 'Wait for an element to appear on the page. Useful after navigation or clicking ' +
|
|
808
|
+
'something that triggers dynamic content loading.',
|
|
809
|
+
inputSchema: {
|
|
810
|
+
sessionId: defaultableSessionIdSchema,
|
|
811
|
+
selector: cssSelector.optional().describe('CSS selector to wait for'),
|
|
812
|
+
state: z
|
|
813
|
+
.enum(['visible', 'hidden', 'attached'])
|
|
814
|
+
.optional()
|
|
815
|
+
.describe('Element state to wait for (default: visible)'),
|
|
816
|
+
timeout: z.number().optional().describe('Max wait time in milliseconds (default: 30000)'),
|
|
817
|
+
},
|
|
818
|
+
}, async (args) => {
|
|
819
|
+
const { sessionId: _sid, ...params } = args;
|
|
820
|
+
const result = await c().wait(sid(args), params);
|
|
821
|
+
return {
|
|
822
|
+
content: [{
|
|
823
|
+
type: 'text',
|
|
824
|
+
text: JSON.stringify(result, null, 2),
|
|
825
|
+
}],
|
|
826
|
+
};
|
|
827
|
+
});
|
|
828
|
+
// ================================================================
|
|
829
|
+
// Interaction
|
|
830
|
+
// ================================================================
|
|
831
|
+
server.registerTool('click', {
|
|
832
|
+
title: 'Click Element',
|
|
833
|
+
description: 'Click an element on the page using a CSS selector. ' +
|
|
834
|
+
'Prefer specific selectors like button[data-testid="submit"] over fragile ones like div > button.',
|
|
835
|
+
inputSchema: {
|
|
836
|
+
sessionId: defaultableSessionIdSchema,
|
|
837
|
+
selector: cssSelector.describe('CSS selector of the element to click'),
|
|
838
|
+
thought: z.string().optional().describe('Why you are clicking this element'),
|
|
839
|
+
button: z
|
|
840
|
+
.enum(['left', 'right', 'middle'])
|
|
841
|
+
.optional()
|
|
842
|
+
.describe('Mouse button (default: left)'),
|
|
843
|
+
clickCount: z.number().optional().describe('Number of clicks (2 for double-click)'),
|
|
844
|
+
captureHtml: z.boolean().optional().describe('Capture page HTML after click'),
|
|
845
|
+
},
|
|
846
|
+
}, async (args) => {
|
|
847
|
+
const { sessionId: _sid, ...params } = args;
|
|
848
|
+
const result = await c().click(sid(args), params);
|
|
849
|
+
return {
|
|
850
|
+
content: [{
|
|
851
|
+
type: 'text',
|
|
852
|
+
text: JSON.stringify(result, null, 2),
|
|
853
|
+
}],
|
|
854
|
+
};
|
|
855
|
+
});
|
|
856
|
+
server.registerTool('type_text', {
|
|
857
|
+
title: 'Type Text',
|
|
858
|
+
description: 'Type text into an element (appends to existing text, triggers key events). ' +
|
|
859
|
+
'Use fill instead if you want to replace the current value.',
|
|
860
|
+
inputSchema: {
|
|
861
|
+
sessionId: defaultableSessionIdSchema,
|
|
862
|
+
selector: cssSelector.describe('CSS selector of the input element'),
|
|
863
|
+
text: z.string().describe('Text to type'),
|
|
864
|
+
thought: z.string().optional().describe('Why you are typing this'),
|
|
865
|
+
},
|
|
866
|
+
}, async (args) => {
|
|
867
|
+
const { sessionId: _sid, ...params } = args;
|
|
868
|
+
const result = await c().type(sid(args), params);
|
|
869
|
+
return {
|
|
870
|
+
content: [{
|
|
871
|
+
type: 'text',
|
|
872
|
+
text: JSON.stringify(result, null, 2),
|
|
873
|
+
}],
|
|
874
|
+
};
|
|
875
|
+
});
|
|
876
|
+
server.registerTool('fill', {
|
|
877
|
+
title: 'Fill Input Field',
|
|
878
|
+
description: 'Fill a form field with a value (replaces existing content). ' +
|
|
879
|
+
'Unlike type_text, this clears the field first and sets the value directly.',
|
|
880
|
+
inputSchema: {
|
|
881
|
+
sessionId: defaultableSessionIdSchema,
|
|
882
|
+
selector: cssSelector.describe('CSS selector of the input element'),
|
|
883
|
+
value: z.string().describe('Value to fill in'),
|
|
884
|
+
thought: z.string().optional().describe('Why you are filling this field'),
|
|
885
|
+
},
|
|
886
|
+
}, async (args) => {
|
|
887
|
+
const { sessionId: _sid, ...params } = args;
|
|
888
|
+
const result = await c().fill(sid(args), params);
|
|
889
|
+
return {
|
|
890
|
+
content: [{
|
|
891
|
+
type: 'text',
|
|
892
|
+
text: JSON.stringify(result, null, 2),
|
|
893
|
+
}],
|
|
894
|
+
};
|
|
895
|
+
});
|
|
896
|
+
server.registerTool('press_key', {
|
|
897
|
+
title: 'Press Keyboard Key',
|
|
898
|
+
description: 'Press a keyboard key. Common keys: Enter, Tab, Escape, ArrowDown, ArrowUp, Backspace.',
|
|
899
|
+
inputSchema: {
|
|
900
|
+
sessionId: defaultableSessionIdSchema,
|
|
901
|
+
key: z.string().describe('Key to press (e.g. "Enter", "Tab", "Escape", "ArrowDown")'),
|
|
902
|
+
thought: z.string().optional().describe('Why you are pressing this key'),
|
|
903
|
+
},
|
|
904
|
+
}, async (args) => {
|
|
905
|
+
const { sessionId: _sid, ...params } = args;
|
|
906
|
+
const result = await c().press(sid(args), params);
|
|
907
|
+
return {
|
|
908
|
+
content: [{
|
|
909
|
+
type: 'text',
|
|
910
|
+
text: JSON.stringify(result, null, 2),
|
|
911
|
+
}],
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
server.registerTool('scroll', {
|
|
915
|
+
title: 'Scroll Page',
|
|
916
|
+
description: 'Scroll the page or a specific element. Positive y scrolls down, negative scrolls up.',
|
|
917
|
+
inputSchema: {
|
|
918
|
+
sessionId: defaultableSessionIdSchema,
|
|
919
|
+
selector: cssSelector.optional().describe('CSS selector to scroll within (default: page)'),
|
|
920
|
+
x: z.number().optional().describe('Horizontal scroll pixels'),
|
|
921
|
+
y: z.number().optional().describe('Vertical scroll pixels (positive = down)'),
|
|
922
|
+
thought: z.string().optional().describe('Why you are scrolling'),
|
|
923
|
+
},
|
|
924
|
+
}, async (args) => {
|
|
925
|
+
const { sessionId: _sid, ...params } = args;
|
|
926
|
+
const result = await c().scroll(sid(args), params);
|
|
927
|
+
return {
|
|
928
|
+
content: [{
|
|
929
|
+
type: 'text',
|
|
930
|
+
text: JSON.stringify(result, null, 2),
|
|
931
|
+
}],
|
|
932
|
+
};
|
|
933
|
+
});
|
|
934
|
+
server.registerTool('hover', {
|
|
935
|
+
title: 'Hover Over Element',
|
|
936
|
+
description: 'Hover the mouse over an element. Useful for triggering tooltips or dropdown menus.',
|
|
937
|
+
inputSchema: {
|
|
938
|
+
sessionId: defaultableSessionIdSchema,
|
|
939
|
+
selector: cssSelector.describe('CSS selector of the element to hover'),
|
|
940
|
+
thought: z.string().optional().describe('Why you are hovering'),
|
|
941
|
+
},
|
|
942
|
+
}, async (args) => {
|
|
943
|
+
const { sessionId: _sid, ...params } = args;
|
|
944
|
+
const result = await c().hover(sid(args), params);
|
|
945
|
+
return {
|
|
946
|
+
content: [{
|
|
947
|
+
type: 'text',
|
|
948
|
+
text: JSON.stringify(result, null, 2),
|
|
949
|
+
}],
|
|
950
|
+
};
|
|
951
|
+
});
|
|
952
|
+
server.registerTool('select_option', {
|
|
953
|
+
title: 'Select Dropdown Option',
|
|
954
|
+
description: 'Select an option from a <select> dropdown element.',
|
|
955
|
+
inputSchema: {
|
|
956
|
+
sessionId: defaultableSessionIdSchema,
|
|
957
|
+
selector: cssSelector.describe('CSS selector of the <select> element'),
|
|
958
|
+
value: z.string().describe('Value of the option to select'),
|
|
959
|
+
thought: z.string().optional().describe('Why you are selecting this option'),
|
|
960
|
+
},
|
|
961
|
+
}, async (args) => {
|
|
962
|
+
const { sessionId: _sid, ...params } = args;
|
|
963
|
+
const result = await c().select(sid(args), params);
|
|
964
|
+
return {
|
|
965
|
+
content: [{
|
|
966
|
+
type: 'text',
|
|
967
|
+
text: JSON.stringify(result, null, 2),
|
|
968
|
+
}],
|
|
969
|
+
};
|
|
970
|
+
});
|
|
971
|
+
// ================================================================
|
|
972
|
+
// Observation
|
|
973
|
+
// ================================================================
|
|
974
|
+
server.registerTool('snapshot', {
|
|
975
|
+
title: 'Page Snapshot',
|
|
976
|
+
description: 'Get an accessibility tree snapshot of the current page. This is a lightweight ' +
|
|
977
|
+
'alternative to screenshots that returns structured text representing the page content. ' +
|
|
978
|
+
'Use this to understand page structure before interacting with elements.',
|
|
979
|
+
inputSchema: {
|
|
980
|
+
sessionId: defaultableSessionIdSchema,
|
|
981
|
+
},
|
|
982
|
+
}, async (args) => {
|
|
983
|
+
const result = await c().snapshot(sid(args));
|
|
984
|
+
if (result.snapshot) {
|
|
985
|
+
return {
|
|
986
|
+
content: [{
|
|
987
|
+
type: 'text',
|
|
988
|
+
text: result.snapshot,
|
|
989
|
+
}],
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
return {
|
|
993
|
+
content: [{
|
|
994
|
+
type: 'text',
|
|
995
|
+
text: JSON.stringify(result, null, 2),
|
|
996
|
+
}],
|
|
997
|
+
isError: !result.success,
|
|
998
|
+
};
|
|
999
|
+
});
|
|
1000
|
+
server.registerTool('screenshot', {
|
|
1001
|
+
title: 'Take Screenshot',
|
|
1002
|
+
description: 'Capture a screenshot of the current page. Returns a base64-encoded image. ' +
|
|
1003
|
+
'Use fullPage to capture the entire scrollable page.',
|
|
1004
|
+
inputSchema: {
|
|
1005
|
+
sessionId: defaultableSessionIdSchema,
|
|
1006
|
+
type: z
|
|
1007
|
+
.enum(['png', 'jpeg', 'webp'])
|
|
1008
|
+
.optional()
|
|
1009
|
+
.describe('Image format (default: png)'),
|
|
1010
|
+
fullPage: z.boolean().optional().describe('Capture full scrollable page'),
|
|
1011
|
+
quality: z
|
|
1012
|
+
.number()
|
|
1013
|
+
.min(0)
|
|
1014
|
+
.max(100)
|
|
1015
|
+
.optional()
|
|
1016
|
+
.describe('Image quality for jpeg/webp (0-100)'),
|
|
1017
|
+
maxDimension: z
|
|
1018
|
+
.number()
|
|
1019
|
+
.int()
|
|
1020
|
+
.min(1)
|
|
1021
|
+
.optional()
|
|
1022
|
+
.describe('Resize so the largest dimension does not exceed this value (e.g. 1280)'),
|
|
1023
|
+
},
|
|
1024
|
+
}, async (args) => {
|
|
1025
|
+
const { sessionId: _sid, ...params } = args;
|
|
1026
|
+
const result = await c().screenshot(sid(args), params);
|
|
1027
|
+
if (result.screenshot) {
|
|
1028
|
+
return {
|
|
1029
|
+
content: [{
|
|
1030
|
+
type: 'image',
|
|
1031
|
+
data: result.screenshot,
|
|
1032
|
+
mimeType: `image/${params.type || 'png'}`,
|
|
1033
|
+
}],
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
content: [{
|
|
1038
|
+
type: 'text',
|
|
1039
|
+
text: JSON.stringify(result, null, 2),
|
|
1040
|
+
}],
|
|
1041
|
+
isError: !result.success,
|
|
1042
|
+
};
|
|
1043
|
+
});
|
|
1044
|
+
server.registerTool('extract', {
|
|
1045
|
+
title: 'Extract Page Content',
|
|
1046
|
+
description: 'Extract content from the page. Types: "text" for visible text, "html" for raw HTML, ' +
|
|
1047
|
+
'"dom" for structured DOM, "evaluate" to run a JavaScript expression.',
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
sessionId: defaultableSessionIdSchema,
|
|
1050
|
+
type: z
|
|
1051
|
+
.enum(['text', 'html', 'dom', 'evaluate'])
|
|
1052
|
+
.optional()
|
|
1053
|
+
.describe('Extraction type (default: text)'),
|
|
1054
|
+
selector: cssSelector.optional().describe('CSS selector to scope extraction'),
|
|
1055
|
+
script: z.string().optional().describe('JavaScript expression (when type=evaluate)'),
|
|
1056
|
+
attribute: z.string().optional().describe('HTML attribute to extract'),
|
|
1057
|
+
multiple: z.boolean().optional().describe('Extract from all matching elements'),
|
|
1058
|
+
},
|
|
1059
|
+
}, async (args) => {
|
|
1060
|
+
const { sessionId: _sid, ...params } = args;
|
|
1061
|
+
const result = await c().extract(sid(args), params);
|
|
1062
|
+
const extracted = result.text || result.html
|
|
1063
|
+
|| (result.data != null ? JSON.stringify(result.data, null, 2) : null);
|
|
1064
|
+
const text = extracted ?? JSON.stringify(result, null, 2);
|
|
1065
|
+
return {
|
|
1066
|
+
content: [{ type: 'text', text }],
|
|
1067
|
+
isError: !result.success,
|
|
1068
|
+
};
|
|
1069
|
+
});
|
|
1070
|
+
server.registerTool('evaluate', {
|
|
1071
|
+
title: 'Run JavaScript',
|
|
1072
|
+
description: 'Execute JavaScript in the browser page context. The script runs in the page, ' +
|
|
1073
|
+
'not in Node.js. Simple expressions are auto-wrapped with return.',
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
sessionId: defaultableSessionIdSchema,
|
|
1076
|
+
script: z.string().describe('JavaScript code to execute in the page'),
|
|
1077
|
+
},
|
|
1078
|
+
}, async (args) => {
|
|
1079
|
+
const result = await c().evaluate(sid(args), { script: args.script });
|
|
1080
|
+
let text;
|
|
1081
|
+
if (result.error) {
|
|
1082
|
+
text = result.error;
|
|
1083
|
+
}
|
|
1084
|
+
else if (result.result === undefined) {
|
|
1085
|
+
text = 'undefined';
|
|
1086
|
+
}
|
|
1087
|
+
else if (result.result === null) {
|
|
1088
|
+
text = 'null';
|
|
1089
|
+
}
|
|
1090
|
+
else if (typeof result.result === 'string') {
|
|
1091
|
+
text = result.result;
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
text = JSON.stringify(result.result, null, 2);
|
|
1095
|
+
}
|
|
1096
|
+
return {
|
|
1097
|
+
content: [{ type: 'text', text }],
|
|
1098
|
+
isError: !result.success,
|
|
1099
|
+
};
|
|
1100
|
+
});
|
|
1101
|
+
// ================================================================
|
|
1102
|
+
// Tab management (local mode)
|
|
1103
|
+
// ================================================================
|
|
1104
|
+
server.registerTool('tab_list', {
|
|
1105
|
+
title: 'List Browser Tabs',
|
|
1106
|
+
description: 'List all open browser tabs. Local mode only. ' +
|
|
1107
|
+
'In cloud mode, each session has a single page.',
|
|
1108
|
+
inputSchema: {
|
|
1109
|
+
sessionId: defaultableSessionIdSchema,
|
|
1110
|
+
},
|
|
1111
|
+
}, async (args) => {
|
|
1112
|
+
const result = await c().listTabs(args.sessionId ?? sessionStateRef.current ?? '');
|
|
1113
|
+
return {
|
|
1114
|
+
content: [{
|
|
1115
|
+
type: 'text',
|
|
1116
|
+
text: JSON.stringify(result, null, 2),
|
|
1117
|
+
}],
|
|
1118
|
+
isError: !result.success,
|
|
1119
|
+
};
|
|
1120
|
+
});
|
|
1121
|
+
server.registerTool('tab_new', {
|
|
1122
|
+
title: 'Open New Tab',
|
|
1123
|
+
description: 'Open a new browser tab, optionally navigating to a URL. Local mode only.',
|
|
1124
|
+
inputSchema: {
|
|
1125
|
+
sessionId: defaultableSessionIdSchema,
|
|
1126
|
+
url: z.string().optional().describe('URL to open in the new tab'),
|
|
1127
|
+
},
|
|
1128
|
+
}, async (args) => {
|
|
1129
|
+
try {
|
|
1130
|
+
const result = await c().newTab(args.sessionId ?? sessionStateRef.current ?? '', args.url);
|
|
1131
|
+
return {
|
|
1132
|
+
content: [{
|
|
1133
|
+
type: 'text',
|
|
1134
|
+
text: JSON.stringify(result, null, 2),
|
|
1135
|
+
}],
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
catch (error) {
|
|
1139
|
+
return {
|
|
1140
|
+
content: [{
|
|
1141
|
+
type: 'text',
|
|
1142
|
+
text: JSON.stringify({ success: false, error: String(error) }),
|
|
1143
|
+
}],
|
|
1144
|
+
isError: true,
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
server.registerTool('tab_close', {
|
|
1149
|
+
title: 'Close Browser Tab',
|
|
1150
|
+
description: 'Close a browser tab by ID. If no tabId is provided, closes the current tab. Local mode only.',
|
|
1151
|
+
inputSchema: {
|
|
1152
|
+
sessionId: defaultableSessionIdSchema,
|
|
1153
|
+
tabId: z.number().int().positive().optional().describe('Tab ID to close (default: current tab)'),
|
|
1154
|
+
},
|
|
1155
|
+
}, async (args) => {
|
|
1156
|
+
const result = await c().closeTab(sid(args), args.tabId);
|
|
1157
|
+
return {
|
|
1158
|
+
content: [{
|
|
1159
|
+
type: 'text',
|
|
1160
|
+
text: JSON.stringify(result, null, 2),
|
|
1161
|
+
}],
|
|
1162
|
+
isError: !result.success,
|
|
1163
|
+
};
|
|
1164
|
+
});
|
|
1165
|
+
server.registerTool('tab_attach', {
|
|
1166
|
+
title: 'Attach To Existing Browser Tab',
|
|
1167
|
+
description: 'Attach to an existing local Chrome tab by tab ID and make it the default session. Local mode only.',
|
|
1168
|
+
inputSchema: {
|
|
1169
|
+
tabId: z.number().int().positive().describe('Chrome tab ID to attach to'),
|
|
1170
|
+
},
|
|
1171
|
+
}, (args) => handleTabAttach(c(), args, sessionStateRef));
|
|
1172
|
+
server.registerTool('tab_switch', {
|
|
1173
|
+
title: 'Switch Active Browser Tab',
|
|
1174
|
+
description: 'Switch the active local Chrome tab without rebinding the current default session. Local mode only.',
|
|
1175
|
+
inputSchema: {
|
|
1176
|
+
tabId: z.number().int().positive().describe('Chrome tab ID to switch to'),
|
|
1177
|
+
},
|
|
1178
|
+
}, (args) => handleTabSwitch(c(), args, sessionStateRef));
|
|
1179
|
+
server.registerTool('focus', {
|
|
1180
|
+
title: 'Focus Current Browser Tab',
|
|
1181
|
+
description: 'Bring the currently bound local browser tab/window to the foreground without rebinding the default session. Local mode only.',
|
|
1182
|
+
inputSchema: {
|
|
1183
|
+
sessionId: defaultableSessionIdSchema,
|
|
1184
|
+
},
|
|
1185
|
+
}, (args) => handleTabFocus(c(), args, sid, sessionStateRef));
|
|
1186
|
+
server.registerTool('window_new', {
|
|
1187
|
+
title: 'Open New Browser Window',
|
|
1188
|
+
description: 'Open a new local Chrome window and bind the resulting tab as the default session. Local mode only.',
|
|
1189
|
+
inputSchema: {
|
|
1190
|
+
url: z.string().optional().describe('Optional initial URL for the new window'),
|
|
1191
|
+
},
|
|
1192
|
+
}, (args) => handleWindowNew(c(), args, sessionStateRef));
|
|
1193
|
+
server.registerTool('session_release', {
|
|
1194
|
+
title: 'Release Local Session Ownership',
|
|
1195
|
+
description: 'Release the current local tab ownership without closing the tab. Clears the default session when releasing the bound local session. Local mode only.',
|
|
1196
|
+
inputSchema: {
|
|
1197
|
+
sessionId: z.string().optional().describe('Optional session ID to release (defaults to the current bound local session)'),
|
|
1198
|
+
},
|
|
1199
|
+
}, (args) => handleSessionRelease(c(), args, sessionStateRef));
|
|
1200
|
+
server.registerTool('local_diagnostics', {
|
|
1201
|
+
title: 'Inspect Local Continuity And Bridge State',
|
|
1202
|
+
description: 'Return structured local continuity, ownership, reclaim, and bridge recovery diagnostics. Local mode only.',
|
|
1203
|
+
}, async () => handleLocalDiagnostics(c()));
|
|
1204
|
+
server.registerTool('local_reset_connection', {
|
|
1205
|
+
title: 'Reset Local Circuit Breaker',
|
|
1206
|
+
description: 'Trigger the bounded native-host reset-connection recovery path used for false local circuit-breaker trips. Local mode only.',
|
|
1207
|
+
}, async () => handleLocalResetConnection(c()));
|
|
1208
|
+
server.registerTool('get_url', {
|
|
1209
|
+
title: 'Get Current Page URL',
|
|
1210
|
+
description: 'Return the current page URL for the active session.',
|
|
1211
|
+
inputSchema: {
|
|
1212
|
+
sessionId: defaultableSessionIdSchema,
|
|
1213
|
+
},
|
|
1214
|
+
}, (args) => handleGetUrl(c(), args, sid));
|
|
1215
|
+
server.registerTool('get_title', {
|
|
1216
|
+
title: 'Get Current Page Title',
|
|
1217
|
+
description: 'Return the current page title for the active session.',
|
|
1218
|
+
inputSchema: {
|
|
1219
|
+
sessionId: defaultableSessionIdSchema,
|
|
1220
|
+
},
|
|
1221
|
+
}, (args) => handleGetTitle(c(), args, sid));
|
|
1222
|
+
server.registerTool('get_html', {
|
|
1223
|
+
title: 'Get Current Page HTML',
|
|
1224
|
+
description: 'Return the current page HTML for the active session. Large documents are truncated explicitly in the response.',
|
|
1225
|
+
inputSchema: {
|
|
1226
|
+
sessionId: defaultableSessionIdSchema,
|
|
1227
|
+
},
|
|
1228
|
+
}, (args) => handleGetHtml(c(), args, sid));
|
|
1229
|
+
server.registerTool('sleep', {
|
|
1230
|
+
title: 'Pause Execution',
|
|
1231
|
+
description: `Pause for a bounded duration in milliseconds (max ${MAX_SLEEP_MS}).`,
|
|
1232
|
+
inputSchema: {
|
|
1233
|
+
durationMs: z.number().int().min(0).max(MAX_SLEEP_MS).describe('Pause duration in milliseconds'),
|
|
1234
|
+
},
|
|
1235
|
+
}, handleSleep);
|
|
1236
|
+
// ================================================================
|
|
1237
|
+
// Dialog handling
|
|
1238
|
+
// ================================================================
|
|
1239
|
+
server.registerTool('get_dialog', {
|
|
1240
|
+
title: 'Get Dialog State',
|
|
1241
|
+
description: 'Check for pending browser dialogs (alert, confirm, prompt) and view dialog history. ' +
|
|
1242
|
+
'Use this to detect dialogs that may be blocking page interaction.',
|
|
1243
|
+
inputSchema: {
|
|
1244
|
+
sessionId: defaultableSessionIdSchema,
|
|
1245
|
+
},
|
|
1246
|
+
}, async (args) => {
|
|
1247
|
+
const result = await c().getDialog(sid(args));
|
|
1248
|
+
return {
|
|
1249
|
+
content: [{
|
|
1250
|
+
type: 'text',
|
|
1251
|
+
text: JSON.stringify(result, null, 2),
|
|
1252
|
+
}],
|
|
1253
|
+
isError: !result.success,
|
|
1254
|
+
};
|
|
1255
|
+
});
|
|
1256
|
+
server.registerTool('handle_dialog', {
|
|
1257
|
+
title: 'Handle Dialog',
|
|
1258
|
+
description: 'Accept or dismiss a pending browser dialog (alert, confirm, prompt). ' +
|
|
1259
|
+
'For prompt dialogs, you can provide text to enter. ' +
|
|
1260
|
+
'In local mode, dialogs are auto-handled — this returns dialog history.',
|
|
1261
|
+
inputSchema: {
|
|
1262
|
+
sessionId: defaultableSessionIdSchema,
|
|
1263
|
+
accept: z.boolean().optional().describe('Accept (true) or dismiss (false) the dialog. Default: true'),
|
|
1264
|
+
promptText: z.string().optional().describe('Text to enter for prompt dialogs'),
|
|
1265
|
+
},
|
|
1266
|
+
}, async (args) => {
|
|
1267
|
+
const result = await c().handleDialog(sid(args), {
|
|
1268
|
+
accept: args.accept,
|
|
1269
|
+
promptText: args.promptText,
|
|
1270
|
+
});
|
|
1271
|
+
return {
|
|
1272
|
+
content: [{
|
|
1273
|
+
type: 'text',
|
|
1274
|
+
text: result.success ? `Dialog ${args.accept !== false ? 'accepted' : 'dismissed'}` : (result.error || 'Failed to handle dialog'),
|
|
1275
|
+
}],
|
|
1276
|
+
isError: !result.success,
|
|
1277
|
+
};
|
|
1278
|
+
});
|
|
1279
|
+
// ================================================================
|
|
1280
|
+
// Wait for text
|
|
1281
|
+
// ================================================================
|
|
1282
|
+
server.registerTool('wait_for_text', {
|
|
1283
|
+
title: 'Wait for Text',
|
|
1284
|
+
description: 'Wait for specific text to appear on the page. ' +
|
|
1285
|
+
'Useful for waiting for loading states, confirmation messages, or dynamic content.',
|
|
1286
|
+
inputSchema: {
|
|
1287
|
+
sessionId: defaultableSessionIdSchema,
|
|
1288
|
+
text: z.string().max(1000).describe('Text to wait for on the page'),
|
|
1289
|
+
timeout: z.number().min(1000).max(60000).optional().describe('Timeout in ms (default 30000, max 60000)'),
|
|
1290
|
+
},
|
|
1291
|
+
}, async (args) => {
|
|
1292
|
+
const result = await c().waitForText(sid(args), {
|
|
1293
|
+
text: args.text,
|
|
1294
|
+
timeout: args.timeout,
|
|
1295
|
+
});
|
|
1296
|
+
return {
|
|
1297
|
+
content: [{
|
|
1298
|
+
type: 'text',
|
|
1299
|
+
text: result.success ? `Found text: "${args.text}"` : (result.error || `Text "${args.text}" not found`),
|
|
1300
|
+
}],
|
|
1301
|
+
isError: !result.success,
|
|
1302
|
+
};
|
|
1303
|
+
});
|
|
1304
|
+
// ================================================================
|
|
1305
|
+
// Monitoring
|
|
1306
|
+
// ================================================================
|
|
1307
|
+
server.registerTool('console_messages', {
|
|
1308
|
+
title: 'Get Console Messages',
|
|
1309
|
+
description: 'Get browser console messages (log, warn, error) from the current session. ' +
|
|
1310
|
+
'Useful for debugging JavaScript errors or checking application state.',
|
|
1311
|
+
inputSchema: {
|
|
1312
|
+
sessionId: defaultableSessionIdSchema,
|
|
1313
|
+
},
|
|
1314
|
+
}, async (args) => {
|
|
1315
|
+
const result = await c().getConsoleMessages(sid(args));
|
|
1316
|
+
if (result.logs && result.logs.length > 0) {
|
|
1317
|
+
const formatted = result.logs
|
|
1318
|
+
.map(l => `[${l.level}] ${l.text}`)
|
|
1319
|
+
.join('\n');
|
|
1320
|
+
return {
|
|
1321
|
+
content: [{
|
|
1322
|
+
type: 'text',
|
|
1323
|
+
text: formatted,
|
|
1324
|
+
}],
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
return {
|
|
1328
|
+
content: [{
|
|
1329
|
+
type: 'text',
|
|
1330
|
+
text: result.error || 'No console messages.',
|
|
1331
|
+
}],
|
|
1332
|
+
isError: !result.success,
|
|
1333
|
+
};
|
|
1334
|
+
});
|
|
1335
|
+
server.registerTool('network_requests', {
|
|
1336
|
+
title: 'Get Network Requests',
|
|
1337
|
+
description: 'Get network requests made by the browser during this session. ' +
|
|
1338
|
+
'Useful for debugging API calls, checking responses, and monitoring traffic.',
|
|
1339
|
+
inputSchema: {
|
|
1340
|
+
sessionId: defaultableSessionIdSchema,
|
|
1341
|
+
},
|
|
1342
|
+
}, async (args) => {
|
|
1343
|
+
const result = await c().getNetworkRequests(sid(args));
|
|
1344
|
+
if (result.requests && result.requests.length > 0) {
|
|
1345
|
+
const formatted = result.requests
|
|
1346
|
+
.map(r => `${r.method} ${r.url} → ${r.status ?? 'pending'}`)
|
|
1347
|
+
.join('\n');
|
|
1348
|
+
return {
|
|
1349
|
+
content: [{
|
|
1350
|
+
type: 'text',
|
|
1351
|
+
text: `${result.count} requests:\n${formatted}`,
|
|
1352
|
+
}],
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
content: [{
|
|
1357
|
+
type: 'text',
|
|
1358
|
+
text: result.error || 'No network requests recorded.',
|
|
1359
|
+
}],
|
|
1360
|
+
isError: !result.success,
|
|
1361
|
+
};
|
|
1362
|
+
});
|
|
1363
|
+
server.registerTool('clear_logs', {
|
|
1364
|
+
title: 'Clear Console and Network Logs',
|
|
1365
|
+
description: 'Clear console and network logs for the session. ' +
|
|
1366
|
+
'Useful after inspecting logs so the next capture is clean.',
|
|
1367
|
+
inputSchema: {
|
|
1368
|
+
sessionId: defaultableSessionIdSchema,
|
|
1369
|
+
},
|
|
1370
|
+
}, (args) => handleClearLogs(c(), args, sid));
|
|
1371
|
+
server.registerTool('local_action_run', {
|
|
1372
|
+
title: 'Run Local Action',
|
|
1373
|
+
description: 'Run a direct native-host action against the currently attached local tab. ' +
|
|
1374
|
+
'Local mode only.',
|
|
1375
|
+
inputSchema: {
|
|
1376
|
+
instruction: z.string().min(1).describe('Natural language instruction for the local browser action'),
|
|
1377
|
+
maxIterations: z.number().int().positive().optional().describe('Optional upper bound for AI iterations'),
|
|
1378
|
+
},
|
|
1379
|
+
}, (args) => handleLocalActionRun(c(), args));
|
|
1380
|
+
server.registerTool('local_action_status', {
|
|
1381
|
+
title: 'Get Local Action Status',
|
|
1382
|
+
description: 'Check the status of a previously started local action. Local mode only.',
|
|
1383
|
+
inputSchema: {
|
|
1384
|
+
actionId: z.string().min(1).describe('The action ID returned from local_action_run'),
|
|
1385
|
+
},
|
|
1386
|
+
}, (args) => handleLocalActionStatus(c(), args));
|
|
1387
|
+
server.registerTool('local_action_cancel', {
|
|
1388
|
+
title: 'Cancel Local Action',
|
|
1389
|
+
description: 'Cancel a running local action. Local mode only.',
|
|
1390
|
+
inputSchema: {
|
|
1391
|
+
actionId: z.string().min(1).describe('The action ID to cancel'),
|
|
1392
|
+
},
|
|
1393
|
+
}, (args) => handleLocalActionCancel(c(), args));
|
|
1394
|
+
// ================================================================
|
|
1395
|
+
// Task planning (cloud only)
|
|
1396
|
+
// ================================================================
|
|
1397
|
+
server.registerTool('task_create', {
|
|
1398
|
+
title: 'Create Task Plan',
|
|
1399
|
+
description: 'Create an AI task plan from natural language. The plan breaks your request into ' +
|
|
1400
|
+
'individual browser actions. Cloud mode only.',
|
|
1401
|
+
inputSchema: {
|
|
1402
|
+
sessionId: defaultableSessionIdSchema,
|
|
1403
|
+
text: z.string().describe('Natural language description of what to do'),
|
|
1404
|
+
url: z.string().optional().describe('Starting URL for the task'),
|
|
1405
|
+
},
|
|
1406
|
+
}, async (args) => {
|
|
1407
|
+
const result = await c().createTask({
|
|
1408
|
+
sessionId: sid(args),
|
|
1409
|
+
text: args.text,
|
|
1410
|
+
url: args.url,
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
content: [{
|
|
1414
|
+
type: 'text',
|
|
1415
|
+
text: JSON.stringify(result, null, 2),
|
|
1416
|
+
}],
|
|
1417
|
+
isError: !result.success,
|
|
1418
|
+
};
|
|
1419
|
+
});
|
|
1420
|
+
server.registerTool('task_execute', {
|
|
1421
|
+
title: 'Execute Task Plan',
|
|
1422
|
+
description: 'Execute a previously created task plan. Cloud mode only.',
|
|
1423
|
+
inputSchema: {
|
|
1424
|
+
planId: z.string().describe('The plan ID returned from task_create'),
|
|
1425
|
+
},
|
|
1426
|
+
}, async (args) => {
|
|
1427
|
+
const result = await c().executeTask(args.planId);
|
|
1428
|
+
return {
|
|
1429
|
+
content: [{
|
|
1430
|
+
type: 'text',
|
|
1431
|
+
text: JSON.stringify(result, null, 2),
|
|
1432
|
+
}],
|
|
1433
|
+
isError: !result.success,
|
|
1434
|
+
};
|
|
1435
|
+
});
|
|
1436
|
+
server.registerTool('task_status', {
|
|
1437
|
+
title: 'Get Task Status',
|
|
1438
|
+
description: 'Check the status of a task plan execution. Cloud mode only.',
|
|
1439
|
+
inputSchema: {
|
|
1440
|
+
planId: z.string().describe('The plan ID to check'),
|
|
1441
|
+
},
|
|
1442
|
+
}, async (args) => {
|
|
1443
|
+
const result = await c().getTaskStatus(args.planId);
|
|
1444
|
+
return {
|
|
1445
|
+
content: [{
|
|
1446
|
+
type: 'text',
|
|
1447
|
+
text: JSON.stringify(result, null, 2),
|
|
1448
|
+
}],
|
|
1449
|
+
isError: !result.success,
|
|
1450
|
+
};
|
|
1451
|
+
});
|
|
1452
|
+
return server;
|
|
1453
|
+
}
|
|
1454
|
+
//# sourceMappingURL=server.js.map
|