viyv-browser-mcp 0.2.0 → 0.3.0

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/dist/index.js CHANGED
@@ -3,14 +3,8 @@
3
3
  // src/index.ts
4
4
  import { existsSync as existsSync3, readdirSync } from "fs";
5
5
 
6
- // src/server.ts
7
- import { randomUUID as randomUUID2 } from "crypto";
8
- import { existsSync, unlinkSync } from "fs";
9
- import http from "http";
10
- import { createServer } from "net";
11
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
12
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
13
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ // src/native-host/bridge.ts
7
+ import { createConnection } from "net";
14
8
 
15
9
  // ../shared/dist/constants.js
16
10
  var PROTOCOL_VERSION = "1.0.0";
@@ -45,7 +39,7 @@ var LIMITS = {
45
39
  /** A11y tree max elements */
46
40
  A11Y_MAX_ELEMENTS: 5e3,
47
41
  /** A11y tree default depth */
48
- A11Y_DEFAULT_DEPTH: 8,
42
+ A11Y_DEFAULT_DEPTH: 15,
49
43
  /** A11y tree default max chars */
50
44
  A11Y_DEFAULT_MAX_CHARS: 5e4,
51
45
  /** Event buffer max entries */
@@ -78,6 +72,214 @@ var MCP_SERVER = {
78
72
  SOCKET_PATH_TEMPLATE: "/tmp/viyv-browser-{pid}.sock"
79
73
  };
80
74
 
75
+ // src/native-host/compression.ts
76
+ import { gunzipSync, gzipSync } from "zlib";
77
+ var CHUNK_SIZE = 768 * 1024;
78
+ function compressPayload(data) {
79
+ const original = Buffer.from(data, "utf-8");
80
+ const gzipped = gzipSync(original);
81
+ if (gzipped.length < original.length) {
82
+ return { compressed: gzipped.toString("base64"), wasCompressed: true };
83
+ }
84
+ return { compressed: data, wasCompressed: false };
85
+ }
86
+ function decompressPayload(data, isCompressed) {
87
+ if (!isCompressed) return data;
88
+ const buf = Buffer.from(data, "base64");
89
+ return gunzipSync(buf).toString("utf-8");
90
+ }
91
+
92
+ // src/native-host/transport.ts
93
+ var MAX_MESSAGE_SIZE = 1024 * 1024;
94
+ function encodeMessage(message) {
95
+ const json = JSON.stringify(message);
96
+ const body = Buffer.from(json, "utf-8");
97
+ if (body.length > MAX_MESSAGE_SIZE) {
98
+ throw new Error(`Message too large: ${body.length} bytes (max ${MAX_MESSAGE_SIZE})`);
99
+ }
100
+ const header = Buffer.alloc(4);
101
+ header.writeUInt32LE(body.length, 0);
102
+ return Buffer.concat([header, body]);
103
+ }
104
+ function createMessageReader(stream, onMessage, onError, onClose) {
105
+ let buffer = Buffer.alloc(0);
106
+ let expectedLength = null;
107
+ stream.on("data", (chunk) => {
108
+ buffer = Buffer.concat([buffer, chunk]);
109
+ while (true) {
110
+ if (expectedLength === null) {
111
+ if (buffer.length < 4) break;
112
+ expectedLength = buffer.readUInt32LE(0);
113
+ buffer = buffer.subarray(4);
114
+ if (expectedLength > MAX_MESSAGE_SIZE) {
115
+ onError?.(new Error(`Message too large: ${expectedLength} bytes`));
116
+ expectedLength = null;
117
+ buffer = Buffer.alloc(0);
118
+ break;
119
+ }
120
+ }
121
+ if (buffer.length < expectedLength) break;
122
+ const jsonBuffer = buffer.subarray(0, expectedLength);
123
+ buffer = buffer.subarray(expectedLength);
124
+ expectedLength = null;
125
+ try {
126
+ const message = JSON.parse(jsonBuffer.toString("utf-8"));
127
+ onMessage(message);
128
+ } catch (error) {
129
+ onError?.(new Error(`Invalid JSON: ${error.message}`));
130
+ }
131
+ }
132
+ });
133
+ stream.on("error", (error) => {
134
+ onError?.(new Error(`Stream error: ${error.message}`));
135
+ });
136
+ stream.on("end", () => {
137
+ onClose?.();
138
+ });
139
+ stream.on("close", () => {
140
+ onClose?.();
141
+ });
142
+ }
143
+ function writeMessage(stream, message) {
144
+ const encoded = encodeMessage(message);
145
+ stream.write(encoded);
146
+ }
147
+
148
+ // src/native-host/bridge.ts
149
+ var MAX_BUFFER_SIZE = 1e3;
150
+ function startBridge(options) {
151
+ const { socketPath, onError } = options;
152
+ let socket = null;
153
+ let reconnecting = false;
154
+ let retryCount = 0;
155
+ const pendingMessages = [];
156
+ function flushBuffer() {
157
+ if (!socket || socket.destroyed) return;
158
+ while (pendingMessages.length > 0) {
159
+ const msg = pendingMessages[0];
160
+ try {
161
+ const written = socket.write(`${JSON.stringify(msg)}
162
+ `);
163
+ pendingMessages.shift();
164
+ if (!written) {
165
+ socket.once("drain", () => flushBuffer());
166
+ return;
167
+ }
168
+ } catch (error) {
169
+ onError?.(error);
170
+ return;
171
+ }
172
+ }
173
+ }
174
+ function connectSocket() {
175
+ socket = createConnection(socketPath);
176
+ socket.on("connect", () => {
177
+ process.stderr.write(`[viyv-browser:native-host] Connected to MCP server at ${socketPath}
178
+ `);
179
+ retryCount = 0;
180
+ flushBuffer();
181
+ });
182
+ let lineBuffer = "";
183
+ socket.on("data", (data) => {
184
+ lineBuffer += data.toString("utf-8");
185
+ const lines = lineBuffer.split("\n");
186
+ lineBuffer = lines.pop() ?? "";
187
+ for (const line of lines) {
188
+ if (!line) continue;
189
+ try {
190
+ let message = JSON.parse(line);
191
+ if (message.type === "compressed" && typeof message.data === "string") {
192
+ const decompressed = decompressPayload(message.data, true);
193
+ message = JSON.parse(decompressed);
194
+ }
195
+ writeMessage(process.stdout, message);
196
+ } catch (error) {
197
+ onError?.(error);
198
+ }
199
+ }
200
+ });
201
+ socket.on("error", (error) => {
202
+ process.stderr.write(`[viyv-browser:native-host] Socket error: ${error.message}
203
+ `);
204
+ onError?.(error);
205
+ });
206
+ socket.on("close", () => {
207
+ process.stderr.write("[viyv-browser:native-host] Socket closed\n");
208
+ socket = null;
209
+ if (!reconnecting) {
210
+ reconnecting = true;
211
+ const delay = Math.min(
212
+ RECONNECT.INITIAL_DELAY * RECONNECT.MULTIPLIER ** retryCount,
213
+ RECONNECT.MAX_DELAY
214
+ );
215
+ retryCount++;
216
+ process.stderr.write(
217
+ `[viyv-browser:native-host] Reconnecting in ${delay}ms (attempt ${retryCount})
218
+ `
219
+ );
220
+ setTimeout(() => {
221
+ reconnecting = false;
222
+ connectSocket();
223
+ }, delay);
224
+ }
225
+ });
226
+ }
227
+ createMessageReader(
228
+ process.stdin,
229
+ (message) => {
230
+ if (socket && !socket.destroyed) {
231
+ const json = JSON.stringify(message);
232
+ if (json.length > LIMITS.CHUNK_SIZE) {
233
+ const { compressed, wasCompressed } = compressPayload(json);
234
+ if (wasCompressed) {
235
+ socket.write(`${JSON.stringify({ type: "compressed", data: compressed })}
236
+ `);
237
+ } else {
238
+ socket.write(`${json}
239
+ `);
240
+ }
241
+ } else {
242
+ socket.write(`${json}
243
+ `);
244
+ }
245
+ } else {
246
+ if (pendingMessages.length < MAX_BUFFER_SIZE) {
247
+ pendingMessages.push(message);
248
+ } else {
249
+ process.stderr.write("[viyv-browser:native-host] Message buffer full, dropping message\n");
250
+ }
251
+ }
252
+ },
253
+ onError
254
+ );
255
+ connectSocket();
256
+ process.on("SIGINT", () => {
257
+ socket?.destroy();
258
+ process.exit(0);
259
+ });
260
+ process.on("SIGTERM", () => {
261
+ socket?.destroy();
262
+ process.exit(0);
263
+ });
264
+ process.stdin.on("end", () => {
265
+ process.stderr.write("[viyv-browser:native-host] stdin closed, shutting down\n");
266
+ socket?.destroy();
267
+ process.exit(0);
268
+ });
269
+ }
270
+
271
+ // src/server.ts
272
+ import { randomUUID as randomUUID2 } from "crypto";
273
+ import { existsSync, statSync, unlinkSync } from "fs";
274
+ import http from "http";
275
+ import { createServer } from "net";
276
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
277
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
278
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
279
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
280
+ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
281
+ import { z as z2 } from "zod";
282
+
81
283
  // src/agent-session.ts
82
284
  import { randomUUID } from "crypto";
83
285
  var sessions = /* @__PURE__ */ new Map();
@@ -130,10 +332,8 @@ function cleanupStaleSessions() {
130
332
  }
131
333
  }
132
334
  if (cleaned > 0) {
133
- process.stderr.write(
134
- `[viyv-browser:mcp] Cleaned up ${cleaned} stale session(s)
135
- `
136
- );
335
+ process.stderr.write(`[viyv-browser:mcp] Cleaned up ${cleaned} stale session(s)
336
+ `);
137
337
  }
138
338
  return cleaned;
139
339
  }
@@ -201,26 +401,15 @@ function isExtensionConnected() {
201
401
  return true;
202
402
  }
203
403
 
204
- // src/native-host/compression.ts
205
- import { gzipSync, gunzipSync } from "zlib";
206
- var CHUNK_SIZE = 768 * 1024;
207
- function compressPayload(data) {
208
- const original = Buffer.from(data, "utf-8");
209
- const gzipped = gzipSync(original);
210
- if (gzipped.length < original.length) {
211
- return { compressed: gzipped.toString("base64"), wasCompressed: true };
212
- }
213
- return { compressed: data, wasCompressed: false };
214
- }
215
- function decompressPayload(data, isCompressed) {
216
- if (!isCompressed) return data;
217
- const buf = Buffer.from(data, "base64");
218
- return gunzipSync(buf).toString("utf-8");
219
- }
220
-
221
404
  // src/tools/index.ts
222
405
  import { z } from "zod";
223
406
 
407
+ // src/tools/advanced/file-upload.ts
408
+ var FILE_UPLOAD_DESCRIPTION = `Upload local files to a file input element on the page using absolute file paths.
409
+ Do not click on file input elements \u2014 clicking opens a native file picker dialog.
410
+ Instead, use read_page or find to locate the file input, then use this tool with its ref.
411
+ The paths must be absolute paths accessible from the machine running Chrome.`;
412
+
224
413
  // src/tools/advanced/gif-creator.ts
225
414
  var GIF_CREATOR_DESCRIPTION = `Record a GIF of browser activity.
226
415
  Captures a sequence of screenshots over a specified duration
@@ -261,10 +450,10 @@ allowing the user to approve, modify, or reject the plan before
261
450
  execution begins.`;
262
451
 
263
452
  // src/tools/advanced/upload-image.ts
264
- var UPLOAD_IMAGE_DESCRIPTION = `Upload an image to a file input element on the page.
265
- Accepts a local file path or base64-encoded image data and
266
- programmatically sets it on the target file input. Supports
267
- common image formats including PNG, JPEG, GIF, and WebP.`;
453
+ var UPLOAD_IMAGE_DESCRIPTION = `Upload a previously captured screenshot or user-uploaded image to a file input or drag & drop target.
454
+ Provide imageId from a prior screenshot action. Supports targeting by
455
+ element ref (for file inputs) or coordinate (for drag & drop).
456
+ Supports common image formats including PNG, JPEG, GIF, and WebP.`;
268
457
 
269
458
  // src/tools/core/click.ts
270
459
  var CLICK_DESCRIPTION = `Click at coordinates or on a referenced element.
@@ -305,13 +494,15 @@ is actively displayed on the page.`;
305
494
  // src/tools/core/hover.ts
306
495
  var HOVER_DESCRIPTION = `Move mouse to coordinates or element without clicking.
307
496
 
497
+ Provide either coordinate [x, y] or ref (element reference from read_page/find).
308
498
  Useful for revealing tooltips, dropdown menus, or triggering hover states.`;
309
499
 
310
500
  // src/tools/core/javascript-exec.ts
311
501
  var JAVASCRIPT_EXEC_DESCRIPTION = `Execute arbitrary JavaScript code in the page context.
312
502
  The script runs in the main world of the active page and can access
313
503
  the DOM, window object, and any page-level APIs. Returns the
314
- serialized result of the last evaluated expression.`;
504
+ serialized result of the last evaluated expression.
505
+ Supports top-level await (e.g., "await fetch('/api').then(r => r.json())").`;
315
506
 
316
507
  // src/tools/core/key.ts
317
508
  var KEY_DESCRIPTION = `Press keyboard key(s).
@@ -320,62 +511,690 @@ Space-separated key names. Supports keyboard shortcuts.
320
511
  Examples: "Enter", "Tab", "Backspace", "ctrl+a", "cmd+c".
