mcp-scraper 0.2.19 → 0.2.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.
Files changed (71) hide show
  1. package/README.md +3 -3
  2. package/dist/bin/api-server.cjs +1020 -348
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/browser-agent-stdio-server.cjs +314 -55
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +321 -167
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +3 -2
  11. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +1128 -530
  13. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-scraper-combined-stdio-server.js +5 -4
  15. package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
  16. package/dist/bin/mcp-scraper-install.cjs +3 -3
  17. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  18. package/dist/bin/mcp-scraper-install.js +2 -2
  19. package/dist/bin/mcp-stdio-server.cjs +726 -387
  20. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  21. package/dist/bin/mcp-stdio-server.js +3 -2
  22. package/dist/bin/mcp-stdio-server.js.map +1 -1
  23. package/dist/bin/paa-harvest.cjs +5 -3
  24. package/dist/bin/paa-harvest.cjs.map +1 -1
  25. package/dist/bin/paa-harvest.js +2 -1
  26. package/dist/bin/paa-harvest.js.map +1 -1
  27. package/dist/{chunk-TL7YTFLH.js → chunk-DBQDG7EH.js} +99 -27
  28. package/dist/chunk-DBQDG7EH.js.map +1 -0
  29. package/dist/{chunk-F44RBOJ5.js → chunk-H2R232HK.js} +216 -108
  30. package/dist/chunk-H2R232HK.js.map +1 -0
  31. package/dist/{chunk-76P4DKWR.js → chunk-L4OWOUGR.js} +315 -56
  32. package/dist/chunk-L4OWOUGR.js.map +1 -0
  33. package/dist/chunk-LFATOGDF.js +111 -0
  34. package/dist/chunk-LFATOGDF.js.map +1 -0
  35. package/dist/{chunk-MCBOZ2JF.js → chunk-MLNCKQ2B.js} +3 -3
  36. package/dist/chunk-MLNCKQ2B.js.map +1 -0
  37. package/dist/{chunk-4UEVYYDW.js → chunk-O5J7E4VX.js} +394 -57
  38. package/dist/chunk-O5J7E4VX.js.map +1 -0
  39. package/dist/{chunk-BSYPATSM.js → chunk-SXTXMFEQ.js} +2 -2
  40. package/dist/{chunk-CQTAKXBN.js → chunk-XGUDTDZ2.js} +7 -104
  41. package/dist/chunk-XGUDTDZ2.js.map +1 -0
  42. package/dist/chunk-XKUDVN2E.js +7 -0
  43. package/dist/chunk-XKUDVN2E.js.map +1 -0
  44. package/dist/{db-P5X6UQ3E.js → db-BE4JVB3V.js} +8 -2
  45. package/dist/index.cjs +5 -3
  46. package/dist/index.cjs.map +1 -1
  47. package/dist/index.d.cts +2 -2
  48. package/dist/index.d.ts +2 -2
  49. package/dist/index.js +2 -1
  50. package/dist/index.js.map +1 -1
  51. package/dist/{server-CRUSZA2Q.js → server-7NE45K2A.js} +211 -37
  52. package/dist/server-7NE45K2A.js.map +1 -0
  53. package/dist/{worker-OZSWIS3F.js → worker-MIYG2B2I.js} +5 -4
  54. package/dist/{worker-OZSWIS3F.js.map → worker-MIYG2B2I.js.map} +1 -1
  55. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +2 -2
  56. package/docs/mcp-tool-craft-lint.generated.md +51 -0
  57. package/docs/mcp-tool-manifest.generated.json +1406 -0
  58. package/docs/specs/local-competitive-audit-spec.md +1 -2
  59. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +1602 -0
  60. package/package.json +4 -1
  61. package/dist/chunk-4UEVYYDW.js.map +0 -1
  62. package/dist/chunk-76P4DKWR.js.map +0 -1
  63. package/dist/chunk-CQTAKXBN.js.map +0 -1
  64. package/dist/chunk-F44RBOJ5.js.map +0 -1
  65. package/dist/chunk-MCBOZ2JF.js.map +0 -1
  66. package/dist/chunk-RU2HZD55.js +0 -7
  67. package/dist/chunk-RU2HZD55.js.map +0 -1
  68. package/dist/chunk-TL7YTFLH.js.map +0 -1
  69. package/dist/server-CRUSZA2Q.js.map +0 -1
  70. /package/dist/{chunk-BSYPATSM.js.map → chunk-SXTXMFEQ.js.map} +0 -0
  71. /package/dist/{db-P5X6UQ3E.js.map → db-BE4JVB3V.js.map} +0 -0
@@ -14,10 +14,22 @@ var import_node_os2 = require("os");
14
14
  var import_node_path2 = require("path");
15
15
 
16
16
  // src/version.ts
17
- var PACKAGE_VERSION = "0.2.19";
17
+ var PACKAGE_VERSION = "0.2.20";
18
18
 
19
19
  // src/mcp/browser-agent-tool-schemas.ts
20
20
  var import_zod = require("zod");