321
512
  Use repeat parameter for repeated presses (e.g., arrow keys).`;
322
513
 
323
- // src/tools/core/navigate.ts
324
- var NAVIGATE_DESCRIPTION = `Navigate to a URL, or go forward/back in browser history.
514
+ // src/tools/core/navigate.ts
515
+ var NAVIGATE_DESCRIPTION = `Navigate to a URL, or go forward/back in browser history.
516
+
517
+ Use "back" to go back in history, "forward" to go forward.
518
+ Provide a full URL (with protocol) to navigate to a new page.
519
+ Waits for page load to complete before returning.`;
520
+
521
+ // src/tools/core/read-page.ts
522
+ var READ_PAGE_DESCRIPTION = `Get accessibility tree representation of page elements.
523
+
524
+ Supports:
525
+ - filter: "interactive" for buttons/links/inputs only, "all" for everything
526
+ - depth: max tree depth (1-20, default 8)
527
+ - refId: focus on a specific element subtree
528
+ - maxChars: limit output size (default 50000)
529
+
530
+ Returns elements with ref IDs that can be used with click, form_input, etc.`;
531
+
532
+ // src/tools/core/screenshot.ts
533
+ var SCREENSHOT_DESCRIPTION = `Take a screenshot of a tab.
534
+
535
+ Returns image as an MCP ImageContent block, plus metadata (imageId) as text.
536
+ Default format is JPEG with quality 80.
537
+
538
+ IMPORTANT - Token-efficient alternatives (prefer these when possible):
539
+ - read_page: Get accessibility tree with element text, values, and states
540
+ - get_page_text: Extract all readable text from the page
541
+ - find: Locate elements by natural language description
542
+ Use screenshot only when visual verification is needed (layout, styling, images, charts).
543
+
544
+ For example: to verify text was typed into an input, use read_page to check the
545
+ input's value \u2014 don't take a screenshot. To confirm a page loaded, check the
546
+ accessibility tree for expected headings or elements.
547
+
548
+ Use ref to capture just a specific element (with padding) instead of the full page.
549
+ This dramatically reduces image size. If both ref and region are provided, region
550
+ takes precedence. The response includes an imageId for use with upload_image.`;
551
+
552
+ // src/tools/core/scroll.ts
553
+ var SCROLL_DESCRIPTION = `Scroll in a direction at given coordinates, or scroll an element into view by ref.
554
+
555
+ Two modes:
556
+ 1. Directional scroll: provide coordinate + direction (+ optional amount 1-10, default 3).
557
+ 2. Scroll to element: provide ref (element reference ID from read_page/find). Scrolls the element into view using smooth scrolling.`;
558
+
559
+ // src/tools/core/type.ts
560
+ var TYPE_DESCRIPTION = `Type text into the currently focused element.
561
+
562
+ Each character is typed individually with keyDown/keyUp events.
563
+ For special keys (Enter, Tab, etc.), use the 'key' tool instead.`;
564
+
565
+ // src/tools/core/wait-for.ts
566
+ var WAIT_FOR_DESCRIPTION = `Wait for a specified condition before proceeding.
567
+ Supports waiting for a CSS selector to appear in the DOM,
568
+ for a navigation event to complete, or for a fixed timeout duration.
569
+ Useful for synchronizing with asynchronous page updates.
570
+ At least one of selector, navigation, or timeout must be specified.`;
571
+
572
+ // src/tools/debug/read-console-messages.ts
573
+ var READ_CONSOLE_MESSAGES_DESCRIPTION = `Read console messages captured from the page.
574
+ Returns log, warning, error, and info messages that have been
575
+ emitted by page scripts. Supports filtering by log level
576
+ and limiting the number of returned entries.
577
+ Use the pattern parameter to filter messages by regex, and
578
+ the clear parameter to remove messages after reading.`;
579
+
580
+ // src/tools/debug/read-network-requests.ts
581
+ var READ_NETWORK_REQUESTS_DESCRIPTION = `Read network requests captured from the page.
582
+ Returns details of HTTP requests and responses including URL, method,
583
+ status code, headers, and timing information. Supports filtering
584
+ by URL pattern or resource type.`;
585
+
586
+ // src/tools/semantic/sm-add-action.ts
587
+ var SM_ADD_ACTION_DESCRIPTION = `Define a semantic action (step sequence) for a registered page.
588
+
589
+ Creates an executable action with typed parameters and ordered steps.
590
+ Steps reference targets by target_id and support param_ref for dynamic values.
591
+
592
+ Parameters:
593
+ - page_id: the page this action belongs to
594
+ - label: action name (e.g., "Login")
595
+ - verb: action verb (e.g., "login", "submit", "search")
596
+ - description (optional): LLM-facing description for discovery via sm_capabilities
597
+ - params[]: parameter definitions [{name, type, required, secret, description, default_value}]
598
+ - steps[]: ordered step sequence (minimum 1):
599
+ - type: 'click' | 'type' | 'select' | 'wait' | 'scroll' | 'key' | 'navigate'
600
+ - target_id: target to act on
601
+ - params: {text, value, keys, direction, amount, url, param_ref}
602
+ - pre_wait: {strategy, selector, timeout_ms}
603
+ - description: step description
604
+ - retry_policy (optional): {max_retries:0-5, backoff, base_delay_ms, jitter}
605
+
606
+ Returns:
607
+ - action_id, step_count
608
+ - validation: {valid, warnings[]} with target/param reference checks`;
609
+
610
+ // src/tools/semantic/sm-add-fetch.ts
611
+ var SM_ADD_FETCH_DESCRIPTION = `Define a data extraction rule (fetch) for a registered page.
612
+
613
+ Creates a structured data extraction definition that maps DOM elements to typed output fields.
614
+ Supports text, attribute, list, count, and existence extraction types.
615
+
616
+ Parameters:
617
+ - page_id: the page this fetch belongs to
618
+ - label: fetch name (e.g., "Get Posts")
619
+ - type_name: output type name (e.g., "posts", "profile")
620
+ - description (optional): LLM-facing description for discovery via sm_capabilities
621
+ - fields[]: extraction field definitions (minimum 1):
622
+ - name: output field name
623
+ - target_id: target element to extract from
624
+ - extraction_type: 'text' | 'attribute' | 'list' | 'count' | 'exists'
625
+ - attribute: attribute name (for 'attribute' type)
626
+ - item_selector: CSS selector for list items (for 'list' type)
627
+ - item_fields[]: structured extraction for each list item (for 'list' type):
628
+ - name: output field name
629
+ - selector: CSS selector relative to each list item (use ":scope" for item element itself)
630
+ - extraction_type: 'text' | 'attribute' | 'exists'
631
+ - attribute: attribute name (for 'attribute' type)
632
+ - limits (optional): {max_chars:1000-200000, max_items:1-1000}
633
+
634
+ Returns:
635
+ - fetch_id, field_count
636
+ - validation: {valid, warnings[]} with target reference checks`;
637
+
638
+ // src/tools/semantic/sm-add-target.ts
639
+ var SM_ADD_TARGET_DESCRIPTION = `Create or update semantic targets (element locators) for a registered page.
640
+
641
+ Supports batch creation and re-binding of existing targets.
642
+ Use ref from read_page/find to auto-generate locators, or provide locators explicitly.
643
+
644
+ Parameters:
645
+ - tabId: target tab where elements are present
646
+ - page_id: the page these targets belong to
647
+ - targets[]: array of target definitions:
648
+ - target_id (optional): specify to re-bind an existing target's locators
649
+ - label: target name (e.g., "Username Input")
650
+ - role: semantic role (e.g., "textbox", "button")
651
+ - ref (optional): element ref from read_page/find for auto-locator generation
652
+ - locators (optional): explicit locator array [{type, value, stability}]
653
+
654
+ Each target must have either ref or locators.
655
+ When target_id is provided, only locators are updated (label/role preserved).
656
+
657
+ Returns per target:
658
+ - target_id, label, locators, resolve_test (live element check), updated (true = re-bind)`;
659
+
660
+ // src/tools/semantic/sm-capabilities.ts
661
+ var SM_CAPABILITIES_DESCRIPTION = `Query semantic capabilities available for the current page.
662
+
663
+ Returns the matched page definition, available actions, and fetches for the active tab.
664
+ Uses page signature matching (URL pattern + DOM markers) to identify the current page state.
665
+
666
+ Returns:
667
+ - ok: whether a page match was found
668
+ - domain: hostname of the current tab
669
+ - page_id / page_type / variant_id: matched page and variant
670
+ - match_score: confidence score of the match
671
+ - actions[]: available actions with their parameters
672
+ - fetches[]: available data extractions with their parameters
673
+ - other_pages[]: other registered pages on the same domain (for cross-page navigation)
674
+ - hint: guidance message when no page matches
675
+
676
+ Use this before sm_invoke or sm_fetch to discover what operations are available.
677
+ If no page matches (ok: false), use sm_register_page to register the current page,
678
+ then sm_add_target and sm_add_action/sm_add_fetch to configure automation.
679
+ Use sm_status to review all existing configurations.`;
680
+
681
+ // src/tools/semantic/sm-custom-view.ts
682
+ var SM_CUSTOM_VIEW_CREATE_DESCRIPTION = `Create a custom HTML view that can be opened in a new tab.
683
+
684
+ Use this to build dashboards, charts, or reports using HTML/CSS/JS. The HTML can reference external CDN libraries (e.g., Chart.js, D3) and access bound data via window.__VIYV_DATA__.
685
+
686
+ Parameters:
687
+ - label: view name (max 100 chars)
688
+ - description: optional description (max 500 chars)
689
+ - html: full HTML document (max 200KB)
690
+ - data_bindings: optional array of data source references (max 10)
691
+ - binding_id: unique ID within this view (e.g., "b1")
692
+ - source_type: "job_report" | "report" | "job" | "scenario"
693
+ - source_id: entity ID (e.g., "JRP_xxxx")
694
+ - projection: optional {fields, max_entries} to limit injected data
695
+
696
+ The HTML should reference data as window.__VIYV_DATA__[binding_id]. Data is injected when the user opens the view.
697
+
698
+ Returns: {ok, view_id, html_size, binding_count}`;
699
+ var SM_CUSTOM_VIEW_UPDATE_DESCRIPTION = `Update an existing custom view.
700
+
701
+ Use this to refresh HTML after data structure changes (stale bindings) or to modify the view content.
702
+
703
+ Parameters:
704
+ - view_id: view to update
705
+ - label: new label (optional)
706
+ - description: new description (optional)
707
+ - html: new HTML content (optional, max 200KB)
708
+ - data_bindings: new bindings array (optional, replaces all bindings)
709
+
710
+ Returns: {ok, view_id, html_size, binding_count}`;
711
+ var SM_CUSTOM_VIEW_DELETE_DESCRIPTION = `Delete a custom view.
712
+
713
+ Parameters:
714
+ - view_id: view to delete
715
+
716
+ Returns: {ok, view_id}`;
717
+ var SM_CUSTOM_VIEW_LIST_DESCRIPTION = `List all custom views with binding health status.
718
+
719
+ Use this to check which views exist and whether their data bindings are still valid. Each view includes binding_status for every binding:
720
+ - "ok": source exists and structure matches
721
+ - "stale": source exists but structure changed (stale_reason: "schema_changed", includes current vs saved fingerprint)
722
+ - "missing": source has been deleted
723
+
724
+ When bindings are stale, use sm_custom_view_get with include_data=true to fetch current data, then sm_custom_view_update to refresh the HTML.
725
+
726
+ Returns: {ok, views[]: {view_id, label, description, html_size, binding_count, binding_status[], created_at, updated_at}}`;
727
+ var SM_CUSTOM_VIEW_GET_DESCRIPTION = `Get a custom view with optional data injection.
728
+
729
+ Parameters:
730
+ - view_id: view to retrieve
731
+ - include_data: if true, resolve all bindings and return actual data in injected_data (default: false)
732
+
733
+ When include_data=true:
734
+ - injected_data maps binding_id to resolved data from the source
735
+ - Data is filtered by projection (fields, max_entries) if specified
736
+ - Secret parameter values are masked
737
+
738
+ binding_status shows current health of each binding (ok/stale/missing).
739
+
740
+ Returns: {ok, view, binding_status[], injected_data?}`;
741
+
742
+ // src/tools/semantic/sm-delete.ts
743
+ var SM_DELETE_DESCRIPTION = `Delete a semantic entity (page, target, action, or fetch).
744
+
745
+ Supports dry_run mode to preview deletion impact before committing.
746
+ Page deletion cascades to all dependent targets, actions, and fetches.
747
+ Target deletion warns about referencing actions and fetches.
748
+
749
+ Parameters:
750
+ - entity_type: 'page' | 'target' | 'action' | 'fetch'
751
+ - entity_id: the entity ID to delete
752
+ - dry_run (optional): if true, return impact preview without deleting
753
+
754
+ Returns:
755
+ - entity_type, entity_id, label
756
+ - dry_run: whether this was a preview
757
+ - cascaded: (page deletion) lists of deleted targets, actions, fetches
758
+ - warnings: (target deletion) referencing actions/fetches that may break`;
759
+
760
+ // src/tools/semantic/sm-export.ts
761
+ var SM_EXPORT_DESCRIPTION = `Export semantic configuration as JSON.
762
+
763
+ Exports pages, targets, actions, and fetches. Optionally filter by domain.
764
+ Secret parameter default values are stripped from the export.
765
+ Includes a SHA-256 checksum for integrity verification on import.
766
+
767
+ Parameters:
768
+ - domain (optional): export only pages matching this domain (subdomain-aware). If omitted, exports all.
769
+
770
+ Returns:
771
+ - SemanticExport JSON with schema_version, checksum, pages[], targets[], actions[], fetches[]`;
772
+
773
+ // src/tools/semantic/sm-fetch.ts
774
+ var SM_FETCH_DESCRIPTION = `Extract structured data from the current page using predefined extraction rules.
775
+
776
+ Resolves target elements and extracts text, attributes, lists, counts, or existence checks.
777
+ Applies output limits (max_chars, max_items) to prevent oversized responses.
778
+
779
+ Parameters:
780
+ - tabId: target tab
781
+ - fetch_id: the fetch definition to execute (from sm_capabilities)
782
+ - params: optional parameters for the fetch
783
+
784
+ Returns:
785
+ - ok: whether extraction succeeded
786
+ - data: extracted key-value data matching the fetch's return_schema
787
+ - page_id_before / page_id_after: page state tracking
788
+ - needs_rebind: true if any targets could not be resolved
789
+ - missing_targets[]: list of target IDs that failed to resolve
790
+ - hint: recovery guidance when errors occur
791
+
792
+ When needs_rebind is true, use sm_test_target to diagnose each failing target,
793
+ then sm_add_target with target_id to re-bind locators.
794
+ When fetch_id is not found, use sm_capabilities to discover available fetches.`;
795
+
796
+ // src/tools/semantic/sm-import.ts
797
+ var SM_IMPORT_DESCRIPTION = `Import semantic configuration from JSON.
798
+
799
+ Imports pages, targets, actions, and fetches from a SemanticExport JSON object.
800
+ Verifies checksum integrity before importing.
801
+
802
+ Parameters:
803
+ - data (required): SemanticExport JSON object (from sm_export)
804
+ - strategy (optional): "merge" (default) adds new entities, skips existing IDs.
805
+ "replace-domain" deletes all existing pages matching the import's domains
806
+ (with cascade to targets/actions/fetches), then inserts all import entities.
807
+
808
+ Returns:
809
+ - ok: whether import succeeded
810
+ - imported: number of entities imported
811
+ - skipped: number of entities skipped (existing IDs in merge mode)
812
+ - errors[]: any error messages`;
813
+
814
+ // src/tools/semantic/sm-invoke.ts
815
+ var SM_INVOKE_DESCRIPTION = `Execute a semantic action on the current page.
816
+
817
+ Runs a predefined sequence of steps (click, type, select, wait, etc.) using
818
+ stable element locators with automatic fallback. Handles navigation detection,
819
+ retry with jitter, and template parameter substitution.
820
+
821
+ Parameters:
822
+ - tabId: target tab
823
+ - action_id: the action to execute (from sm_capabilities)
824
+ - params: key-value parameters for template substitution (e.g., { username: "..." })
825
+
826
+ Returns:
827
+ - ok: whether all steps completed successfully
828
+ - completed_steps / total_steps: execution progress
829
+ - page_id_before / page_id_after: page state tracking (navigation detection)
830
+ - observations[]: human-readable log of what happened (secrets redacted)
831
+ - needs_rebind: true if any targets could not be resolved
832
+ - missing_targets[]: list of target IDs that failed to resolve
833
+ - hint: recovery guidance when errors occur
834
+
835
+ When needs_rebind is true, use sm_test_target to diagnose each failing target,
836
+ then sm_add_target with target_id to re-bind locators.
837
+ When action_id is not found, use sm_capabilities to discover available actions.`;
838
+
839
+ // src/tools/semantic/sm-job-cancel.ts
840
+ var SM_JOB_CANCEL_DESCRIPTION = `Cancel a running job and its currently executing scenario.
841
+
842
+ Remaining unexecuted entries are marked as skipped.
843
+
844
+ Parameters:
845
+ - tabId: tab where the job is running
846
+
847
+ Returns:
848
+ - ok: success status
849
+ - cancelled: true if a job was found and cancelled`;
850
+
851
+ // src/tools/semantic/sm-job-create.ts
852
+ var SM_JOB_CREATE_DESCRIPTION = `Create a reusable job that groups multiple scenarios for sequential batch execution.
853
+
854
+ Each entry specifies a scenario_id and optional parameter overrides. When executed, scenarios
855
+ run one after another on the same tab, each producing its own Report.
856
+
857
+ Parameters:
858
+ - label: job name (e.g., "Print Price Collection")
859
+ - description: optional description
860
+ - entries: ordered list of scenarios to execute
861
+ - scenario_id: existing scenario ID
862
+ - label: display label override (uses scenario label if omitted)
863
+ - params: parameter overrides for this scenario execution
864
+ - on_error: 'stop' (default) halts on first failure, 'skip_and_continue' skips failed and continues
865
+
866
+ Returns:
867
+ - job_id: the created job ID (JOB_xxxx)
868
+ - entry_count: number of entries`;
869
+
870
+ // src/tools/semantic/sm-job-delete.ts
871
+ var SM_JOB_DELETE_DESCRIPTION = `Delete a job definition.
872
+
873
+ Running executions are not affected \u2014 only the job definition is removed.
874
+
875
+ Parameters:
876
+ - job_id: job to delete
877
+
878
+ Returns:
879
+ - ok: success status
880
+ - job_id: deleted job ID`;
881
+
882
+ // src/tools/semantic/sm-job-history.ts
883
+ var SM_JOB_HISTORY_DESCRIPTION = `Get execution history for a job. Returns condensed snapshots that persist beyond report pruning.
884
+
885
+ Use this to review past executions, track trends over time (e.g. price changes), or select snapshots for comparison.
886
+
887
+ Parameters:
888
+ - job_id: job to get history for
889
+ - limit: max snapshots to return (default: 20, most recent first)
890
+
891
+ Returns:
892
+ - job_id, job_label
893
+ - snapshots[]: list of {snapshot_id, status, started_at, completed_at, duration_ms, scenario_count}`;
894
+ var SM_JOB_COMPARE_DESCRIPTION = `Compare two job executions to see data changes between them.
895
+
896
+ Matches scenarios by scenario_id, data groups by result_key, and rows by loop_params. Computes per-cell diffs with numeric deltas.
897
+
898
+ Parameters:
899
+ - job_id: the job to compare
900
+ - base_snapshot_id: older snapshot (optional, defaults to second-most-recent)
901
+ - compare_snapshot_id: newer snapshot (optional, defaults to most-recent)
902
+ - changed_only: if true, omit unchanged rows (default: false)
903
+
904
+ Returns:
905
+ - diffs[]: per-scenario, per-result_key diff with rows showing added/removed/modified/unchanged
906
+ - summary: total counts of changes`;
907
+ var SM_JOB_SNAPSHOT_GET_DESCRIPTION = `Get detailed data from a specific job snapshot.
908
+
909
+ Use this to inspect the full captured data for a single execution, including all scenario results and data groups.
910
+
911
+ Parameters:
912
+ - snapshot_id: the snapshot to retrieve (SNP_xxxx)
913
+
914
+ Returns:
915
+ - snapshot: full JobSnapshot with scenario_snapshots and data_groups`;
916
+
917
+ // src/tools/semantic/sm-job-list.ts
918
+ var SM_JOB_LIST_DESCRIPTION = `List all defined jobs with entry counts and configuration.
919
+
920
+ Returns:
921
+ - jobs: array of job summaries
922
+ - job_id, label, description, entry_count, on_error, created_at`;
923
+
924
+ // src/tools/semantic/sm-job-report-export.ts
925
+ var SM_JOB_REPORT_EXPORT_DESCRIPTION = `Export all scenario results from a job execution as aggregated JSON or CSV.
926
+
927
+ Combines data from all linked scenario reports into a single file.
928
+ CSV includes a job_entry column to distinguish scenarios.
929
+
930
+ Parameters:
931
+ - job_report_id: job report ID (JRP_xxxx)
932
+ - format: 'json' (default) or 'csv'
933
+
934
+ Returns:
935
+ - data: export content string
936
+ - filename: suggested filename`;
937
+
938
+ // src/tools/semantic/sm-job-report-get.ts
939
+ var SM_JOB_REPORT_GET_DESCRIPTION = `Get job execution status and results.
940
+
941
+ Shows overall progress and per-scenario report summaries.
942
+ Use sm_report_get with individual report_ids for detailed scenario data.
943
+
944
+ Parameters:
945
+ - job_report_id: job report ID (JRP_xxxx)
946
+
947
+ Returns:
948
+ - job_report: full JobReport with entry statuses
949
+ - scenario_summaries: per-scenario report summaries (entry_count, error_count)`;
950
+
951
+ // src/tools/semantic/sm-job-run.ts
952
+ var SM_JOB_RUN_DESCRIPTION = `Execute a job on a browser tab. Scenarios run sequentially.
953
+
954
+ Returns job_report_id immediately by default. Poll with sm_job_report_get to check progress.
955
+ Set wait_for_completion=true to block until all scenarios finish.
956
+
957
+ Only one job or scenario can run per tab at a time.
958
+
959
+ Parameters:
960
+ - tabId: target tab for execution
961
+ - job_id: the job to execute
962
+ - params: optional per-scenario param overrides keyed by scenario_id
963
+ - wait_for_completion: if true, wait for all scenarios to complete (default: false)
964
+
965
+ Returns:
966
+ - job_report_id: ID of the created job report (JRP_xxxx)
967
+ - job_id: the executed job
968
+ - status: 'running' (async) or 'completed'/'failed' (when wait_for_completion=true)`;
969
+
970
+ // src/tools/semantic/sm-job-update.ts
971
+ var SM_JOB_UPDATE_DESCRIPTION = `Update an existing job definition.
972
+
973
+ Supports partial updates \u2014 only specified fields are changed.
974
+
975
+ Parameters:
976
+ - job_id: job to update
977
+ - label: new name (optional)
978
+ - description: new description (optional)
979
+ - entries: new entry list (optional, replaces all entries)
980
+ - on_error: new error handling strategy (optional)
981
+
982
+ Returns:
983
+ - job_id: updated job ID
984
+ - entry_count: number of entries after update`;
985
+
986
+ // src/tools/semantic/sm-register-page.ts
987
+ var SM_REGISTER_PAGE_DESCRIPTION = `Register a page for semantic automation.
988
+
989
+ Creates a page definition with URL pattern matching and DOM markers.
990
+ Idempotent: if a registered page already matches the current tab, returns the existing page_id.
991
+ When page_type is specified on an already-registered page, it updates the page_type.
992
+
993
+ Pages are grouped by domain. Use page_type to distinguish different page types
994
+ within the same domain (e.g., "profile", "compose", "search").
995
+
996
+ Parameters:
997
+ - tabId: target tab (used to auto-derive url_pattern and domains if omitted)
998
+ - label: human-readable page name (e.g., "GitHub Login")
999
+ - page_type: page type within the domain (e.g., "profile", "compose", "search")
1000
+ - url_pattern: URL regex (auto-derived from tab URL if omitted)
1001
+ - domains: domain scope array (auto-derived from tab hostname if omitted)
1002
+ - auth_state: 'logged_in' | 'logged_out' | 'any' (default: 'any')
1003
+ - must_exist: CSS selectors that must be present on the page
1004
+ - must_not_exist: CSS selectors that must not be present
1005
+ - title_regex: regex to match page title
1006
+
1007
+ Returns:
1008
+ - page_id, page_type, variant_id, label
1009
+ - already_registered: true if an existing page matched
1010
+ - match_test: result of matching the current tab against the page definition`;
1011
+
1012
+ // src/tools/semantic/sm-report-delete.ts
1013
+ var SM_REPORT_DELETE_DESCRIPTION = `Delete an execution report.
1014
+
1015
+ Permanently removes a report and its data.
1016
+
1017
+ Parameters:
1018
+ - report_id: the report to delete
1019
+
1020
+ Returns:
1021
+ - ok: whether deletion succeeded
1022
+ - report_id: the deleted report ID`;
1023
+
1024
+ // src/tools/semantic/sm-report-export.ts
1025
+ var SM_REPORT_EXPORT_DESCRIPTION = `Export a report as JSON or CSV.
1026
+
1027
+ JSON format includes the complete report structure.
1028
+ CSV format flattens entries into rows with columns for loop parameters and data fields.
1029
+ CSV uses UTF-8 BOM for Excel compatibility with non-ASCII characters.
1030
+
1031
+ Parameters:
1032
+ - report_id: the report to export
1033
+ - format: 'json' (default) or 'csv'
1034
+
1035
+ Returns:
1036
+ - format: the export format used
1037
+ - data: the exported content as a string
1038
+ - filename: suggested filename for download`;
1039
+
1040
+ // src/tools/semantic/sm-report-get.ts
1041
+ var SM_REPORT_GET_DESCRIPTION = `Get detailed execution report.
1042
+
1043
+ Returns the full report including all data entries, observations, errors,
1044
+ and grouped data (entries organized by result_key).
1045
+
1046
+ Use this to poll running scenarios for progress updates.
1047
+
1048
+ Parameters:
1049
+ - report_id: the report to retrieve
1050
+
1051
+ Returns:
1052
+ - report: full Report object with entries[], grouped_data, observations[], errors[]
1053
+ - report.status: 'running' | 'completed' | 'failed' | 'cancelled'
1054
+ - report.entries[]: collected data with loop_params and extracted data
1055
+ - report.grouped_data: entries grouped by result_key for easy access`;
1056
+
1057
+ // src/tools/semantic/sm-report-list.ts
1058
+ var SM_REPORT_LIST_DESCRIPTION = `List execution reports.
1059
+
1060
+ Returns summaries of scenario execution reports, sorted by most recent first.
1061
+ Optionally filter by scenario_id.
1062
+
1063
+ Parameters:
1064
+ - scenario_id (optional): filter reports for a specific scenario
1065
+
1066
+ Returns:
1067
+ - reports[]: array of report summaries with status, timing, and entry/error counts`;
1068
+
1069
+ // src/tools/semantic/sm-scenario-create.ts
1070
+ var SM_SCENARIO_CREATE_DESCRIPTION = `Create a scenario for multi-page workflow automation.
1071
+
1072
+ Scenarios define sequences of steps (navigate, action, fetch, loop, wait) that execute
1073
+ across multiple pages. Loop steps support cartesian product iteration over variables
1074
+ (static values, select options, or previous fetch results).
1075
+
1076
+ Parameters:
1077
+ - label: scenario name (e.g., "Price Table Collection")
1078
+ - description: optional LLM-facing description
1079
+ - params: global parameter definitions (available as {{param_name}} templates)
1080
+ - steps: ordered step sequence
1081
+ - max_duration_ms: timeout (default: 300000ms = 5min)
1082
+ - on_error: 'stop' (default) or 'skip_and_continue'
1083
+
1084
+ Step types:
1085
+ - navigate: { url, wait_for_load } - navigate to URL (supports {{templates}})
1086
+ - action: { action_id, params } - execute an existing semantic action
1087
+ - fetch: { fetch_id, result_key, params } - extract data, stored in report under result_key
1088
+ - loop: { variables, steps, delay_between_ms, max_iterations } - iterate combinations
1089
+ - wait: { strategy, selector, timeout_ms } - wait for condition
1090
+
1091
+ Loop variables:
1092
+ - static: explicit value list
1093
+ - target_options: auto-extract all <option> values from a <select> target
1094
+ - previous_fetch: extract values from prior fetch results
1095
+
1096
+ Inside loops, use {{loop.variable_name}} to reference loop variables.
1097
+
1098
+ Returns:
1099
+ - scenario_id: the created scenario ID (SCN_xxxx)
1100
+ - step_count: total step count
1101
+ - validation: { valid, warnings[] } referential integrity check`;
1102
+
1103
+ // src/tools/semantic/sm-scenario-delete.ts
1104
+ var SM_SCENARIO_DELETE_DESCRIPTION = `Delete a scenario by ID.
1105
+
1106
+ Associated reports become orphaned but are not deleted.
1107
+
1108
+ Parameters:
1109
+ - scenario_id: the scenario to delete
1110
+
1111
+ Returns:
1112
+ - ok: whether deletion succeeded
1113
+ - scenario_id: the deleted scenario ID`;
1114
+
1115
+ // src/tools/semantic/sm-scenario-list.ts
1116
+ var SM_SCENARIO_LIST_DESCRIPTION = `List all registered scenarios.
1117
+
1118
+ Returns a summary of each scenario including ID, label, step count,
1119
+ parameter names, and creation timestamp.
1120
+
1121
+ No parameters required.
1122
+
1123
+ Returns:
1124
+ - scenarios[]: array of scenario summaries`;
1125
+
1126
+ // src/tools/semantic/sm-scenario-run.ts
1127
+ var SM_SCENARIO_RUN_DESCRIPTION = `Execute a scenario on a tab.
1128
+
1129
+ Starts scenario execution and returns immediately with a report_id.
1130
+ Use sm_report_get to poll for progress and final results.
1131
+
1132
+ Only one scenario can run per tab at a time.
325
1133
 
326
- Use "back" to go back in history, "forward" to go forward.
327
- Provide a full URL (with protocol) to navigate to a new page.
328
- Waits for page load to complete before returning.`;
1134
+ Parameters:
1135
+ - tabId: target tab for execution
1136
+ - scenario_id: the scenario to execute
1137
+ - params: parameter values for template substitution
1138
+ - wait_for_completion: if true, wait for scenario to finish (up to max_duration_ms)
329
1139
 
330
- // src/tools/core/read-page.ts
331
- var READ_PAGE_DESCRIPTION = `Get accessibility tree representation of page elements.
1140
+ Returns:
1141
+ - report_id: ID of the created report (RPT_xxxx)
1142
+ - scenario_id: the executed scenario
1143
+ - status: 'running' (async) or 'completed'/'failed' (when wait_for_completion=true)
1144
+ - error: error message if startup failed`;
332
1145
 
333
- Supports:
334
- - filter: "interactive" for buttons/links/inputs only, "all" for everything
335
- - depth: max tree depth (1-20, default 8)
336
- - refId: focus on a specific element subtree
337
- - maxChars: limit output size (default 50000)
1146
+ // src/tools/semantic/sm-scenario-update.ts
1147
+ var SM_SCENARIO_UPDATE_DESCRIPTION = `Update an existing scenario.
338
1148
 
339
- Returns elements with ref IDs that can be used with click, form_input, etc.`;
1149
+ Supports partial updates - only provide fields you want to change.
340
1150
 
341
- // src/tools/core/screenshot.ts
342
- var SCREENSHOT_DESCRIPTION = `Take a screenshot of a tab.
1151
+ Parameters:
1152
+ - scenario_id: the scenario to update
1153
+ - label: new name
1154
+ - description: new description
1155
+ - params: new parameter definitions
1156
+ - steps: new step sequence
1157
+ - max_duration_ms: new timeout
1158
+ - on_error: new error handling strategy
343
1159
 
344
- Returns base64-encoded image data.
345
- Default format is JPEG with quality 80 (optimized for Native Messaging 1MB limit).
346
- Use PNG for lossless screenshots when needed.
347
- Optionally capture a specific region [x0, y0, x1, y1].`;
1160
+ Returns:
1161
+ - scenario_id, step_count, validation`;
348
1162
 
349
- // src/tools/core/scroll.ts
350
- var SCROLL_DESCRIPTION = `Scroll in a direction at given coordinates, or scroll an element into view by ref.
1163
+ // src/tools/semantic/sm-status.ts
1164
+ var SM_STATUS_DESCRIPTION = `View all semantic configuration (pages, targets, actions, fetches).
351
1165
 
352
- Two modes:
353
- 1. Directional scroll: provide coordinate + direction (+ optional amount 1-10, default 3).
354
- 2. Scroll to element: provide ref (element reference ID from read_page/find). Scrolls the element into view using smooth scrolling.`;
1166
+ Returns a summary and detailed listing of all registered entities.
1167
+ Optionally test-resolve all targets against the current tab.
355
1168
 