21
+ var NullableString = import_zod.z.string().nullable();
22
+ var BrowserRawObject = import_zod.z.record(import_zod.z.unknown());
23
+ var BrowserElementOutput = import_zod.z.record(import_zod.z.unknown());
24
+ var BrowserBaseOutput = {
25
+ ok: import_zod.z.boolean().describe("Whether the browser-agent action succeeded."),
26
+ tool: import_zod.z.string().describe("Browser Agent MCP tool that produced this response."),
27
+ session_id: NullableString.describe("Browser session id when the response is scoped to a session.")
28
+ };
29
+ var BrowserReplayBaseOutput = {
30
+ ...BrowserBaseOutput,
31
+ replay_id: NullableString.describe("Replay id when the response is scoped to a replay.")
32
+ };
21
33
  var BrowserOpenInputSchema = {
22
34
  label: import_zod.z.string().optional().describe("Optional human label for this session, shown in the watch console."),
23
35
  url: import_zod.z.string().url().optional().describe("Optional URL to navigate to immediately after opening."),
@@ -25,7 +37,7 @@ var BrowserOpenInputSchema = {
25
37
  timeout_seconds: import_zod.z.number().int().min(60).max(259200).optional().describe("How long the session may live before auto-termination. Defaults to 600. The browser idles into a zero-cost standby between actions, so a longer timeout is cheap.")
26
38
  };
27
39
  var BrowserSessionInputSchema = {
28
- session_id: import_zod.z.string().describe("The session id returned by browser_open.")
40
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself.")
29
41
  };
30
42
  var BrowserLocateTargetSchema = import_zod.z.object({
31
43
  name: import_zod.z.string().optional().describe("Optional label for this target, echoed in the result."),
@@ -37,43 +49,43 @@ var BrowserLocateTargetSchema = import_zod.z.object({
37
49
  message: "target requires selector or text"
38
50
  });
39
51
  var BrowserLocateInputSchema = {
40
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
52
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
41
53
  targets: import_zod.z.array(BrowserLocateTargetSchema).min(1).max(20).describe("DOM targets to locate in the current viewport. Use selectors for exact elements, or text for visible text ranges.")
42
54
  };
43
55
  var BrowserGotoInputSchema = {
44
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
56
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
45
57
  url: import_zod.z.string().url().describe("URL to navigate the browser to.")
46
58
  };
47
59
  var BrowserClickInputSchema = {
48
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
49
- x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use the x of an element from the latest screenshot."),
50
- y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels."),
60
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
61
+ x: import_zod.z.number().describe("X coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
62
+ y: import_zod.z.number().describe("Y coordinate to click, in screenshot pixels. Use only coordinates from the latest browser_screenshot, browser_read, or browser_locate result; do not guess."),
51
63
  button: import_zod.z.enum(["left", "right", "middle"]).default("left").describe("Mouse button."),
52
64
  num_clicks: import_zod.z.number().int().min(1).max(3).optional().describe("Number of clicks, e.g. 2 for double-click.")
53
65
  };
54
66
  var BrowserTypeInputSchema = {
55
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
67
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
56
68
  text: import_zod.z.string().describe("Text to type at the current focus. Click a field first to focus it."),
57
69
  delay: import_zod.z.number().int().min(0).max(500).optional().describe("Optional per-keystroke delay in ms for human-like typing.")
58
70
  };
59
71
  var BrowserScrollInputSchema = {
60
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
72
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
61
73
  delta_y: import_zod.z.number().default(5).describe("Vertical scroll in wheel units. Positive scrolls down, negative up."),
62
74
  delta_x: import_zod.z.number().default(0).describe("Horizontal scroll in wheel units."),
63
75
  x: import_zod.z.number().optional().describe("X position to scroll at. Defaults to screen center."),
64
76
  y: import_zod.z.number().optional().describe("Y position to scroll at. Defaults to screen center.")
65
77
  };
66
78
  var BrowserPressInputSchema = {
67
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
79
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
68
80
  keys: import_zod.z.array(import_zod.z.string()).min(1).describe('Keys or combinations to press, e.g. ["Return"], ["Ctrl+a"], ["Ctrl+Shift+Tab"].')
69
81
  };
70
82
  var BrowserReplayStopInputSchema = {
71
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
72
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start.")
83
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
84
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself.")
73
85
  };
74
86
  var BrowserReplayDownloadInputSchema = {
75
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
76
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
87
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
88
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
77
89
  filename: import_zod.z.string().optional().describe("Optional local MP4 filename. Defaults to a timestamped replay filename.")
78
90
  };
79
91
  var BrowserReplayAnnotationSchema = import_zod.z.object({
@@ -95,7 +107,7 @@ var BrowserReplayAnnotationSchema = import_zod.z.object({
95
107
  thickness: import_zod.z.number().min(1).max(24).optional().describe("Stroke thickness in pixels. Defaults to 5.")
96
108
  });
97
109
  var BrowserReplayMarkInputSchema = {
98
- session_id: import_zod.z.string().describe("The session id returned by browser_open. A replay must already be recording."),
110
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself. A replay must already be recording."),
99
111
  target: BrowserLocateTargetSchema.describe("The exact DOM element or text range to mark in the current viewport."),
100
112
  type: import_zod.z.enum(["box", "circle", "underline", "arrow"]).default("box").describe("Annotation style to generate. Labels are included on the returned annotation when label is provided."),
101
113
  label: import_zod.z.string().max(120).optional().describe("Optional callout text to render near the target."),
@@ -106,8 +118,8 @@ var BrowserReplayMarkInputSchema = {
106
118
  duration_seconds: import_zod.z.number().min(0.5).max(30).default(4).describe("How long the generated annotation should remain visible.")
107
119
  };
108
120
  var BrowserReplayAnnotateInputSchema = {
109
- session_id: import_zod.z.string().describe("The session id returned by browser_open."),
110
- replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays."),
121
+ session_id: import_zod.z.string().describe("The session id returned by browser_open or browser_list_sessions. Use only a returned session_id; do not construct one yourself."),
122
+ replay_id: import_zod.z.string().describe("The replay id returned by browser_replay_start or browser_list_replays. Use only a returned replay_id; do not construct one yourself."),
111
123
  annotations: import_zod.z.array(BrowserReplayAnnotationSchema).min(1).max(50).describe("Timed overlay annotations to render on the replay. Prefer annotations returned by browser_replay_mark; otherwise use exact DOM bounds from browser_locate/browser_screenshot/browser_read."),
112
124
  filename: import_zod.z.string().optional().describe("Optional output MP4 filename. Defaults to a timestamped annotated replay filename."),
113
125
  source_width: import_zod.z.number().positive().optional().describe("Width of the screenshot coordinate space used for annotations. Defaults to the replay video width."),
@@ -118,6 +130,113 @@ var BrowserReplayAnnotateInputSchema = {
118
130
  var BrowserListInputSchema = {
119
131
  include_closed: import_zod.z.boolean().default(false).describe("Include closed sessions in the list.")
120
132
  };
133
+ var BrowserOpenOutputSchema = {
134
+ ok: import_zod.z.boolean(),
135
+ tool: import_zod.z.literal("browser_open"),
136
+ session_id: import_zod.z.string().describe("Session id returned by browser_open. Use only this exact value in later browser_* calls; do not construct one yourself."),
137
+ watch_url: import_zod.z.string().describe("Human watch/takeover URL for this browser session."),
138
+ live_view_url: NullableString.describe("Raw live-view URL from the browser service when available."),
139
+ url: NullableString.describe("Initial URL requested by the caller, when provided."),
140
+ hint: import_zod.z.string(),
141
+ raw: BrowserRawObject.optional()
142
+ };
143
+ var BrowserScreenshotOutputSchema = {
144
+ ...BrowserBaseOutput,
145
+ tool: import_zod.z.literal("browser_screenshot"),
146
+ url: NullableString,
147
+ title: NullableString,
148
+ text: import_zod.z.string(),
149
+ elements: import_zod.z.array(BrowserElementOutput),
150
+ screenshot: import_zod.z.object({
151
+ mime_type: import_zod.z.string(),
152
+ inline: import_zod.z.boolean()
153
+ }).nullable()
154
+ };
155
+ var BrowserReadOutputSchema = {
156
+ ...BrowserBaseOutput,
157
+ tool: import_zod.z.literal("browser_read"),
158
+ url: NullableString,
159
+ title: NullableString,
160
+ text: import_zod.z.string(),
161
+ elements: import_zod.z.array(BrowserElementOutput),
162
+ raw: BrowserRawObject.optional()
163
+ };
164
+ var BrowserLocateOutputSchema = {
165
+ ...BrowserBaseOutput,
166
+ tool: import_zod.z.literal("browser_locate"),
167
+ url: NullableString,
168
+ title: NullableString,
169
+ viewport: BrowserRawObject.nullable(),
170
+ replay: BrowserRawObject.nullable(),
171
+ targets: import_zod.z.array(BrowserRawObject),
172
+ raw: BrowserRawObject.optional()
173
+ };
174
+ var BrowserActionOutputSchema = {
175
+ ...BrowserBaseOutput,
176
+ result: BrowserRawObject.describe("Provider action result. Check ok and follow with browser_screenshot/browser_read when page state matters."),
177
+ nextRecommendedTool: import_zod.z.string().nullable()
178
+ };
179
+ var BrowserReplayStartOutputSchema = {
180
+ ...BrowserReplayBaseOutput,
181
+ tool: import_zod.z.literal("browser_replay_start"),
182
+ view_url: NullableString,
183
+ download_url: NullableString,
184
+ raw: BrowserRawObject.optional()
185
+ };
186
+ var BrowserReplayStopOutputSchema = {
187
+ ...BrowserReplayBaseOutput,
188
+ tool: import_zod.z.literal("browser_replay_stop"),
189
+ view_url: NullableString,
190
+ download_url: NullableString,
191
+ raw: BrowserRawObject.optional()
192
+ };
193
+ var BrowserListReplaysOutputSchema = {
194
+ ...BrowserBaseOutput,
195
+ tool: import_zod.z.literal("browser_list_replays"),
196
+ replays: import_zod.z.array(BrowserRawObject),
197
+ count: import_zod.z.number().int().min(0)
198
+ };
199
+ var BrowserReplayDownloadOutputSchema = {
200
+ ...BrowserReplayBaseOutput,
201
+ tool: import_zod.z.literal("browser_replay_download"),
202
+ file_path: NullableString,
203
+ bytes: import_zod.z.number().int().min(0).nullable(),
204
+ mime_type: NullableString,
205
+ download_url: NullableString
206
+ };
207
+ var BrowserReplayMarkOutputSchema = {
208
+ ...BrowserReplayBaseOutput,
209
+ tool: import_zod.z.literal("browser_replay_mark"),
210
+ annotation: BrowserRawObject,
211
+ source_width: import_zod.z.number().nullable(),
212
+ source_height: import_zod.z.number().nullable(),
213
+ target: BrowserRawObject.nullable(),
214
+ hint: import_zod.z.string()
215
+ };
216
+ var BrowserReplayAnnotateOutputSchema = {
217
+ ...BrowserReplayBaseOutput,
218
+ tool: import_zod.z.literal("browser_replay_annotate"),
219
+ source_file_path: NullableString,
220
+ annotated_file_path: NullableString,
221
+ bytes: import_zod.z.number().int().min(0).nullable(),
222
+ width: import_zod.z.number().int().min(0).nullable(),
223
+ height: import_zod.z.number().int().min(0).nullable(),
224
+ annotation_count: import_zod.z.number().int().min(0).nullable(),
225
+ mime_type: NullableString
226
+ };
227
+ var BrowserCloseOutputSchema = {
228
+ ...BrowserBaseOutput,
229
+ tool: import_zod.z.literal("browser_close"),
230
+ closed: import_zod.z.boolean(),
231
+ raw: BrowserRawObject.optional()
232
+ };
233
+ var BrowserListSessionsOutputSchema = {
234
+ ok: import_zod.z.boolean(),
235
+ tool: import_zod.z.literal("browser_list_sessions"),
236
+ session_id: import_zod.z.null(),
237
+ sessions: import_zod.z.array(BrowserRawObject),
238
+ count: import_zod.z.number().int().min(0)
239
+ };
121
240
 
122
241
  // src/mcp/replay-annotator.ts
123
242
  var import_node_child_process = require("child_process");
@@ -380,8 +499,40 @@ async function annotateReplayVideo(inputFilePath, outputFilePath, options) {
380
499
  }
381
500
 
382
501
  // src/mcp/browser-agent-mcp-server.ts
383
- function textResult(value, isError = false) {
384
- return { content: [{ type: "text", text: JSON.stringify(value) }], isError };
502
+ function structuredResult(value, isError = false) {
503
+ return {
504
+ content: [{ type: "text", text: JSON.stringify(value) }],
505
+ structuredContent: value,
506
+ isError
507
+ };
508
+ }
509
+ function errorMessage(value) {
510
+ if (value && typeof value === "object") {
511
+ const data = value;
512
+ if (typeof data.error === "string") return data.error;
513
+ if (typeof data.message === "string") return data.message;
514
+ }
515
+ return typeof value === "string" ? value : "Browser Agent request failed";
516
+ }
517
+ function errorResult(tool, value, sessionId = null, replayId = null) {
518
+ return structuredResult({
519
+ ok: false,
520
+ tool,
521
+ session_id: sessionId,
522
+ ...replayId !== null ? { replay_id: replayId } : {},
523
+ error: errorMessage(value),
524
+ raw: value && typeof value === "object" ? value : { value }
525
+ }, true);
526
+ }
527
+ function actionResult(tool, sessionId, ok, data, nextRecommendedTool = "browser_screenshot") {
528
+ if (!ok) return errorResult(tool, data, sessionId);
529
+ return structuredResult({
530
+ ok: true,
531
+ tool,
532
+ session_id: sessionId,
533
+ result: data && typeof data === "object" ? data : { value: data },
534
+ nextRecommendedTool
535
+ });
385
536
  }
386
537
  function outputBaseDir() {
387
538
  return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
@@ -486,6 +637,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
486
637
  title: "Open Browser Session",
487
638
  description: "Open a fresh cloud browser you can drive. Returns a session_id used by all other browser_* tools, and a watch_url where a human can watch live or take over. Anti-bot stealth and automatic CAPTCHA/Cloudflare solving are on by default: if a Cloudflare or CAPTCHA challenge appears, do NOT click it \u2014 wait a few seconds and call browser_screenshot again; it is solved automatically. Billing: metered per second of active browser work at ~4 credits per minute; idle and standby time are free. Call browser_close when done to stop the meter. After opening, call browser_screenshot to see the page.",
488
639
  inputSchema: BrowserOpenInputSchema,
640
+ outputSchema: BrowserOpenOutputSchema,
489
641
  annotations: annotations("Open Browser Session")
490
642
  },
491
643
  async (input) => {
@@ -494,16 +646,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
494
646
  profile: input.profile,
495
647
  timeout_seconds: input.timeout_seconds
496
648
  });
497
- if (!open.ok) return textResult(open.data, true);
649
+ if (!open.ok) return errorResult("browser_open", open.data);
498
650
  const session = open.data;
499
651
  if (input.url) {
500
652
  await req("POST", `/agent/sessions/${session.session_id}/goto`, { url: input.url });
501
653
  }
502
- return textResult({
654
+ return structuredResult({
655
+ ok: true,
656
+ tool: "browser_open",
503
657
  session_id: session.session_id,
504
658
  watch_url: `${consoleBase}/console/${session.session_id}`,
505
659
  live_view_url: session.live_view_url ?? null,
506
- hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot."
660
+ url: input.url ?? null,
661
+ hint: "Call browser_screenshot to see the page. Click by the x,y of an element from the snapshot.",
662
+ raw: session
507
663
  });
508
664
  }
509
665
  );
@@ -513,19 +669,30 @@ function registerBrowserAgentMcpTools(server2, opts) {
513
669
  title: "See Page (Screenshot + Elements)",
514
670
  description: "Capture what the browser currently shows. Returns a screenshot image PLUS a text snapshot listing interactive elements with their center x,y coordinates, the page url and title, and visible text. This is your primary way to perceive the page. Click elements by their listed x,y. If a Cloudflare/CAPTCHA challenge is visible, wait and screenshot again rather than clicking it.",
515
671
  inputSchema: BrowserSessionInputSchema,
672
+ outputSchema: BrowserScreenshotOutputSchema,
516
673
  annotations: annotations("See Page", true)
517
674
  },
518
675
  async (input) => {
519
676
  const res = await req("POST", `/agent/sessions/${input.session_id}/screenshot`);
520
- if (!res.ok) return textResult(res.data, true);
677
+ if (!res.ok) return errorResult("browser_screenshot", res.data, input.session_id);
521
678
  const { image_base64, mime_type, url, title, elements, text } = res.data;
522
679
  const content = [];
523
680
  if (image_base64) content.push({ type: "image", data: image_base64, mimeType: mime_type ?? "image/png" });
681
+ const structured = {
682
+ ok: true,
683
+ tool: "browser_screenshot",
684
+ session_id: input.session_id,
685
+ url: url ?? null,
686
+ title: title ?? null,
687
+ text: typeof text === "string" ? text : "",
688
+ elements: Array.isArray(elements) ? elements : [],
689
+ screenshot: image_base64 ? { mime_type: mime_type ?? "image/png", inline: true } : null
690
+ };
524
691
  content.push({
525
692
  type: "text",
526
- text: JSON.stringify({ url, title, elements, text })
693
+ text: JSON.stringify(structured)
527
694
  });
528
- return { content };
695
+ return { content, structuredContent: structured };
529
696
  }
530
697
  );
531
698
  server2.registerTool(
@@ -534,11 +701,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
534
701
  title: "Read Page Text + Elements",
535
702
  description: "Return the page url, title, visible text, and the list of interactive elements (with x,y) without an image. Cheaper than browser_screenshot when you only need to read content or find a target element to click.",
536
703
  inputSchema: BrowserSessionInputSchema,
704
+ outputSchema: BrowserReadOutputSchema,
537
705
  annotations: annotations("Read Page", true)
538
706
  },
539
707
  async (input) => {
540
708
  const res = await req("POST", `/agent/sessions/${input.session_id}/read`);
541
- return textResult(res.data, !res.ok);
709
+ if (!res.ok) return errorResult("browser_read", res.data, input.session_id);
710
+ return structuredResult({
711
+ ok: true,
712
+ tool: "browser_read",
713
+ session_id: input.session_id,
714
+ url: res.data?.url ?? null,
715
+ title: res.data?.title ?? null,
716
+ text: typeof res.data?.text === "string" ? res.data.text : "",
717
+ elements: Array.isArray(res.data?.elements) ? res.data.elements : [],
718
+ raw: res.data
719
+ });
542
720
  }
543
721
  );
544
722
  server2.registerTool(
@@ -547,32 +725,46 @@ function registerBrowserAgentMcpTools(server2, opts) {
547
725
  title: "Locate DOM Targets",
548
726
  description: "Locate exact visible DOM elements or text ranges in the current browser viewport and return left/top/width/height bounds in screenshot pixels. Use this before drawing annotations or when a callout must literally circle, box, underline, or point to a real element. Prefer CSS selectors for exact UI elements; use text when selector is unknown. When a replay is actively recording, the result includes replay_elapsed_seconds for timing.",
549
727
  inputSchema: BrowserLocateInputSchema,
728
+ outputSchema: BrowserLocateOutputSchema,
550
729
  annotations: annotations("Locate DOM Targets", true)
551
730
  },
552
731
  async (input) => {
553
732
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: input.targets });
554
- return textResult(res.data, !res.ok);
733
+ if (!res.ok) return errorResult("browser_locate", res.data, input.session_id);
734
+ return structuredResult({
735
+ ok: true,
736
+ tool: "browser_locate",
737
+ session_id: input.session_id,
738
+ url: res.data?.url ?? null,
739
+ title: res.data?.title ?? null,
740
+ viewport: res.data?.viewport ?? null,
741
+ replay: res.data?.replay ?? null,
742
+ targets: Array.isArray(res.data?.targets) ? res.data.targets : [],
743
+ raw: res.data
744
+ });
555
745
  }
556
746
  );
557
747
  server2.registerTool(
558
748
  "browser_goto",
559
749
  {
560
750
  title: "Navigate To URL",
561
- description: "Navigate the browser to a URL. Follow with browser_screenshot to see the result.",
751
+ description: "Navigate an existing browser session to a URL when the user asks you to go to another page or continue browsing in the same session. Use browser_open first if no session exists. Follow with browser_screenshot to see redirects, login walls, CAPTCHA/Cloudflare state, or the loaded page.",
562
752
  inputSchema: BrowserGotoInputSchema,
753
+ outputSchema: BrowserActionOutputSchema,
563
754
  annotations: annotations("Navigate To URL")
564
755
  },
565
756
  async (input) => {
566
757
  const res = await req("POST", `/agent/sessions/${input.session_id}/goto`, { url: input.url });
567
- return textResult(res.data, !res.ok);
758
+ return actionResult("browser_goto", input.session_id, res.ok, res.data, "browser_screenshot");
568
759
  }
569
760
  );
570
761
  server2.registerTool(
571
762
  "browser_click",
572
763
  {
573
764
  title: "Click",
574
- description: "Click at x,y (screenshot pixel coordinates). Use the x,y of a target element from the latest browser_screenshot or browser_read.",
765
+ description: "Click a visible page target using screenshot pixel coordinates. Use this when the user asks you to press a button, open a menu, choose a result, or activate a visible UI element. Use x/y only from the latest browser_screenshot, browser_read, or browser_locate result; do not guess coordinates. Follow with browser_screenshot when the click may change the page.",
575
766
  inputSchema: BrowserClickInputSchema,
767
+ outputSchema: BrowserActionOutputSchema,
576
768
  annotations: annotations("Click")
577
769
  },
578
770
  async (input) => {
@@ -582,28 +774,30 @@ function registerBrowserAgentMcpTools(server2, opts) {
582
774
  button: input.button,
583
775
  num_clicks: input.num_clicks
584
776
  });
585
- return textResult(res.data, !res.ok);
777
+ return actionResult("browser_click", input.session_id, res.ok, res.data, "browser_screenshot");
586
778
  }
587
779
  );
588
780
  server2.registerTool(
589
781
  "browser_type",
590
782
  {
591
783
  title: "Type Text",
592
- description: 'Type text at the current focus. Click an input field first to focus it. Use browser_press with ["Return"] to submit.',
784
+ description: 'Type text into the currently focused browser field. Use this when the user asks you to fill a search box, form field, login field, or editable text area. Click or Tab to the field first if focus is uncertain. Use browser_press with ["Return"] to submit, and browser_screenshot afterward when page state matters.',
593
785
  inputSchema: BrowserTypeInputSchema,
786
+ outputSchema: BrowserActionOutputSchema,
594
787
  annotations: annotations("Type Text")
595
788
  },
596
789
  async (input) => {
597
790
  const res = await req("POST", `/agent/sessions/${input.session_id}/type`, { text: input.text, delay: input.delay });
598
- return textResult(res.data, !res.ok);
791
+ return actionResult("browser_type", input.session_id, res.ok, res.data, "browser_screenshot");
599
792
  }
600
793
  );
601
794
  server2.registerTool(
602
795
  "browser_scroll",
603
796
  {
604
797
  title: "Scroll",
605
- description: "Scroll the page. Positive delta_y scrolls down. Follow with browser_screenshot to see newly revealed content.",
798
+ description: "Scroll the page to reveal more content before reading, clicking, or locating elements. Positive delta_y scrolls down; negative delta_y scrolls up. Follow with browser_screenshot or browser_read to inspect newly revealed content.",
606
799
  inputSchema: BrowserScrollInputSchema,
800
+ outputSchema: BrowserActionOutputSchema,
607
801
  annotations: annotations("Scroll")
608
802
  },
609
803
  async (input) => {
@@ -613,20 +807,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
613
807
  x: input.x,
614
808
  y: input.y
615
809
  });
616
- return textResult(res.data, !res.ok);
810
+ return actionResult("browser_scroll", input.session_id, res.ok, res.data, "browser_screenshot");
617
811
  }
618
812
  );
619
813
  server2.registerTool(
620
814
  "browser_press",
621
815
  {
622
816
  title: "Press Keys",
623
- description: 'Press keys or combinations, e.g. ["Return"] to submit, ["Ctrl+a"] to select all, ["Ctrl+Shift+Tab"] to switch tabs.',
817
+ description: 'Press keyboard keys or combinations in the active browser session. Use this for submit, Escape, Tab navigation, select-all, or keyboard shortcuts. Examples: ["Return"], ["Escape"], ["Tab"], ["Ctrl+a"], ["Ctrl+Shift+Tab"]. Use browser_type for text entry and browser_screenshot after keypresses that may change the page.',
624
818
  inputSchema: BrowserPressInputSchema,
819
+ outputSchema: BrowserActionOutputSchema,
625
820
  annotations: annotations("Press Keys")
626
821
  },
627
822
  async (input) => {
628
823
  const res = await req("POST", `/agent/sessions/${input.session_id}/press`, { keys: input.keys });
629
- return textResult(res.data, !res.ok);
824
+ return actionResult("browser_press", input.session_id, res.ok, res.data, "browser_screenshot");
630
825
  }
631
826
  );
632
827
  server2.registerTool(
@@ -635,11 +830,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
635
830
  title: "Start Recording",
636
831
  description: "Start recording an MP4 replay of the session. Returns replay_id, view_url when available, and a download_url. Use to capture a task for later review; stop with browser_replay_stop.",
637
832
  inputSchema: BrowserSessionInputSchema,
833
+ outputSchema: BrowserReplayStartOutputSchema,
638
834
  annotations: annotations("Start Recording")
639
835
  },
640
836
  async (input) => {
641
837
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/start`);
642
- return textResult(res.data, !res.ok);
838
+ if (!res.ok) return errorResult("browser_replay_start", res.data, input.session_id);
839
+ return structuredResult({
840
+ ok: true,
841
+ tool: "browser_replay_start",
842
+ session_id: input.session_id,
843
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? null,
844
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
845
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
846
+ raw: res.data
847
+ });
643
848
  }
644
849
  );
645
850
  server2.registerTool(
@@ -648,11 +853,21 @@ function registerBrowserAgentMcpTools(server2, opts) {
648
853
  title: "Stop Recording",
649
854
  description: "Stop a replay recording and expose the final view_url and download_url. Use browser_replay_download to save the MP4 locally.",
650
855
  inputSchema: BrowserReplayStopInputSchema,
856
+ outputSchema: BrowserReplayStopOutputSchema,
651
857
  annotations: annotations("Stop Recording")
652
858
  },
653
859
  async (input) => {
654
860
  const res = await req("POST", `/agent/sessions/${input.session_id}/replay/stop`, { replay_id: input.replay_id });
655
- return textResult(res.data, !res.ok);
861
+ if (!res.ok) return errorResult("browser_replay_stop", res.data, input.session_id, input.replay_id);
862
+ return structuredResult({
863
+ ok: true,
864
+ tool: "browser_replay_stop",
865
+ session_id: input.session_id,
866
+ replay_id: res.data?.replay_id ?? res.data?.replayId ?? input.replay_id,
867
+ view_url: res.data?.view_url ?? res.data?.viewUrl ?? null,
868
+ download_url: res.data?.download_url ?? res.data?.downloadUrl ?? null,
869
+ raw: res.data
870
+ });
656
871
  }
657
872
  );
658
873
  server2.registerTool(
@@ -661,11 +876,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
661
876
  title: "List Replay Videos",
662
877
  description: "List replay recordings for a browser session, including final view_url and authenticated download_url values when available.",
663
878
  inputSchema: BrowserSessionInputSchema,
879
+ outputSchema: BrowserListReplaysOutputSchema,
664
880
  annotations: annotations("List Replay Videos", true)
665
881
  },
666
882
  async (input) => {
667
883
  const res = await req("GET", `/agent/sessions/${input.session_id}/replays`);
668
- return textResult(res.data, !res.ok);
884
+ if (!res.ok) return errorResult("browser_list_replays", res.data, input.session_id);
885
+ const replays = Array.isArray(res.data?.replays) ? res.data.replays : [];
886
+ return structuredResult({
887
+ ok: true,
888
+ tool: "browser_list_replays",
889
+ session_id: input.session_id,
890
+ replays,
891
+ count: replays.length
892
+ });
669
893
  }
670
894
  );
671
895
  server2.registerTool(
@@ -674,11 +898,22 @@ function registerBrowserAgentMcpTools(server2, opts) {
674
898
  title: "Download Replay MP4",
675
899
  description: "Download a replay recording through MCP Scraper and save the MP4 locally under MCP_SCRAPER_OUTPUT_DIR/browser-replays. Use after browser_replay_stop or browser_list_replays.",
676
900
  inputSchema: BrowserReplayDownloadInputSchema,
677
- annotations: annotations("Download Replay MP4", true)
901
+ outputSchema: BrowserReplayDownloadOutputSchema,
902
+ annotations: annotations("Download Replay MP4")
678
903
  },
679
904
  async (input) => {
680
905
  const res = await downloadReplay(input.session_id, input.replay_id, input.filename);
681
- return textResult(res.data, !res.ok);
906
+ if (!res.ok) return errorResult("browser_replay_download", res.data, input.session_id, input.replay_id);
907
+ return structuredResult({
908
+ ok: true,
909
+ tool: "browser_replay_download",
910
+ session_id: input.session_id,
911
+ replay_id: input.replay_id,
912
+ file_path: res.data?.file_path ?? null,
913
+ bytes: typeof res.data?.bytes === "number" ? res.data.bytes : null,
914
+ mime_type: res.data?.mime_type ?? null,
915
+ download_url: res.data?.download_url ?? null
916
+ });
682
917
  }
683
918
  );
684
919
  server2.registerTool(
@@ -687,19 +922,20 @@ function registerBrowserAgentMcpTools(server2, opts) {
687
922
  title: "Mark Replay Annotation",
688
923
  description: "While a replay is actively recording, locate one exact DOM target and return a ready-to-use annotation object with DOM bounds and replay-relative timing. Use this instead of guessing start_seconds or drawing rough rectangles. Workflow: start browser_replay_start, navigate until the target is visible and stable, call browser_replay_mark for each callout, then stop the replay and pass the returned annotations to browser_replay_annotate.",
689
924
  inputSchema: BrowserReplayMarkInputSchema,
925
+ outputSchema: BrowserReplayMarkOutputSchema,
690
926
  annotations: annotations("Mark Replay Annotation", true)
691
927
  },
692
928
  async (input) => {
693
929
  const res = await req("POST", `/agent/sessions/${input.session_id}/locate`, { targets: [input.target] });
694
- if (!res.ok) return textResult(res.data, true);
930
+ if (!res.ok) return errorResult("browser_replay_mark", res.data, input.session_id);
695
931
  const target = res.data?.targets?.[0];
696
932
  const element = target?.element;
697
933
  const elapsed = res.data?.replay?.replay_elapsed_seconds;
698
934
  if (!target?.found || !element) {
699
- return textResult({ error: target?.error ?? "target not found in current viewport", target }, true);
935
+ return errorResult("browser_replay_mark", { error: target?.error ?? "target not found in current viewport", target }, input.session_id);
700
936
  }
701
937
  if (!finiteNumber2(elapsed)) {
702
- return textResult({ error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, true);
938
+ return errorResult("browser_replay_mark", { error: "no active replay clock found; call browser_replay_start before browser_replay_mark" }, input.session_id);
703
939
  }
704
940
  const padded = expandElementBounds(element, res.data?.viewport, input.padding ?? 8);
705
941
  const start = Math.max(0, elapsed + (input.start_offset_seconds ?? -0.25));
@@ -716,7 +952,11 @@ function registerBrowserAgentMcpTools(server2, opts) {
716
952
  ...input.color ? { color: input.color } : {},
717
953
  ...input.thickness ? { thickness: input.thickness } : {}
718
954
  };
719
- return textResult({
955
+ return structuredResult({
956
+ ok: true,
957
+ tool: "browser_replay_mark",
958
+ session_id: input.session_id,
959
+ replay_id: res.data?.replay?.replay_id ?? res.data?.replay?.replayId ?? null,
720
960
  annotation,
721
961
  source_width: padded.sourceWidth,
722
962
  source_height: padded.sourceHeight,
@@ -732,12 +972,13 @@ function registerBrowserAgentMcpTools(server2, opts) {
732
972
  title: "Annotate Replay MP4",
733
973
  description: "Download a browser replay MP4, render visual annotations over it, and save a new annotated MP4 locally. Use this after browser_replay_stop when the user wants proof videos with circles, boxes, arrows, underlines, or labels. For accurate timing and placement, prefer annotations returned by browser_replay_mark while the replay is recording; otherwise use exact left/top/width/height bounds from browser_locate. If the replay video size differs from the screenshot coordinate space, pass source_width and source_height.",
734
974
  inputSchema: BrowserReplayAnnotateInputSchema,
735
- annotations: annotations("Annotate Replay MP4", true)
975
+ outputSchema: BrowserReplayAnnotateOutputSchema,
976
+ annotations: annotations("Annotate Replay MP4")
736
977
  },
737
978
  async (input) => {
738
979
  const sourceName = input.filename ? `${input.filename}-source` : void 0;
739
980
  const downloaded = await downloadReplay(input.session_id, input.replay_id, sourceName);
740
- if (!downloaded.ok) return textResult(downloaded.data, true);
981
+ if (!downloaded.ok) return errorResult("browser_replay_annotate", downloaded.data, input.session_id, input.replay_id);
741
982
  try {
742
983
  const sourcePath = String(downloaded.data.file_path);
743
984
  const outputPath = annotatedReplayFilePath(input.session_id, input.replay_id, input.filename);
@@ -749,7 +990,10 @@ function registerBrowserAgentMcpTools(server2, opts) {
749
990
  sourceLeftOffset: input.source_left_offset,
750
991
  sourceTopOffset: input.source_top_offset
751
992
  });
752
- return textResult({
993
+ return structuredResult({
994
+ ok: true,
995
+ tool: "browser_replay_annotate",
996
+ session_id: input.session_id,
753
997
  replay_id: input.replay_id,
754
998
  source_file_path: sourcePath,
755
999
  annotated_file_path: result.filePath,
@@ -760,7 +1004,7 @@ function registerBrowserAgentMcpTools(server2, opts) {
760
1004
  mime_type: "video/mp4"
761
1005
  });
762
1006
  } catch (err) {
763
- return textResult({ error: err instanceof Error ? err.message : String(err) }, true);
1007
+ return errorResult("browser_replay_annotate", { error: err instanceof Error ? err.message : String(err) }, input.session_id, input.replay_id);
764
1008
  }
765
1009
  }
766
1010
  );
@@ -768,28 +1012,43 @@ function registerBrowserAgentMcpTools(server2, opts) {
768
1012
  "browser_close",
769
1013
  {
770
1014
  title: "Close Browser Session",
771
- description: "Close and release the browser session when the task is done.",
1015
+ description: "Close and release a browser session when the task is done, when the user asks to stop the browser, or when active browser billing should end. Use browser_list_sessions first if you need to recover the session_id.",
772
1016
  inputSchema: BrowserSessionInputSchema,
1017
+ outputSchema: BrowserCloseOutputSchema,
773
1018
  annotations: { title: "Close Browser Session", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false }
774
1019
  },
775
1020
  async (input) => {
776
1021
  const res = await req("DELETE", `/agent/sessions/${input.session_id}`);
777
- return textResult(res.data, !res.ok);
1022
+ if (!res.ok) return errorResult("browser_close", res.data, input.session_id);
1023
+ return structuredResult({
1024
+ ok: true,
1025
+ tool: "browser_close",
1026
+ session_id: input.session_id,
1027
+ closed: true,
1028
+ raw: res.data
1029
+ });
778
1030
  }
779
1031
  );
780
1032
  server2.registerTool(
781
1033
  "browser_list_sessions",
782
1034
  {
783
1035
  title: "List Browser Sessions",
784
- description: "List your browser sessions and their status, with a watch_url for each.",
1036
+ description: "List browser sessions and their status, with a watch_url for each. Use this to recover a session_id, find an active browser, or decide which session to close. Use browser_open to create a new session.",
785
1037
  inputSchema: BrowserListInputSchema,
1038
+ outputSchema: BrowserListSessionsOutputSchema,
786
1039
  annotations: annotations("List Browser Sessions", true)
787
1040
  },
788
1041
  async (input) => {
789
1042
  const res = await req("GET", `/agent/sessions${input.include_closed ? "?all=1" : ""}`);
790
- if (!res.ok) return textResult(res.data, true);
1043
+ if (!res.ok) return errorResult("browser_list_sessions", res.data);
791
1044
  const sessions = (res.data.sessions ?? []).map((s) => ({ ...s, watch_url: `${consoleBase}/console/${s.session_id}` }));
792
- return textResult({ sessions });
1045
+ return structuredResult({
1046
+ ok: true,
1047
+ tool: "browser_list_sessions",
1048
+ session_id: null,
1049
+ sessions,
1050
+ count: sessions.length
1051
+ });
793
1052
  }
794
1053
  );
795
1054
  }