356
- // src/tools/core/type.ts
357
- var TYPE_DESCRIPTION = `Type text into the currently focused element.
1169
+ Parameters:
1170
+ - tabId (optional): required when test_resolve is true
1171
+ - page_id (optional): filter results to a specific page
1172
+ - domain (optional): filter results to pages matching this domain
1173
+ - test_resolve (optional): if true, resolve all targets on the current tab
358
1174
 
359
- Each character is typed individually with keyDown/keyUp events.
360
- For special keys (Enter, Tab, etc.), use the 'key' tool instead.`;
1175
+ Returns:
1176
+ - summary: {total_pages, total_targets, total_actions, total_fetches}
1177
+ - pages[]: page definitions with page_type, domains, url_patterns, and counts
1178
+ - targets[]: targets with locator info and optional resolve_result
1179
+ - actions[]: actions with step count and param names
1180
+ - fetches[]: fetches with field count
1181
+ - integrity[]: referential integrity warnings`;
361
1182
 
362
- // src/tools/core/wait-for.ts
363
- var WAIT_FOR_DESCRIPTION = `Wait for a specified condition before proceeding.
364
- Supports waiting for a CSS selector to appear in the DOM,
365
- for a navigation event to complete, or for a fixed timeout duration.
366
- Useful for synchronizing with asynchronous page updates.`;
1183
+ // src/tools/semantic/sm-test-target.ts
1184
+ var SM_TEST_TARGET_DESCRIPTION = `Test a specific target's locators against the current page.
367
1185
 
368
- // src/tools/debug/read-console-messages.ts
369
- var READ_CONSOLE_MESSAGES_DESCRIPTION = `Read console messages captured from the page.
370
- Returns log, warning, error, and info messages that have been
371
- emitted by page scripts. Supports filtering by log level
372
- and limiting the number of returned entries.`;
1186
+ Resolves each locator individually and reports which ones matched.
1187
+ Useful for diagnosing broken targets and deciding which locators to update.
373
1188
 
374
- // src/tools/debug/read-network-requests.ts
375
- var READ_NETWORK_REQUESTS_DESCRIPTION = `Read network requests captured from the page.
376
- Returns details of HTTP requests and responses including URL, method,
377
- status code, headers, and timing information. Supports filtering
378
- by URL pattern or resource type.`;
1189
+ Parameters:
1190
+ - tabId: target tab to test against
1191
+ - target_id: the target to test
1192
+
1193
+ Returns:
1194
+ - found: whether any locator resolved successfully
1195
+ - locator_results[]: per-locator results {type, value, stability, matched}
1196
+ - element: resolved element info {tag, text, rect} if found
1197
+ - test_status: updated test status saved to storage`;
379
1198
 
380
1199
  // src/tools/tabs/select-tab.ts
381
1200
  var SELECT_TAB_DESCRIPTION = `Switch focus to a specific tab by its identifier.
@@ -415,7 +1234,7 @@ in each group.`;
415
1234
 
416
1235
  // src/tools/viyv/artifact-from-page.ts
417
1236
  var ARTIFACT_FROM_PAGE_DESCRIPTION = `Save the current page as a persistent artifact.
418
- Captures the page content as HTML, PDF, or screenshot and stores it
1237
+ Captures the page content as HTML, text, or screenshot and stores it
419
1238
  as a named artifact for later reference. Artifacts can be retrieved
420
1239
  by other tools or returned to the user as output.`;
421
1240
 
@@ -439,16 +1258,17 @@ diagnostic information useful for troubleshooting connectivity issues.`;
439
1258
 
440
1259
  // src/tools/viyv/page-data-extract.ts
441
1260
  var PAGE_DATA_EXTRACT_DESCRIPTION = `Extract structured data from the current page.
442
- Parses the page content according to a provided schema or natural
443
- language description, returning well-formed JSON. Supports extracting
444
- tables, lists, key-value pairs, and other structured patterns.`;
1261
+ Parses the page content according to a provided schema object,
1262
+ returning well-formed JSON. Supports extracting tables, lists,
1263
+ key-value pairs, and other structured patterns.
1264
+ The schema defines CSS selectors for each field to extract.`;
445
1265
 
446
1266
  // src/tools/index.ts
447
1267
  var navigateTool = {
448
1268
  name: "navigate",
449
1269
  description: NAVIGATE_DESCRIPTION,
450
1270
  inputSchema: z.object({
451
- tabId: z.number().describe("Tab ID to navigate"),
1271
+ tabId: z.coerce.number().describe("Tab ID to navigate"),
452
1272
  url: z.string().describe('URL to navigate to, or "back"/"forward" for history')
453
1273
  })
454
1274
  };
@@ -456,18 +1276,21 @@ var screenshotTool = {
456
1276
  name: "screenshot",
457
1277
  description: SCREENSHOT_DESCRIPTION,
458
1278
  inputSchema: z.object({
459
- tabId: z.number().describe("Tab ID to capture"),
1279
+ tabId: z.coerce.number().describe("Tab ID to capture"),
460
1280
  format: z.enum(["jpeg", "png"]).optional().describe("Image format (default: jpeg)"),
461
- quality: z.number().min(1).max(100).optional().describe("JPEG quality (default: 80)"),
462
- region: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional().describe("Capture region [x0, y0, x1, y1]")
1281
+ quality: z.coerce.number().min(1).max(100).optional().describe("JPEG quality (default: 80)"),
1282
+ region: z.tuple([z.coerce.number(), z.coerce.number(), z.coerce.number(), z.coerce.number()]).optional().describe("Capture region [x0, y0, x1, y1]"),
1283
+ ref: z.string().optional().describe(
1284
+ "Element reference ID from read_page or find. Captures only that element with padding. Ignored if region is also provided."
1285
+ )
463
1286
  })
464
1287
  };
465
1288
  var clickTool = {
466
1289
  name: "click",
467
1290
  description: CLICK_DESCRIPTION,
468
1291
  inputSchema: z.object({
469
- tabId: z.number().describe("Tab ID"),
470
- coordinate: z.tuple([z.number(), z.number()]).optional().describe("Click position [x, y]"),
1292
+ tabId: z.coerce.number().describe("Tab ID"),
1293
+ coordinate: z.tuple([z.coerce.number(), z.coerce.number()]).optional().describe("Click position [x, y]"),
471
1294
  ref: z.string().optional().describe("Element reference ID"),
472
1295
  action: z.enum(["left_click", "right_click", "double_click", "triple_click"]).optional().describe("Click type (default: left_click)"),
473
1296
  modifiers: z.string().optional().describe('Modifier keys (e.g., "ctrl+shift")')
@@ -477,7 +1300,7 @@ var typeTool = {
477
1300
  name: "type",
478
1301
  description: TYPE_DESCRIPTION,
479
1302
  inputSchema: z.object({
480
- tabId: z.number().describe("Tab ID"),
1303
+ tabId: z.coerce.number().describe("Tab ID"),
481
1304
  text: z.string().describe("Text to type")
482
1305
  })
483
1306
  };
@@ -485,19 +1308,19 @@ var keyTool = {
485
1308
  name: "key",
486
1309
  description: KEY_DESCRIPTION,
487
1310
  inputSchema: z.object({
488
- tabId: z.number().describe("Tab ID"),
1311
+ tabId: z.coerce.number().describe("Tab ID"),
489
1312
  keys: z.string().describe('Space-separated keys (e.g., "Enter", "ctrl+a")'),
490
- repeat: z.number().min(1).max(100).optional().describe("Repeat count")
1313
+ repeat: z.coerce.number().min(1).max(100).optional().describe("Repeat count")
491
1314
  })
492
1315
  };
493
1316
  var scrollTool = {
494
1317
  name: "scroll",
495
1318
  description: SCROLL_DESCRIPTION,
496
1319
  inputSchema: z.object({
497
- tabId: z.number().describe("Tab ID"),
498
- coordinate: z.tuple([z.number(), z.number()]).optional().describe("Scroll position [x, y] (required for directional scroll)"),
1320
+ tabId: z.coerce.number().describe("Tab ID"),
1321
+ coordinate: z.tuple([z.coerce.number(), z.coerce.number()]).optional().describe("Scroll position [x, y] (required for directional scroll)"),
499
1322
  direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction (required for directional scroll)"),
500
- amount: z.number().min(1).max(10).optional().describe("Scroll amount (default: 3)"),
1323
+ amount: z.coerce.number().min(1).max(10).optional().describe("Scroll amount (default: 3)"),
501
1324
  ref: z.string().optional().describe("Element reference ID to scroll into view")
502
1325
  })
503
1326
  };
@@ -505,8 +1328,8 @@ var hoverTool = {
505
1328
  name: "hover",
506
1329
  description: HOVER_DESCRIPTION,
507
1330
  inputSchema: z.object({
508
- tabId: z.number().describe("Tab ID"),
509
- coordinate: z.tuple([z.number(), z.number()]).optional().describe("Hover position [x, y]"),
1331
+ tabId: z.coerce.number().describe("Tab ID"),
1332
+ coordinate: z.tuple([z.coerce.number(), z.coerce.number()]).optional().describe("Hover position [x, y]"),
510
1333
  ref: z.string().optional().describe("Element reference ID")
511
1334
  })
512
1335
  };
@@ -514,27 +1337,27 @@ var dragTool = {
514
1337
  name: "drag",
515
1338
  description: DRAG_DESCRIPTION,
516
1339
  inputSchema: z.object({
517
- tabId: z.number().describe("Tab ID"),
518
- startCoordinate: z.tuple([z.number(), z.number()]).describe("Start position [x, y]"),
519
- endCoordinate: z.tuple([z.number(), z.number()]).describe("End position [x, y]")
1340
+ tabId: z.coerce.number().describe("Tab ID"),
1341
+ startCoordinate: z.tuple([z.coerce.number(), z.coerce.number()]).describe("Start position [x, y]"),
1342
+ endCoordinate: z.tuple([z.coerce.number(), z.coerce.number()]).describe("End position [x, y]")
520
1343
  })
521
1344
  };
522
1345
  var readPageTool = {
523
1346
  name: "read_page",
524
1347
  description: READ_PAGE_DESCRIPTION,
525
1348
  inputSchema: z.object({
526
- tabId: z.number().describe("Tab ID"),
1349
+ tabId: z.coerce.number().describe("Tab ID"),
527
1350
  filter: z.enum(["interactive", "all"]).optional().describe('Filter: "interactive" for buttons/links/inputs, "all" for everything'),
528
- depth: z.number().min(1).max(20).optional().describe("Max tree depth (default: 8)"),
1351
+ depth: z.coerce.number().min(1).max(20).optional().describe("Max tree depth (default: 15)"),
529
1352
  refId: z.string().optional().describe("Focus on a specific element by ref"),
530
- maxChars: z.number().optional().describe("Max output characters (default: 50000)")
1353
+ maxChars: z.coerce.number().optional().describe("Max output characters (default: 50000)")
531
1354
  })
532
1355
  };
533
1356
  var findTool = {
534
1357
  name: "find",
535
1358
  description: FIND_DESCRIPTION,
536
1359
  inputSchema: z.object({
537
- tabId: z.number().describe("Tab ID"),
1360
+ tabId: z.coerce.number().describe("Tab ID"),
538
1361
  query: z.string().describe("Natural language description of what to find")
539
1362
  })
540
1363
  };
@@ -542,16 +1365,16 @@ var formInputTool = {
542
1365
  name: "form_input",
543
1366
  description: FORM_INPUT_DESCRIPTION,
544
1367
  inputSchema: z.object({
545
- tabId: z.number().describe("Tab ID"),
1368
+ tabId: z.coerce.number().describe("Tab ID"),
546
1369
  ref: z.string().describe("Element reference ID"),
547
- value: z.union([z.string(), z.boolean(), z.number()]).describe("Value to set")
1370
+ value: z.union([z.string(), z.boolean(), z.coerce.number()]).describe("Value to set")
548
1371
  })
549
1372
  };
550
1373
  var javascriptExecTool = {
551
1374
  name: "javascript_exec",
552
1375
  description: JAVASCRIPT_EXEC_DESCRIPTION,
553
1376
  inputSchema: z.object({
554
- tabId: z.number().describe("Tab ID"),
1377
+ tabId: z.coerce.number().describe("Tab ID"),
555
1378
  code: z.string().describe("JavaScript code to execute")
556
1379
  })
557
1380
  };
@@ -559,24 +1382,24 @@ var waitForTool = {
559
1382
  name: "wait_for",
560
1383
  description: WAIT_FOR_DESCRIPTION,
561
1384
  inputSchema: z.object({
562
- tabId: z.number().describe("Tab ID"),
1385
+ tabId: z.coerce.number().describe("Tab ID"),
563
1386
  selector: z.string().optional().describe("CSS selector to wait for"),
564
1387
  navigation: z.boolean().optional().describe("Wait for navigation to complete"),
565
- timeout: z.number().optional().describe("Timeout in ms (default: 30000)")
1388
+ timeout: z.coerce.number().optional().describe("Timeout in ms (default: 30000)")
566
1389
  })
567
1390
  };
568
1391
  var getPageTextTool = {
569
1392
  name: "get_page_text",
570
1393
  description: GET_PAGE_TEXT_DESCRIPTION,
571
1394
  inputSchema: z.object({
572
- tabId: z.number().describe("Tab ID")
1395
+ tabId: z.coerce.number().describe("Tab ID")
573
1396
  })
574
1397
  };
575
1398
  var handleDialogTool = {
576
1399
  name: "handle_dialog",
577
1400
  description: HANDLE_DIALOG_DESCRIPTION,
578
1401
  inputSchema: z.object({
579
- tabId: z.number().describe("Tab ID"),
1402
+ tabId: z.coerce.number().describe("Tab ID"),
580
1403
  action: z.enum(["accept", "dismiss"]).describe("Dialog action"),
581
1404
  text: z.string().optional().describe("Text for prompt dialog")
582
1405
  })
@@ -599,24 +1422,24 @@ var tabCloseTool = {
599
1422
  name: "tab_close",
600
1423
  description: TAB_CLOSE_DESCRIPTION,
601
1424
  inputSchema: z.object({
602
- tabId: z.number().describe("Tab ID to close")
1425
+ tabId: z.coerce.number().describe("Tab ID to close")
603
1426
  })
604
1427
  };
605
1428
  var selectTabTool = {
606
1429
  name: "select_tab",
607
1430
  description: SELECT_TAB_DESCRIPTION,
608
1431
  inputSchema: z.object({
609
- tabId: z.number().describe("Tab ID to focus")
1432
+ tabId: z.coerce.number().describe("Tab ID to focus")
610
1433
  })
611
1434
  };
612
1435
  var readConsoleMessagesTool = {
613
1436
  name: "read_console_messages",
614
1437
  description: READ_CONSOLE_MESSAGES_DESCRIPTION,
615
1438
  inputSchema: z.object({
616
- tabId: z.number().describe("Tab ID"),
1439
+ tabId: z.coerce.number().describe("Tab ID"),
617
1440
  pattern: z.string().optional().describe("Regex pattern to filter messages"),
618
1441
  onlyErrors: z.boolean().optional().describe("Only return errors"),
619
- limit: z.number().optional().describe("Max messages to return (default: 100)"),
1442
+ limit: z.coerce.number().optional().describe("Max messages to return (default: 100)"),
620
1443
  clear: z.boolean().optional().describe("Clear messages after reading")
621
1444
  })
622
1445
  };
@@ -624,9 +1447,9 @@ var readNetworkRequestsTool = {
624
1447
  name: "read_network_requests",
625
1448
  description: READ_NETWORK_REQUESTS_DESCRIPTION,
626
1449
  inputSchema: z.object({
627
- tabId: z.number().describe("Tab ID"),
1450
+ tabId: z.coerce.number().describe("Tab ID"),
628
1451
  urlPattern: z.string().optional().describe("URL pattern to filter requests"),
629
- limit: z.number().optional().describe("Max requests to return (default: 100)"),
1452
+ limit: z.coerce.number().optional().describe("Max requests to return (default: 100)"),
630
1453
  clear: z.boolean().optional().describe("Clear requests after reading")
631
1454
  })
632
1455
  };
@@ -634,7 +1457,7 @@ var gifCreatorTool = {
634
1457
  name: "gif_creator",
635
1458
  description: GIF_CREATOR_DESCRIPTION,
636
1459
  inputSchema: z.object({
637
- tabId: z.number().describe("Tab ID"),
1460
+ tabId: z.coerce.number().describe("Tab ID"),
638
1461
  action: z.enum(["start_recording", "stop_recording", "export", "clear"]).describe("GIF action"),
639
1462
  filename: z.string().optional().describe("Filename for export"),
640
1463
  options: z.object({
@@ -643,7 +1466,7 @@ var gifCreatorTool = {
643
1466
  showActionLabels: z.boolean().optional(),
644
1467
  showProgressBar: z.boolean().optional(),
645
1468
  showWatermark: z.boolean().optional(),
646
- quality: z.number().min(1).max(30).optional()
1469
+ quality: z.coerce.number().min(1).max(30).optional()
647
1470
  }).optional().describe("GIF rendering options"),
648
1471
  download: z.boolean().optional().describe("Download the GIF")
649
1472
  })
@@ -652,10 +1475,11 @@ var uploadImageTool = {
652
1475
  name: "upload_image",
653
1476
  description: UPLOAD_IMAGE_DESCRIPTION,
654
1477
  inputSchema: z.object({
655
- tabId: z.number().describe("Tab ID"),
1478
+ tabId: z.coerce.number().describe("Tab ID"),
656
1479
  imageId: z.string().describe("Image ID from a previous screenshot"),
657
1480
  ref: z.string().optional().describe("Element reference for file input"),
658
- coordinate: z.tuple([z.number(), z.number()]).optional().describe("Coordinates for drag & drop")
1481
+ coordinate: z.tuple([z.coerce.number(), z.coerce.number()]).optional().describe("Coordinates for drag & drop"),
1482
+ filename: z.string().optional().describe('Filename for the uploaded file (default: "image.png")')
659
1483
  })
660
1484
  };
661
1485
  var updatePlanTool = {
@@ -670,23 +1494,23 @@ var resizeWindowTool = {
670
1494
  name: "resize_window",
671
1495
  description: RESIZE_WINDOW_DESCRIPTION,
672
1496
  inputSchema: z.object({
673
- tabId: z.number().describe("Tab ID"),
674
- width: z.number().describe("Window width in pixels"),
675
- height: z.number().describe("Window height in pixels")
1497
+ tabId: z.coerce.number().describe("Tab ID"),
1498
+ width: z.coerce.number().describe("Window width in pixels"),
1499
+ height: z.coerce.number().describe("Window height in pixels")
676
1500
  })
677
1501
  };
678
1502
  var shortcutsListTool = {
679
1503
  name: "shortcuts_list",
680
1504
  description: SHORTCUTS_LIST_DESCRIPTION,
681
1505
  inputSchema: z.object({
682
- tabId: z.number().optional().describe("Tab ID (used to identify the tab group context)")
1506
+ tabId: z.coerce.number().optional().describe("Tab ID (used to identify the tab group context)")
683
1507
  })
684
1508
  };
685
1509
  var shortcutsExecuteTool = {
686
1510
  name: "shortcuts_execute",
687
1511
  description: SHORTCUTS_EXECUTE_DESCRIPTION,
688
1512
  inputSchema: z.object({
689
- tabId: z.number().describe("Tab ID to execute the shortcut on"),
1513
+ tabId: z.coerce.number().describe("Tab ID to execute the shortcut on"),
690
1514
  command: z.string().optional().describe("Command name of the shortcut"),
691
1515
  shortcutId: z.string().optional().describe("ID of the shortcut")
692
1516
  })
@@ -696,6 +1520,15 @@ var switchBrowserTool = {
696
1520
  description: SWITCH_BROWSER_DESCRIPTION,
697
1521
  inputSchema: z.object({})
698
1522
  };
1523
+ var fileUploadTool = {
1524
+ name: "file_upload",
1525
+ description: FILE_UPLOAD_DESCRIPTION,
1526
+ inputSchema: z.object({
1527
+ tabId: z.coerce.number().describe("Tab ID"),
1528
+ ref: z.string().describe("Element reference ID of the file input"),
1529
+ paths: z.array(z.string()).min(1).describe("Absolute file paths to upload")
1530
+ })
1531
+ };
699
1532
  var agentTabAssignTool = {
700
1533
  name: "agent_tab_assign",
701
1534
  description: AGENT_TAB_ASSIGN_DESCRIPTION,
@@ -730,7 +1563,7 @@ var artifactFromPageTool = {
730
1563
  name: "artifact_from_page",
731
1564
  description: ARTIFACT_FROM_PAGE_DESCRIPTION,
732
1565
  inputSchema: z.object({
733
- tabId: z.number().describe("Tab ID"),
1566
+ tabId: z.coerce.number().describe("Tab ID"),
734
1567
  type: z.string().describe("Artifact type (text, html, screenshot)"),
735
1568
  title: z.string().optional().describe("Artifact title")
736
1569
  })
@@ -739,7 +1572,7 @@ var pageDataExtractTool = {
739
1572
  name: "page_data_extract",
740
1573
  description: PAGE_DATA_EXTRACT_DESCRIPTION,
741
1574
  inputSchema: z.object({
742
- tabId: z.number().describe("Tab ID"),
1575
+ tabId: z.coerce.number().describe("Tab ID"),
743
1576
  schema: z.record(z.unknown()).describe("Data extraction schema"),
744
1577
  selector: z.string().optional().describe("CSS selector to scope extraction")
745
1578
  })
@@ -749,6 +1582,489 @@ var browserHealthTool = {
749
1582
  description: BROWSER_HEALTH_DESCRIPTION,
750
1583
  inputSchema: z.object({})
751
1584
  };
1585
+ var smCapabilitiesTool = {
1586
+ name: "sm_capabilities",
1587
+ description: SM_CAPABILITIES_DESCRIPTION,
1588
+ inputSchema: z.object({
1589
+ tabId: z.coerce.number().describe("Tab ID")
1590
+ })
1591
+ };
1592
+ var smInvokeTool = {
1593
+ name: "sm_invoke",
1594
+ description: SM_INVOKE_DESCRIPTION,
1595
+ inputSchema: z.object({
1596
+ tabId: z.coerce.number().describe("Tab ID"),
1597
+ action_id: z.string().describe("Action ID to execute"),
1598
+ params: z.record(z.unknown()).optional().describe("Parameters for template substitution")
1599
+ })
1600
+ };
1601
+ var smFetchTool = {
1602
+ name: "sm_fetch",
1603
+ description: SM_FETCH_DESCRIPTION,
1604
+ inputSchema: z.object({
1605
+ tabId: z.coerce.number().describe("Tab ID"),
1606
+ fetch_id: z.string().describe("Fetch definition ID to execute"),
1607
+ params: z.record(z.unknown()).optional().describe("Optional parameters")
1608
+ })
1609
+ };
1610
+ var smRegisterPageTool = {
1611
+ name: "sm_register_page",
1612
+ description: SM_REGISTER_PAGE_DESCRIPTION,
1613
+ inputSchema: z.object({
1614
+ tabId: z.coerce.number().describe("Target tab"),
1615
+ label: z.string().describe('Page name (e.g., "GitHub Login")'),
1616
+ page_type: z.string().optional().describe(
1617
+ 'Page type within the domain (e.g., "profile", "compose", "search"). Used for grouping pages.'
1618
+ ),
1619
+ url_pattern: z.string().optional().describe("URL regex (auto-derived from tab URL if omitted)"),
1620
+ domains: z.array(z.string()).optional().describe("Domain scope (auto-derived if omitted)"),
1621
+ auth_state: z.enum(["logged_in", "logged_out", "any"]).optional().describe("Auth state (default: any)"),
1622
+ must_exist: z.array(z.string()).optional().describe("Required CSS selectors"),
1623
+ must_not_exist: z.array(z.string()).optional().describe("Forbidden CSS selectors"),
1624
+ title_regex: z.string().optional().describe("Title regex")
1625
+ })
1626
+ };
1627
+ var smAddTargetTool = {
1628
+ name: "sm_add_target",
1629
+ description: SM_ADD_TARGET_DESCRIPTION,
1630
+ inputSchema: z.object({
1631
+ tabId: z.coerce.number().describe("Target tab where elements are present"),
1632
+ page_id: z.string().describe("Page ID these targets belong to"),
1633
+ targets: z.array(
1634
+ z.object({
1635
+ target_id: z.string().optional().describe("Existing target ID for re-bind"),
1636
+ label: z.string().describe("Target name"),
1637
+ role: z.string().describe('Semantic role (e.g., "textbox", "button")'),
1638
+ ref: z.string().optional().describe("Element ref from read_page/find"),
1639
+ locators: z.array(
1640
+ z.object({
1641
+ type: z.string().describe("Locator type"),
1642
+ value: z.string().describe("Locator value"),
1643
+ stability: z.enum(["high", "medium", "low"]).describe("Stability level")
1644
+ })
1645
+ ).optional().describe("Explicit locators")
1646
+ })
1647
+ ).min(1).describe("Target definitions")
1648
+ })
1649
+ };
1650
+ var smAddActionTool = {
1651
+ name: "sm_add_action",
1652
+ description: SM_ADD_ACTION_DESCRIPTION,
1653
+ inputSchema: z.object({
1654
+ page_id: z.string().describe("Page ID"),
1655
+ label: z.string().describe("Action name"),
1656
+ verb: z.string().describe('Action verb (e.g., "login", "submit")'),
1657
+ description: z.string().optional().describe("LLM-facing description"),
1658
+ params: z.array(
1659
+ z.object({
1660
+ name: z.string(),
1661
+ type: z.enum(["string", "number", "boolean"]).default("string"),
1662
+ required: z.boolean().default(true),
1663
+ secret: z.boolean().default(false),
1664
+ description: z.string().optional(),
1665
+ default_value: z.union([z.string(), z.coerce.number(), z.boolean()]).optional()
1666
+ })
1667
+ ).optional().describe("Parameter definitions"),
1668
+ steps: z.array(
1669
+ z.object({
1670
+ type: z.enum(["click", "type", "select", "wait", "scroll", "key", "navigate", "file_upload"]).describe("Step type"),
1671
+ target_id: z.string().optional().describe("Target to act on"),
1672
+ params: z.object({
1673
+ text: z.string().optional(),
1674
+ value: z.string().optional(),
1675
+ keys: z.string().optional(),
1676
+ direction: z.enum(["up", "down", "left", "right"]).optional(),
1677
+ amount: z.coerce.number().optional(),
1678
+ url: z.string().optional(),
1679
+ param_ref: z.string().optional(),
1680
+ file_paths: z.array(z.string()).optional().describe("File paths for file_upload step")
1681
+ }).optional().describe("Step parameters"),
1682
+ pre_wait: z.object({
1683
+ strategy: z.enum(["selector", "navigation", "time"]),
1684
+ selector: z.string().optional(),
1685
+ timeout_ms: z.coerce.number().default(5e3)
1686
+ }).optional().describe("Pre-step wait rule"),
1687
+ description: z.string().optional()
1688
+ })
1689
+ ).min(1).describe("Step sequence"),
1690
+ retry_policy: z.object({
1691
+ max_retries: z.coerce.number().min(0).max(5).optional(),
1692
+ backoff: z.enum(["fixed", "exponential"]).optional(),
1693
+ base_delay_ms: z.coerce.number().min(100).max(1e4).optional(),
1694
+ jitter: z.boolean().optional()
1695
+ }).optional().describe("Retry policy")
1696
+ })
1697
+ };
1698
+ var smAddFetchTool = {
1699
+ name: "sm_add_fetch",
1700
+ description: SM_ADD_FETCH_DESCRIPTION,
1701
+ inputSchema: z.object({
1702
+ page_id: z.string().describe("Page ID"),
1703
+ label: z.string().describe("Fetch name"),
1704
+ type_name: z.string().describe('Output type name (e.g., "posts")'),
1705
+ description: z.string().optional().describe("LLM-facing description for discovery via sm_capabilities"),
1706
+ fields: z.array(
1707
+ z.object({
1708
+ name: z.string().describe("Output field name"),
1709
+ target_id: z.string().describe("Target element"),
1710
+ extraction_type: z.enum(["text", "attribute", "list", "count", "exists"]).describe("Extraction type"),
1711
+ attribute: z.string().optional().describe("Attribute name (for attribute type)"),
1712
+ item_selector: z.string().optional().describe("List item CSS selector"),
1713
+ item_fields: z.array(
1714
+ z.object({
1715
+ name: z.string(),
1716
+ selector: z.string().describe(
1717
+ 'CSS selector relative to each list item (use ":scope" for item itself)'
1718
+ ),
1719
+ extraction_type: z.enum(["text", "attribute", "exists"]),
1720
+ attribute: z.string().optional()
1721
+ })
1722
+ ).optional().describe("Nested fields for structured list item extraction")
1723
+ })
1724
+ ).min(1).describe("Extraction fields"),
1725
+ limits: z.object({
1726
+ max_chars: z.coerce.number().min(1e3).max(2e5).optional(),
1727
+ max_items: z.coerce.number().min(1).max(1e3).optional()
1728
+ }).optional().describe("Output limits")
1729
+ })
1730
+ };
1731
+ var smStatusTool = {
1732
+ name: "sm_status",
1733
+ description: SM_STATUS_DESCRIPTION,
1734
+ inputSchema: z.object({
1735
+ tabId: z.coerce.number().optional().describe("Tab ID (required for test_resolve)"),
1736
+ page_id: z.string().optional().describe("Filter by page ID"),
1737
+ domain: z.string().optional().describe("Filter results to pages matching this domain"),
1738
+ test_resolve: z.boolean().optional().describe("Test-resolve all targets")
1739
+ })
1740
+ };
1741
+ var smTestTargetTool = {
1742
+ name: "sm_test_target",
1743
+ description: SM_TEST_TARGET_DESCRIPTION,
1744
+ inputSchema: z.object({
1745
+ tabId: z.coerce.number().describe("Tab ID"),
1746
+ target_id: z.string().describe("Target ID to test")
1747
+ })
1748
+ };
1749
+ var smDeleteTool = {
1750
+ name: "sm_delete",
1751
+ description: SM_DELETE_DESCRIPTION,
1752
+ inputSchema: z.object({
1753
+ entity_type: z.enum(["page", "target", "action", "fetch"]).describe("Entity type"),
1754
+ entity_id: z.string().describe("Entity ID to delete"),
1755
+ dry_run: z.boolean().optional().describe("Preview impact without deleting")
1756
+ })
1757
+ };
1758
+ var smExportTool = {
1759
+ name: "sm_export",
1760
+ description: SM_EXPORT_DESCRIPTION,
1761
+ inputSchema: z.object({
1762
+ domain: z.string().optional().describe("Export only pages matching this domain (subdomain-aware). Omit for all.")
1763
+ })
1764
+ };
1765
+ var smImportTool = {
1766
+ name: "sm_import",
1767
+ description: SM_IMPORT_DESCRIPTION,
1768
+ inputSchema: z.object({
1769
+ data: z.object({
1770
+ schema_version: z.string(),
1771
+ checksum: z.string(),
1772
+ exported_at: z.coerce.number().optional(),
1773
+ source: z.record(z.unknown()).optional(),
1774
+ pages: z.array(z.record(z.unknown())),
1775
+ targets: z.array(z.record(z.unknown())).optional(),
1776
+ actions: z.array(z.record(z.unknown())).optional(),
1777
+ fetches: z.array(z.record(z.unknown())).optional()
1778
+ }).passthrough().describe("SemanticExport JSON object from sm_export"),
1779
+ strategy: z.enum(["merge", "replace-domain"]).optional().describe('"merge" (default) or "replace-domain"')
1780
+ })
1781
+ };
1782
+ var scenarioStepSchema = z.lazy(
1783
+ () => z.discriminatedUnion("type", [
1784
+ z.object({
1785
+ type: z.literal("navigate"),
1786
+ label: z.string().optional(),
1787
+ url: z.string(),
1788
+ wait_for_load: z.boolean().optional()
1789
+ }),
1790
+ z.object({
1791
+ type: z.literal("action"),
1792
+ label: z.string().optional(),
1793
+ action_id: z.string(),
1794
+ params: z.record(z.string()).optional()
1795
+ }),
1796
+ z.object({
1797
+ type: z.literal("fetch"),
1798
+ label: z.string().optional(),
1799
+ fetch_id: z.string(),
1800
+ result_key: z.string(),
1801
+ params: z.record(z.string()).optional()
1802
+ }),
1803
+ z.object({
1804
+ type: z.literal("loop"),
1805
+ label: z.string().optional(),
1806
+ variables: z.array(
1807
+ z.object({
1808
+ name: z.string(),
1809
+ source: z.enum(["static", "target_options", "previous_fetch"]),
1810
+ values: z.array(z.string()).optional(),
1811
+ target_id: z.string().optional(),
1812
+ fetch_result_key: z.string().optional(),
1813
+ field_name: z.string().optional()
1814
+ })
1815
+ ),
1816
+ steps: z.array(scenarioStepSchema),
1817
+ delay_between_ms: z.coerce.number().optional(),
1818
+ max_iterations: z.coerce.number().min(1).max(500).optional()
1819
+ }),
1820
+ z.object({
1821
+ type: z.literal("wait"),
1822
+ label: z.string().optional(),
1823
+ strategy: z.enum(["time", "selector", "navigation"]),
1824
+ selector: z.string().optional(),
1825
+ timeout_ms: z.coerce.number()
1826
+ })
1827
+ ])
1828
+ );
1829
+ var smScenarioCreateTool = {
1830
+ name: "sm_scenario_create",
1831
+ description: SM_SCENARIO_CREATE_DESCRIPTION,
1832
+ inputSchema: z.object({
1833
+ label: z.string().describe("Scenario name"),
1834
+ description: z.string().optional().describe("LLM-facing description"),
1835
+ params: z.array(
1836
+ z.object({
1837
+ name: z.string(),
1838
+ type: z.enum(["string", "number", "boolean"]).default("string"),
1839
+ required: z.boolean().default(true),
1840
+ secret: z.boolean().default(false),
1841
+ description: z.string().optional(),
1842
+ default_value: z.union([z.string(), z.coerce.number(), z.boolean()]).optional()
1843
+ })
1844
+ ).optional().describe("Global parameter definitions"),
1845
+ steps: z.array(scenarioStepSchema).min(1).describe("Step sequence"),
1846
+ max_duration_ms: z.coerce.number().optional().describe("Timeout in ms (default: 300000)"),
1847
+ on_error: z.enum(["stop", "skip_and_continue"]).optional().describe("Error handling (default: stop)")
1848
+ })
1849
+ };
1850
+ var smScenarioUpdateTool = {
1851
+ name: "sm_scenario_update",
1852
+ description: SM_SCENARIO_UPDATE_DESCRIPTION,
1853
+ inputSchema: z.object({
1854
+ scenario_id: z.string().describe("Scenario ID to update"),
1855
+ label: z.string().optional(),
1856
+ description: z.string().optional(),
1857
+ params: z.array(
1858
+ z.object({
1859
+ name: z.string(),
1860
+ type: z.enum(["string", "number", "boolean"]).default("string"),
1861
+ required: z.boolean().default(true),
1862
+ secret: z.boolean().default(false),
1863
+ description: z.string().optional(),
1864
+ default_value: z.union([z.string(), z.coerce.number(), z.boolean()]).optional()
1865
+ })
1866
+ ).optional(),
1867
+ steps: z.array(scenarioStepSchema).optional(),
1868
+ max_duration_ms: z.coerce.number().optional(),
1869
+ on_error: z.enum(["stop", "skip_and_continue"]).optional()
1870
+ })
1871
+ };
1872
+ var smScenarioDeleteTool = {
1873
+ name: "sm_scenario_delete",
1874
+ description: SM_SCENARIO_DELETE_DESCRIPTION,
1875
+ inputSchema: z.object({
1876
+ scenario_id: z.string().describe("Scenario ID to delete")
1877
+ })
1878
+ };
1879
+ var smScenarioListTool = {
1880
+ name: "sm_scenario_list",
1881
+ description: SM_SCENARIO_LIST_DESCRIPTION,
1882
+ inputSchema: z.object({})
1883
+ };
1884
+ var smScenarioRunTool = {
1885
+ name: "sm_scenario_run",
1886
+ description: SM_SCENARIO_RUN_DESCRIPTION,
1887
+ inputSchema: z.object({
1888
+ tabId: z.coerce.number().describe("Tab ID for execution"),
1889
+ scenario_id: z.string().describe("Scenario to execute"),
1890
+ params: z.record(z.unknown()).optional().describe("Parameter values"),
1891
+ wait_for_completion: z.boolean().optional().describe("Wait for completion (default: false, async)")
1892
+ })
1893
+ };
1894
+ var smReportListTool = {
1895
+ name: "sm_report_list",
1896
+ description: SM_REPORT_LIST_DESCRIPTION,
1897
+ inputSchema: z.object({
1898
+ scenario_id: z.string().optional().describe("Filter by scenario ID")
1899
+ })
1900
+ };
1901
+ var smReportGetTool = {
1902
+ name: "sm_report_get",
1903
+ description: SM_REPORT_GET_DESCRIPTION,
1904
+ inputSchema: z.object({
1905
+ report_id: z.string().describe("Report ID to retrieve")
1906
+ })
1907
+ };
1908
+ var smReportDeleteTool = {
1909
+ name: "sm_report_delete",
1910
+ description: SM_REPORT_DELETE_DESCRIPTION,
1911
+ inputSchema: z.object({
1912
+ report_id: z.string().describe("Report ID to delete")
1913
+ })
1914
+ };
1915
+ var smReportExportTool = {
1916
+ name: "sm_report_export",
1917
+ description: SM_REPORT_EXPORT_DESCRIPTION,
1918
+ inputSchema: z.object({
1919
+ report_id: z.string().describe("Report ID to export"),
1920
+ format: z.enum(["json", "csv"]).optional().describe("Export format (default: json)")
1921
+ })
1922
+ };
1923
+ var jobEntrySchema = z.object({
1924
+ scenario_id: z.string().describe("Scenario ID to execute"),
1925
+ label: z.string().optional().describe("Display label override"),
1926
+ params: z.record(z.unknown()).optional().default({}).describe("Scenario parameter overrides")
1927
+ });
1928
+ var smJobCreateTool = {
1929
+ name: "sm_job_create",
1930
+ description: SM_JOB_CREATE_DESCRIPTION,
1931
+ inputSchema: z.object({
1932
+ label: z.string().min(1).describe("Job name"),
1933
+ description: z.string().optional().describe("Job description"),
1934
+ entries: z.array(jobEntrySchema).min(1).describe("Ordered list of scenarios to execute"),
1935
+ on_error: z.enum(["stop", "skip_and_continue"]).optional().describe("How to handle scenario failures (default: stop)")
1936
+ })
1937
+ };
1938
+ var smJobUpdateTool = {
1939
+ name: "sm_job_update",
1940
+ description: SM_JOB_UPDATE_DESCRIPTION,
1941
+ inputSchema: z.object({
1942
+ job_id: z.string().describe("Job ID to update"),
1943
+ label: z.string().optional(),
1944
+ description: z.string().optional(),
1945
+ entries: z.array(jobEntrySchema).min(1).optional(),
1946
+ on_error: z.enum(["stop", "skip_and_continue"]).optional()
1947
+ })
1948
+ };
1949
+ var smJobDeleteTool = {
1950
+ name: "sm_job_delete",
1951
+ description: SM_JOB_DELETE_DESCRIPTION,
1952
+ inputSchema: z.object({
1953
+ job_id: z.string().describe("Job ID to delete")
1954
+ })
1955
+ };
1956
+ var smJobListTool = {
1957
+ name: "sm_job_list",
1958
+ description: SM_JOB_LIST_DESCRIPTION,
1959
+ inputSchema: z.object({})
1960
+ };
1961
+ var smJobRunTool = {
1962
+ name: "sm_job_run",
1963
+ description: SM_JOB_RUN_DESCRIPTION,
1964
+ inputSchema: z.object({
1965
+ tabId: z.coerce.number().describe("Tab ID to execute scenarios on"),
1966
+ job_id: z.string().describe("Job ID to execute"),
1967
+ params: z.record(z.record(z.unknown())).optional().describe("Per-scenario param overrides keyed by scenario_id"),
1968
+ wait_for_completion: z.boolean().optional().describe("Wait for all scenarios to complete (default: false)")
1969
+ })
1970
+ };
1971
+ var smJobCancelTool = {
1972
+ name: "sm_job_cancel",
1973
+ description: SM_JOB_CANCEL_DESCRIPTION,
1974
+ inputSchema: z.object({
1975
+ tabId: z.coerce.number().describe("Tab ID where the job is running")
1976
+ })
1977
+ };
1978
+ var smJobReportGetTool = {
1979
+ name: "sm_job_report_get",
1980
+ description: SM_JOB_REPORT_GET_DESCRIPTION,
1981
+ inputSchema: z.object({
1982
+ job_report_id: z.string().describe("Job report ID to retrieve")
1983
+ })
1984
+ };
1985
+ var smJobReportExportTool = {
1986
+ name: "sm_job_report_export",
1987
+ description: SM_JOB_REPORT_EXPORT_DESCRIPTION,
1988
+ inputSchema: z.object({
1989
+ job_report_id: z.string().describe("Job report ID to export"),
1990
+ format: z.enum(["json", "csv"]).optional().describe("Export format (default: json)")
1991
+ })
1992
+ };
1993
+ var smJobHistoryTool = {
1994
+ name: "sm_job_history",
1995
+ description: SM_JOB_HISTORY_DESCRIPTION,
1996
+ inputSchema: z.object({
1997
+ job_id: z.string().describe("Job ID to get history for"),
1998
+ limit: z.coerce.number().min(1).max(100).optional().describe("Max snapshots (default: 20)")
1999
+ })
2000
+ };
2001
+ var smJobCompareTool = {
2002
+ name: "sm_job_compare",
2003
+ description: SM_JOB_COMPARE_DESCRIPTION,
2004
+ inputSchema: z.object({
2005
+ job_id: z.string().describe("Job ID to compare"),
2006
+ base_snapshot_id: z.string().optional().describe("Older snapshot ID (default: second-most-recent)"),
2007
+ compare_snapshot_id: z.string().optional().describe("Newer snapshot ID (default: most-recent)"),
2008
+ changed_only: z.boolean().optional().describe("Omit unchanged rows (default: false)")
2009
+ })
2010
+ };
2011
+ var smJobSnapshotGetTool = {
2012
+ name: "sm_job_snapshot_get",
2013
+ description: SM_JOB_SNAPSHOT_GET_DESCRIPTION,
2014
+ inputSchema: z.object({
2015
+ snapshot_id: z.string().describe("Snapshot ID to retrieve (SNP_xxxx)")
2016
+ })
2017
+ };
2018
+ var dataBindingSchema = z.object({
2019
+ binding_id: z.string().min(1).max(20).describe('Unique binding ID within this view (e.g., "b1")'),
2020
+ source_type: z.enum(["job_report", "report", "job", "scenario"]).describe("Data source type"),
2021
+ source_id: z.string().describe('Source entity ID (e.g., "JRP_xxxx")'),
2022
+ projection: z.object({
2023
+ fields: z.array(z.string()).optional().describe("Field whitelist"),
2024
+ max_entries: z.number().min(1).max(1e3).optional().describe("Max array entries")
2025
+ }).optional().describe("Data projection to limit injected data")
2026
+ });
2027
+ var smCustomViewCreateTool = {
2028
+ name: "sm_custom_view_create",
2029
+ description: SM_CUSTOM_VIEW_CREATE_DESCRIPTION,
2030
+ inputSchema: z.object({
2031
+ label: z.string().min(1).max(100).describe("View name"),
2032
+ description: z.string().max(500).optional().describe("View description"),
2033
+ html: z.string().max(2e5).describe("HTML content (max 200KB)"),
2034
+ data_bindings: z.array(dataBindingSchema).max(10).optional().describe("Data source bindings")
2035
+ })
2036
+ };
2037
+ var smCustomViewUpdateTool = {
2038
+ name: "sm_custom_view_update",
2039
+ description: SM_CUSTOM_VIEW_UPDATE_DESCRIPTION,
2040
+ inputSchema: z.object({
2041
+ view_id: z.string().describe("View ID to update"),
2042
+ label: z.string().min(1).max(100).optional().describe("New label"),
2043
+ description: z.string().max(500).optional().describe("New description"),
2044
+ html: z.string().max(2e5).optional().describe("New HTML content"),
2045
+ data_bindings: z.array(dataBindingSchema).max(10).optional().describe("New data bindings (replaces all)")
2046
+ })
2047
+ };
2048
+ var smCustomViewDeleteTool = {
2049
+ name: "sm_custom_view_delete",
2050
+ description: SM_CUSTOM_VIEW_DELETE_DESCRIPTION,
2051
+ inputSchema: z.object({
2052
+ view_id: z.string().describe("View ID to delete")
2053
+ })
2054
+ };
2055
+ var smCustomViewListTool = {
2056
+ name: "sm_custom_view_list",
2057
+ description: SM_CUSTOM_VIEW_LIST_DESCRIPTION,
2058
+ inputSchema: z.object({})
2059
+ };
2060
+ var smCustomViewGetTool = {
2061
+ name: "sm_custom_view_get",
2062
+ description: SM_CUSTOM_VIEW_GET_DESCRIPTION,
2063
+ inputSchema: z.object({
2064
+ view_id: z.string().describe("View ID to retrieve"),
2065
+ include_data: z.boolean().optional().describe("Include resolved data in response (default: false)")
2066
+ })
2067
+ };
752
2068
  var allTools = [
753
2069
  // Core (15)
754
2070
  navigateTool,
@@ -774,9 +2090,10 @@ var allTools = [
774
2090
  // Debug (2)
775
2091
  readConsoleMessagesTool,
776
2092
  readNetworkRequestsTool,
777
- // Advanced (7)
2093
+ // Advanced (8)
778
2094
  gifCreatorTool,
779
2095
  uploadImageTool,
2096
+ fileUploadTool,
780
2097
  updatePlanTool,
781
2098
  resizeWindowTool,
782
2099
  shortcutsListTool,
@@ -789,47 +2106,115 @@ var allTools = [
789
2106
  browserEventUnsubscribeTool,
790
2107
  artifactFromPageTool,
791
2108
  pageDataExtractTool,
792
- browserHealthTool
2109
+ browserHealthTool,
2110
+ // Semantic (12)
2111
+ smCapabilitiesTool,
2112
+ smInvokeTool,
2113
+ smFetchTool,
2114
+ smRegisterPageTool,
2115
+ smAddTargetTool,
2116
+ smAddActionTool,
2117
+ smAddFetchTool,
2118
+ smStatusTool,
2119
+ smTestTargetTool,
2120
+ smDeleteTool,
2121
+ smExportTool,
2122
+ smImportTool,
2123
+ // Scenario & Report (9)
2124
+ smScenarioCreateTool,
2125
+ smScenarioUpdateTool,
2126
+ smScenarioDeleteTool,
2127
+ smScenarioListTool,
2128
+ smScenarioRunTool,
2129
+ smReportListTool,
2130
+ smReportGetTool,
2131
+ smReportDeleteTool,
2132
+ smReportExportTool,
2133
+ // Job (8)
2134
+ smJobCreateTool,
2135
+ smJobUpdateTool,
2136
+ smJobDeleteTool,
2137
+ smJobListTool,
2138
+ smJobRunTool,
2139
+ smJobCancelTool,
2140
+ smJobReportGetTool,
2141
+ smJobReportExportTool,
2142
+ // Job History (3)
2143
+ smJobHistoryTool,
2144
+ smJobCompareTool,
2145
+ smJobSnapshotGetTool,
2146
+ // Custom View (5)
2147
+ smCustomViewCreateTool,
2148
+ smCustomViewUpdateTool,
2149
+ smCustomViewDeleteTool,
2150
+ smCustomViewListTool,
2151
+ smCustomViewGetTool
793
2152
  ];
794
2153
 
795
2154
  // src/server.ts
796
2155
  var pendingRequests = /* @__PURE__ */ new Map();
797
2156
  var extensionSocket = null;
2157
+ function coerceValue(v) {
2158
+ if (typeof v !== "string") return v;
2159
+ if (v.startsWith("[") && v.endsWith("]") || v.startsWith("{") && v.endsWith("}")) {
2160
+ try {
2161
+ return JSON.parse(v);
2162
+ } catch {
2163
+ }
2164
+ }
2165
+ if (v === "true") return true;
2166
+ if (v === "false") return false;
2167
+ return v;
2168
+ }
2169
+ function coerceShape(shape) {
2170
+ const result = {};
2171
+ for (const [key, schema] of Object.entries(shape)) {
2172
+ result[key] = z2.preprocess(coerceValue, schema);
2173
+ }
2174
+ return result;
2175
+ }
798
2176
  function createConfiguredMcpServer() {
799
2177
  const server = new McpServer({
800
2178
  name: MCP_SERVER.NAME,
801
2179
  version: MCP_SERVER.VERSION
802
2180
  });
803
2181
  for (const tool of allTools) {
804
- const shape = tool.inputSchema._def.shape?.() ?? {};
805
- server.tool(tool.name, tool.description, shape, async (params) => {
806
- const result = await callExtensionTool(tool.name, params);
807
- if (tool.name === "browser_event_subscribe" && result.content[0]) {
808
- try {
809
- const parsed = JSON.parse(result.content[0].text);
810
- if (parsed.subscriptionId) {
811
- const p = params;
812
- addSubscription({
813
- id: parsed.subscriptionId,
814
- agentId: getDefaultAgentId(),
815
- eventTypes: p.eventTypes ?? [],
816
- urlPattern: p.urlPattern,
817
- createdAt: Date.now()
818
- });
2182
+ const def = tool.inputSchema._def;
2183
+ const shape = def.shape?.() ?? {};
2184
+ server.tool(
2185
+ tool.name,
2186
+ tool.description,
2187
+ coerceShape(shape),
2188
+ async (params) => {
2189
+ const result = await callExtensionTool(tool.name, params);
2190
+ const first = result.content[0];
2191
+ if (tool.name === "browser_event_subscribe" && first?.type === "text") {
2192
+ try {
2193
+ const parsed = JSON.parse(first.text);
2194
+ if (parsed.subscriptionId) {
2195
+ const p = params;
2196
+ addSubscription({
2197
+ id: parsed.subscriptionId,
2198
+ agentId: getDefaultAgentId(),
2199
+ eventTypes: p.eventTypes ?? [],
2200
+ urlPattern: p.urlPattern,
2201
+ createdAt: Date.now()
2202
+ });
2203
+ }
2204
+ } catch {
819
2205
  }
820
- } catch {
821
- }
822
- } else if (tool.name === "browser_event_unsubscribe" && result.content[0]) {
823
- try {
824
- const parsed = JSON.parse(result.content[0].text);
825
- if (parsed.subscriptionId) {
826
- removeSubscription(parsed.subscriptionId);
2206
+ } else if (tool.name === "browser_event_unsubscribe" && first?.type === "text") {
2207
+ try {
2208
+ const parsed = JSON.parse(first.text);
2209
+ if (parsed.subscriptionId) {
2210
+ removeSubscription(parsed.subscriptionId);
2211
+ }
2212
+ } catch {
827
2213
  }
828
- } catch {
829
2214
  }
2215
+ return result;
830
2216
  }
831
- return result;
832
- });
2217
+ );
833
2218
  }
834
2219
  const listener = (event) => {
835
2220
  server.sendLoggingMessage({
@@ -896,6 +2281,75 @@ async function startMcpServer(socketPath, agentName, options) {
896
2281
  process.on("exit", () => {
897
2282
  cleanupSocket(socketPath);
898
2283
  });
2284
+ } else if (options?.transport === "streamable-http") {
2285
+ const sessions2 = /* @__PURE__ */ new Map();
2286
+ const SESSION_TTL = 30 * 60 * 1e3;
2287
+ const SWEEP_INTERVAL = 5 * 60 * 1e3;
2288
+ const sweepTimer = setInterval(() => {
2289
+ const now = Date.now();
2290
+ for (const [id, session] of sessions2) {
2291
+ if (now - session.lastActivity > SESSION_TTL) {
2292
+ process.stderr.write(`[viyv-browser:mcp] Streamable HTTP session TTL expired: ${id}
2293
+ `);
2294
+ sessions2.delete(id);
2295
+ session.server.close().catch(() => {
2296
+ });
2297
+ }
2298
+ }
2299
+ }, SWEEP_INTERVAL);
2300
+ sweepTimer.unref();
2301
+ const httpServer = http.createServer();
2302
+ httpServer.on("request", (req, res) => {
2303
+ handleStreamableHttpRequest(req, res, sessions2).catch((error) => {
2304
+ process.stderr.write(
2305
+ `[viyv-browser:mcp] Streamable HTTP request error: ${error.message}
2306
+ `
2307
+ );
2308
+ if (!res.headersSent) {
2309
+ res.writeHead(500).end("Internal server error");
2310
+ }
2311
+ });
2312
+ });
2313
+ const listenPort = options.port ?? 0;
2314
+ httpServer.listen(listenPort, "127.0.0.1", () => {
2315
+ const addr = httpServer.address();
2316
+ const port = typeof addr === "object" ? addr?.port : listenPort;
2317
+ process.stdout.write(`${JSON.stringify({ port })}
2318
+ `);
2319
+ process.stderr.write(
2320
+ `[viyv-browser:mcp] Streamable HTTP server listening on 127.0.0.1:${port}
2321
+ `
2322
+ );
2323
+ });
2324
+ process.stderr.write(
2325
+ `[viyv-browser:mcp] MCP Server started (streamable-http), socket: ${socketPath}
2326
+ `
2327
+ );
2328
+ let shuttingDown = false;
2329
+ const shutdown = async () => {
2330
+ if (shuttingDown) return;
2331
+ shuttingDown = true;
2332
+ clearInterval(sweepTimer);
2333
+ for (const { server: s } of sessions2.values()) {
2334
+ await s.close().catch(() => {
2335
+ });
2336
+ }
2337
+ httpServer.close(() => {
2338
+ });
2339
+ socketServer.close(() => {
2340
+ });
2341
+ cleanupSocket(socketPath);
2342
+ process.exit(0);
2343
+ };
2344
+ process.on("SIGINT", () => {
2345
+ shutdown();
2346
+ });
2347
+ process.on("SIGTERM", () => {
2348
+ shutdown();
2349
+ });
2350
+ process.on("exit", () => {
2351
+ cleanupSocket(socketPath);
2352
+ });
899
2353
  } else {
900
2354
  const server = createConfiguredMcpServer();
901
2355
  const transport = new StdioServerTransport();
@@ -915,28 +2369,168 @@ async function handleSseRequest(req, res, sessions2) {
915
2369
  const transport = new SSEServerTransport("/message", res);
916
2370
  const mcpServer = createConfiguredMcpServer();
917
2371
  transport.onclose = () => {
918
- sessions2.delete(transport.sessionId);
2372
+ sessions2.delete(transport.sessionId);
2373
+ };
2374
+ res.on("close", () => {
2375
+ if (sessions2.has(transport.sessionId)) {
2376
+ sessions2.delete(transport.sessionId);
2377
+ transport.close();
2378
+ }
2379
+ });
2380
+ await mcpServer.connect(transport);
2381
+ sessions2.set(transport.sessionId, { transport, server: mcpServer });
2382
+ } else if (req.method === "POST" && req.url?.startsWith("/message")) {
2383
+ const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
2384
+ const sessionId = url.searchParams.get("sessionId");
2385
+ const session = sessionId ? sessions2.get(sessionId) : null;
2386
+ if (session) {
2387
+ await session.transport.handlePostMessage(req, res);
2388
+ } else {
2389
+ res.writeHead(404).end("Session not found");
2390
+ }
2391
+ } else {
2392
+ res.writeHead(404).end();
2393
+ }
2394
+ }
2395
+ async function handleStreamableHttpRequest(req, res, sessions2) {
2396
+ let url;
2397
+ try {
2398
+ url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
2399
+ } catch {
2400
+ res.writeHead(400).end("Bad request");
2401
+ return;
2402
+ }
2403
+ if (url.pathname !== "/mcp") {
2404
+ res.writeHead(404).end("Not found");
2405
+ return;
2406
+ }
2407
+ const rawSessionId = req.headers["mcp-session-id"];
2408
+ const sessionId = typeof rawSessionId === "string" ? rawSessionId : void 0;
2409
+ const existingSession = sessionId ? sessions2.get(sessionId) : void 0;
2410
+ if (existingSession) {
2411
+ existingSession.lastActivity = Date.now();
2412
+ if (req.method === "POST") {
2413
+ let body;
2414
+ try {
2415
+ body = await parseJsonBody(req);
2416
+ } catch (parseError) {
2417
+ process.stderr.write(
2418
+ `[viyv-browser:mcp] Streamable HTTP body parse failed: ${parseError.message}
2419
+ `
2420
+ );
2421
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
2422
+ JSON.stringify({
2423
+ jsonrpc: "2.0",
2424
+ error: { code: -32700, message: "Parse error" },
2425
+ id: null
2426
+ })
2427
+ );
2428
+ return;
2429
+ }
2430
+ await existingSession.transport.handleRequest(req, res, body);
2431
+ } else {
2432
+ await existingSession.transport.handleRequest(req, res);
2433
+ }
2434
+ return;
2435
+ }
2436
+ if (!sessionId && req.method === "POST") {
2437
+ let body;
2438
+ try {
2439
+ body = await parseJsonBody(req);
2440
+ } catch (parseError) {
2441
+ process.stderr.write(
2442
+ `[viyv-browser:mcp] Streamable HTTP body parse failed: ${parseError.message}
2443
+ `
2444
+ );
2445
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
2446
+ JSON.stringify({
2447
+ jsonrpc: "2.0",
2448
+ error: { code: -32700, message: "Parse error" },
2449
+ id: null
2450
+ })
2451
+ );
2452
+ return;
2453
+ }
2454
+ if (!isInitializeRequest(body)) {
2455
+ const requestId = body && typeof body === "object" && "id" in body ? body.id : null;
2456
+ res.writeHead(400, { "Content-Type": "application/json" }).end(
2457
+ JSON.stringify({
2458
+ jsonrpc: "2.0",
2459
+ error: { code: -32600, message: "First request must be an initialize request" },
2460
+ id: requestId ?? null
2461
+ })
2462
+ );
2463
+ return;
2464
+ }
2465
+ const mcpServer = createConfiguredMcpServer();
2466
+ const transport = new StreamableHTTPServerTransport({
2467
+ sessionIdGenerator: () => randomUUID2(),
2468
+ onsessioninitialized: (id) => {
2469
+ sessions2.set(id, { transport, server: mcpServer, lastActivity: Date.now() });
2470
+ }
2471
+ });
2472
+ transport.onclose = () => {
2473
+ const id = transport.sessionId;
2474
+ if (id) sessions2.delete(id);
919
2475
  };
920
- res.on("close", () => {
921
- if (sessions2.has(transport.sessionId)) {
922
- sessions2.delete(transport.sessionId);
923
- transport.close();
2476
+ let connected = false;
2477
+ try {
2478
+ await mcpServer.connect(transport);
2479
+ connected = true;
2480
+ await transport.handleRequest(req, res, body);
2481
+ } catch (error) {
2482
+ const stage = connected ? "handleRequest" : "connect";
2483
+ process.stderr.write(
2484
+ `[viyv-browser:mcp] Streamable HTTP session ${stage} failed: ${error.message}
2485
+ `
2486
+ );
2487
+ try {
2488
+ mcpServer.server.onclose?.();
2489
+ } catch {
2490
+ }
2491
+ await mcpServer.close().catch(() => {
2492
+ });
2493
+ if (!res.headersSent) {
2494
+ res.writeHead(500).end("Internal server error");
924
2495
  }
925
- });
926
- await mcpServer.connect(transport);
927
- sessions2.set(transport.sessionId, { transport, server: mcpServer });
928
- } else if (req.method === "POST" && req.url?.startsWith("/message")) {
929
- const url = new URL(req.url, `http://${req.headers.host ?? "localhost"}`);
930
- const sessionId = url.searchParams.get("sessionId");
931
- const session = sessionId ? sessions2.get(sessionId) : null;
932
- if (session) {
933
- await session.transport.handlePostMessage(req, res);
934
- } else {
935
- res.writeHead(404).end("Session not found");
936
2496
  }
937
- } else {
938
- res.writeHead(404).end();
2497
+ return;
2498
+ }
2499
+ if (sessionId && !existingSession) {
2500
+ res.writeHead(404).end("Session not found");
2501
+ return;
939
2502
  }
2503
+ res.writeHead(400).end("Bad request");
2504
+ }
2505
+ var MAX_BODY_SIZE = 4 * 1024 * 1024;
2506
+ function parseJsonBody(req) {
2507
+ return new Promise((resolve2, reject) => {
2508
+ const chunks = [];
2509
+ let size = 0;
2510
+ let destroyed = false;
2511
+ req.on("data", (chunk) => {
2512
+ if (destroyed) return;
2513
+ size += chunk.length;
2514
+ if (size > MAX_BODY_SIZE) {
2515
+ destroyed = true;
2516
+ req.destroy();
2517
+ reject(new Error("Request body too large"));
2518
+ return;
2519
+ }
2520
+ chunks.push(chunk);
2521
+ });
2522
+ req.on("end", () => {
2523
+ if (destroyed) return;
2524
+ try {
2525
+ resolve2(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
2526
+ } catch (error) {
2527
+ reject(new Error(`Invalid JSON: ${error.message}`));
2528
+ }
2529
+ });
2530
+ req.on("error", (err) => {
2531
+ if (!destroyed) reject(err);
2532
+ });
2533
+ });
940
2534
  }
941
2535
  function createSocketServer(socketPath) {
942
2536
  cleanupSocket(socketPath);
@@ -1071,7 +2665,39 @@ async function callExtensionTool(tool, input) {
1071
2665
  if (tool === "switch_browser") {
1072
2666
  return handleSwitchBrowser();
1073
2667
  }
1074
- if (!extensionSocket || extensionSocket.destroyed || !isExtensionConnected()) {
2668
+ if (tool === "file_upload") {
2669
+ const paths = input.paths;
2670
+ if (paths) {
2671
+ for (const p of paths) {
2672
+ try {
2673
+ if (!existsSync(p) || !statSync(p).isFile()) {
2674
+ return {
2675
+ content: [
2676
+ {
2677
+ type: "text",
2678
+ text: JSON.stringify({
2679
+ error: { code: "FILE_NOT_FOUND", message: `File not found: ${p}` }
2680
+ })
2681
+ }
2682
+ ]
2683
+ };
2684
+ }
2685
+ } catch {
2686
+ return {
2687
+ content: [
2688
+ {
2689
+ type: "text",
2690
+ text: JSON.stringify({
2691
+ error: { code: "FILE_NOT_FOUND", message: `Cannot access file: ${p}` }
2692
+ })
2693
+ }
2694
+ ]
2695
+ };
2696
+ }
2697
+ }
2698
+ }
2699
+ }
2700
+ if (!extensionSocket || extensionSocket.destroyed) {
1075
2701
  return {
1076
2702
  content: [
1077
2703
  {
@@ -1138,9 +2764,20 @@ async function callExtensionTool(tool, input) {
1138
2764
  pendingRequests.set(requestId, {
1139
2765
  resolve: (result) => {
1140
2766
  removeErrorListener();
1141
- resolve2({
1142
- content: [{ type: "text", text: JSON.stringify(result) }]
1143
- });
2767
+ if (tool === "screenshot" && typeof result.data === "string") {
2768
+ const { data, ...metadata } = result;
2769
+ const mimeType = metadata.format === "png" ? "image/png" : "image/jpeg";
2770
+ resolve2({
2771
+ content: [
2772
+ { type: "image", data, mimeType },
2773
+ { type: "text", text: JSON.stringify(metadata) }
2774
+ ]
2775
+ });
2776
+ } else {
2777
+ resolve2({
2778
+ content: [{ type: "text", text: JSON.stringify(result) }]
2779
+ });
2780
+ }
1144
2781
  },
1145
2782
  reject: (error) => {
1146
2783
  removeErrorListener();
@@ -1228,199 +2865,11 @@ function cleanupSocket(socketPath) {
1228
2865
  }
1229
2866
  }
1230
2867
 
1231
- // src/native-host/bridge.ts
1232
- import { createConnection } from "net";
1233
-
1234
- // src/native-host/transport.ts
1235
- var MAX_MESSAGE_SIZE = 1024 * 1024;
1236
- function encodeMessage(message) {
1237
- const json = JSON.stringify(message);
1238
- const body = Buffer.from(json, "utf-8");
1239
- if (body.length > MAX_MESSAGE_SIZE) {
1240
- throw new Error(`Message too large: ${body.length} bytes (max ${MAX_MESSAGE_SIZE})`);
1241
- }
1242
- const header = Buffer.alloc(4);
1243
- header.writeUInt32LE(body.length, 0);
1244
- return Buffer.concat([header, body]);
1245
- }
1246
- function createMessageReader(stream, onMessage, onError, onClose) {
1247
- let buffer = Buffer.alloc(0);
1248
- let expectedLength = null;
1249
- stream.on("data", (chunk) => {
1250
- buffer = Buffer.concat([buffer, chunk]);
1251
- while (true) {
1252
- if (expectedLength === null) {
1253
- if (buffer.length < 4) break;
1254
- expectedLength = buffer.readUInt32LE(0);
1255
- buffer = buffer.subarray(4);
1256
- if (expectedLength > MAX_MESSAGE_SIZE) {
1257
- onError?.(new Error(`Message too large: ${expectedLength} bytes`));
1258
- expectedLength = null;
1259
- buffer = Buffer.alloc(0);
1260
- break;
1261
- }
1262
- }
1263
- if (buffer.length < expectedLength) break;
1264
- const jsonBuffer = buffer.subarray(0, expectedLength);
1265
- buffer = buffer.subarray(expectedLength);
1266
- expectedLength = null;
1267
- try {
1268
- const message = JSON.parse(jsonBuffer.toString("utf-8"));
1269
- onMessage(message);
1270
- } catch (error) {
1271
- onError?.(new Error(`Invalid JSON: ${error.message}`));
1272
- }
1273
- }
1274
- });
1275
- stream.on("error", (error) => {
1276
- onError?.(new Error(`Stream error: ${error.message}`));
1277
- });
1278
- stream.on("end", () => {
1279
- onClose?.();
1280
- });
1281
- stream.on("close", () => {
1282
- onClose?.();
1283
- });
1284
- }
1285
- function writeMessage(stream, message) {
1286
- const encoded = encodeMessage(message);
1287
- stream.write(encoded);
1288
- }
1289
-
1290
- // src/native-host/bridge.ts
1291
- var MAX_BUFFER_SIZE = 1e3;
1292
- function startBridge(options) {
1293
- const { socketPath, onError } = options;
1294
- let socket = null;
1295
- let reconnecting = false;
1296
- let retryCount = 0;
1297
- const pendingMessages = [];
1298
- function flushBuffer() {
1299
- if (!socket || socket.destroyed) return;
1300
- while (pendingMessages.length > 0) {
1301
- const msg = pendingMessages[0];
1302
- try {
1303
- const written = socket.write(`${JSON.stringify(msg)}
1304
- `);
1305
- pendingMessages.shift();
1306
- if (!written) {
1307
- socket.once("drain", () => flushBuffer());
1308
- return;
1309
- }
1310
- } catch (error) {
1311
- onError?.(error);
1312
- return;
1313
- }
1314
- }
1315
- }
1316
- function connectSocket() {
1317
- socket = createConnection(socketPath);
1318
- socket.on("connect", () => {
1319
- process.stderr.write(
1320
- `[viyv-browser:native-host] Connected to MCP server at ${socketPath}
1321
- `
1322
- );
1323
- retryCount = 0;
1324
- flushBuffer();
1325
- });
1326
- let lineBuffer = "";
1327
- socket.on("data", (data) => {
1328
- lineBuffer += data.toString("utf-8");
1329
- const lines = lineBuffer.split("\n");
1330
- lineBuffer = lines.pop() ?? "";
1331
- for (const line of lines) {
1332
- if (!line) continue;
1333
- try {
1334
- let message = JSON.parse(line);
1335
- if (message.type === "compressed" && typeof message.data === "string") {
1336
- const decompressed = decompressPayload(message.data, true);
1337
- message = JSON.parse(decompressed);
1338
- }
1339
- writeMessage(process.stdout, message);
1340
- } catch (error) {
1341
- onError?.(error);
1342
- }
1343
- }
1344
- });
1345
- socket.on("error", (error) => {
1346
- process.stderr.write(
1347
- `[viyv-browser:native-host] Socket error: ${error.message}
1348
- `
1349
- );
1350
- onError?.(error);
1351
- });
1352
- socket.on("close", () => {
1353
- process.stderr.write("[viyv-browser:native-host] Socket closed\n");
1354
- socket = null;
1355
- if (!reconnecting) {
1356
- reconnecting = true;
1357
- const delay = Math.min(
1358
- RECONNECT.INITIAL_DELAY * Math.pow(RECONNECT.MULTIPLIER, retryCount),
1359
- RECONNECT.MAX_DELAY
1360
- );
1361
- retryCount++;
1362
- process.stderr.write(
1363
- `[viyv-browser:native-host] Reconnecting in ${delay}ms (attempt ${retryCount})
1364
- `
1365
- );
1366
- setTimeout(() => {
1367
- reconnecting = false;
1368
- connectSocket();
1369
- }, delay);
1370
- }
1371
- });
1372
- }
1373
- createMessageReader(
1374
- process.stdin,
1375
- (message) => {
1376
- if (socket && !socket.destroyed) {
1377
- const json = JSON.stringify(message);
1378
- if (json.length > LIMITS.CHUNK_SIZE) {
1379
- const { compressed, wasCompressed } = compressPayload(json);
1380
- if (wasCompressed) {
1381
- socket.write(`${JSON.stringify({ type: "compressed", data: compressed })}
1382
- `);
1383
- } else {
1384
- socket.write(`${json}
1385
- `);
1386
- }
1387
- } else {
1388
- socket.write(`${json}
1389
- `);
1390
- }
1391
- } else {
1392
- if (pendingMessages.length < MAX_BUFFER_SIZE) {
1393
- pendingMessages.push(message);
1394
- } else {
1395
- process.stderr.write(
1396
- "[viyv-browser:native-host] Message buffer full, dropping message\n"
1397
- );
1398
- }
1399
- }
1400
- },
1401
- onError
1402
- );
1403
- connectSocket();
1404
- process.on("SIGINT", () => {
1405
- socket?.destroy();
1406
- process.exit(0);
1407
- });
1408
- process.on("SIGTERM", () => {
1409
- socket?.destroy();
1410
- process.exit(0);
1411
- });
1412
- process.stdin.on("end", () => {
1413
- process.stderr.write("[viyv-browser:native-host] stdin closed, shutting down\n");
1414
- socket?.destroy();
1415
- process.exit(0);
1416
- });
1417
- }
1418
-
1419
2868
  // src/setup.ts
1420
- import { writeFileSync, mkdirSync, chmodSync, existsSync as existsSync2 } from "fs";
1421
- import { resolve, dirname } from "path";
1422
- import { homedir, platform } from "os";
1423
2869
  import { execSync } from "child_process";
2870
+ import { chmodSync, existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
2871
+ import { homedir, platform } from "os";
2872
+ import { dirname, resolve } from "path";
1424
2873
  function runSetup(options = {}) {
1425
2874
  const os = platform();
1426
2875
  const binaryPath = getBinaryPath();
@@ -1481,12 +2930,9 @@ function createNativeHostWrapper(os, binaryPath) {
1481
2930
  return wrapperPath2;
1482
2931
  }
1483
2932
  const wrapperPath = resolve(manifestDir, `${NATIVE_HOST_NAME}.sh`);
1484
- writeFileSync(
1485
- wrapperPath,
1486
- `#!/bin/bash
2933
+ writeFileSync(wrapperPath, `#!/bin/bash
1487
2934
  exec "${nodePath}" "${binaryPath}" --native-host
1488
- `
1489
- );
2935
+ `);
1490
2936
  chmodSync(wrapperPath, 493);
1491
2937
  return wrapperPath;
1492
2938
  }
@@ -1507,11 +2953,7 @@ function getManifestPath(os) {
1507
2953
  `${NATIVE_HOST_NAME}.json`
1508
2954
  );
1509
2955
  case "linux":
1510
- return resolve(
1511
- home,
1512
- ".config/google-chrome/NativeMessagingHosts",
1513
- `${NATIVE_HOST_NAME}.json`
1514
- );
2956
+ return resolve(home, ".config/google-chrome/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`);
1515
2957
  case "win32":
1516
2958
  return resolve(
1517
2959
  home,
@@ -1535,10 +2977,8 @@ if (args.includes("setup")) {
1535
2977
  function poll() {
1536
2978
  const socketPath = findSocketPath();
1537
2979
  if (socketPath) {
1538
- process.stderr.write(
1539
- `[viyv-browser:native-host] Found socket at ${socketPath}
1540
- `
1541
- );
2980
+ process.stderr.write(`[viyv-browser:native-host] Found socket at ${socketPath}
2981
+ `);
1542
2982
  startBridge({
1543
2983
  socketPath,
1544
2984
  onError: (error) => {
@@ -1574,9 +3014,9 @@ if (args.includes("setup")) {
1574
3014
  const agentName = agentNameIdx >= 0 ? args[agentNameIdx + 1] : void 0;
1575
3015
  const transportIdx = args.indexOf("--transport");
1576
3016
  const transportMode = transportIdx >= 0 ? args[transportIdx + 1] : "stdio";
1577
- if (transportMode !== "stdio" && transportMode !== "sse") {
3017
+ if (transportMode !== "stdio" && transportMode !== "sse" && transportMode !== "streamable-http") {
1578
3018
  process.stderr.write(
1579
- `[viyv-browser:mcp] Invalid transport: "${transportMode}". Must be "stdio" or "sse".
3019
+ `[viyv-browser:mcp] Invalid transport: "${transportMode}". Must be "stdio", "sse", or "streamable-http".
1580
3020
  `
1581
3021
  );
1582
3022
  process.exit(1);