viyv-browser-mcp 0.1.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 ADDED
@@ -0,0 +1,1473 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { existsSync as existsSync3, readdirSync } from "fs";
5
+
6
+ // src/server.ts
7
+ import { randomUUID as randomUUID2 } from "crypto";
8
+ import { existsSync, unlinkSync } from "fs";
9
+ import { createServer } from "net";
10
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+
13
+ // ../shared/dist/constants.js
14
+ var PROTOCOL_VERSION = "1.0.0";
15
+ var NATIVE_HOST_NAME = "com.viyv.browser";
16
+ var TIMEOUTS = {
17
+ /** Overall MCP tool timeout */
18
+ MCP_TOOL: 3e4,
19
+ /** Native Messaging request timeout */
20
+ NATIVE_MESSAGE: 15e3,
21
+ /** CDP command timeout */
22
+ CDP_COMMAND: 1e4,
23
+ /** Screenshot capture timeout */
24
+ SCREENSHOT: 5e3,
25
+ /** Navigation timeout */
26
+ NAVIGATION: 3e4,
27
+ /** Chunk reassembly timeout */
28
+ CHUNK_REASSEMBLY: 1e4,
29
+ /** Default wait_for timeout */
30
+ WAIT_FOR: 3e4,
31
+ /** Heartbeat interval */
32
+ HEARTBEAT: 3e4,
33
+ /** CDP idle detach delay */
34
+ CDP_IDLE_DETACH: 5e3,
35
+ /** Tab lock TTL (deadlock prevention) */
36
+ TAB_LOCK_TTL: 6e4
37
+ };
38
+ var LIMITS = {
39
+ /** Native Messaging max message size (Chrome limit) */
40
+ NATIVE_MESSAGE_MAX_BYTES: 1024 * 1024,
41
+ /** Chunk size for large payloads */
42
+ CHUNK_SIZE: 768 * 1024,
43
+ /** A11y tree max elements */
44
+ A11Y_MAX_ELEMENTS: 5e3,
45
+ /** A11y tree default depth */
46
+ A11Y_DEFAULT_DEPTH: 8,
47
+ /** A11y tree default max chars */
48
+ A11Y_DEFAULT_MAX_CHARS: 5e4,
49
+ /** Event buffer max entries */
50
+ EVENT_BUFFER_MAX: 1e3,
51
+ /** Event buffer max bytes */
52
+ EVENT_BUFFER_MAX_BYTES: 10 * 1024 * 1024,
53
+ /** Message buffer during disconnection */
54
+ MESSAGE_BUFFER_MAX: 1e3,
55
+ /** Default screenshot JPEG quality */
56
+ SCREENSHOT_JPEG_QUALITY: 80,
57
+ /** Console buffer per tab */
58
+ CONSOLE_BUFFER_MAX: 1e3,
59
+ /** Network buffer per tab */
60
+ NETWORK_BUFFER_MAX: 1e3
61
+ };
62
+ var RECONNECT = {
63
+ /** Initial delay (ms) */
64
+ INITIAL_DELAY: 1e3,
65
+ /** Max delay (ms) */
66
+ MAX_DELAY: 3e4,
67
+ /** Backoff multiplier */
68
+ MULTIPLIER: 2
69
+ };
70
+ var MCP_SERVER = {
71
+ /** Server name for MCP protocol */
72
+ NAME: "viyv-browser",
73
+ /** Server version */
74
+ VERSION: "0.1.0",
75
+ /** Unix socket path template */
76
+ SOCKET_PATH_TEMPLATE: "/tmp/viyv-browser-{pid}.sock"
77
+ };
78
+
79
+ // src/agent-session.ts
80
+ import { randomUUID } from "crypto";
81
+ var sessions = /* @__PURE__ */ new Map();
82
+ var configuredDefaultAgentId = "default";
83
+ function setDefaultAgentId(id) {
84
+ configuredDefaultAgentId = id;
85
+ }
86
+ function createSession(agentId, agentName) {
87
+ const existing = sessions.get(agentId);
88
+ if (existing) {
89
+ existing.lastActivity = Date.now();
90
+ existing.status = "active";
91
+ return existing;
92
+ }
93
+ const session = {
94
+ agentId,
95
+ sessionToken: randomUUID(),
96
+ agentName: agentName ?? agentId,
97
+ status: "active",
98
+ lastActivity: Date.now(),
99
+ createdAt: Date.now()
100
+ };
101
+ sessions.set(agentId, session);
102
+ return session;
103
+ }
104
+ function touchSession(agentId) {
105
+ const session = sessions.get(agentId);
106
+ if (session) {
107
+ session.lastActivity = Date.now();
108
+ }
109
+ }
110
+ function closeSession(agentId) {
111
+ sessions.delete(agentId);
112
+ }
113
+ function getDefaultAgentId() {
114
+ const active = Array.from(sessions.values()).find((s) => s.status === "active");
115
+ if (active) return active.agentId;
116
+ const defaultSession = createSession(configuredDefaultAgentId);
117
+ return defaultSession.agentId;
118
+ }
119
+ var STALE_SESSION_TTL = 5 * 60 * 1e3;
120
+ var CLEANUP_INTERVAL = 60 * 1e3;
121
+ function cleanupStaleSessions() {
122
+ const now = Date.now();
123
+ let cleaned = 0;
124
+ for (const [agentId, session] of sessions) {
125
+ if (now - session.lastActivity > STALE_SESSION_TTL) {
126
+ sessions.delete(agentId);
127
+ cleaned++;
128
+ }
129
+ }
130
+ if (cleaned > 0) {
131
+ process.stderr.write(
132
+ `[viyv-browser:mcp] Cleaned up ${cleaned} stale session(s)
133
+ `
134
+ );
135
+ }
136
+ return cleaned;
137
+ }
138
+ var cleanupTimer = setInterval(cleanupStaleSessions, CLEANUP_INTERVAL);
139
+ cleanupTimer.unref();
140
+
141
+ // src/event-bridge.ts
142
+ var subscriptions = /* @__PURE__ */ new Map();
143
+ var eventCallback = null;
144
+ function setEventCallback(callback) {
145
+ eventCallback = callback;
146
+ }
147
+ function addSubscription(sub) {
148
+ subscriptions.set(sub.id, sub);
149
+ }
150
+ function removeSubscription(subId) {
151
+ return subscriptions.delete(subId);
152
+ }
153
+ function removeSubscriptionsByAgent(agentId) {
154
+ let removed = 0;
155
+ for (const [id, sub] of subscriptions) {
156
+ if (sub.agentId === agentId) {
157
+ subscriptions.delete(id);
158
+ removed++;
159
+ }
160
+ }
161
+ return removed;
162
+ }
163
+ function processEvent(event) {
164
+ for (const sub of subscriptions.values()) {
165
+ if (sub.agentId !== event.agentId) continue;
166
+ if (!sub.eventTypes.includes(event.eventType)) continue;
167
+ if (sub.urlPattern && !event.url.includes(sub.urlPattern)) continue;
168
+ eventCallback?.({
169
+ type: "browser_event",
170
+ subscriptionId: sub.id,
171
+ ...event,
172
+ timestamp: Date.now()
173
+ });
174
+ }
175
+ }
176
+
177
+ // src/health.ts
178
+ var extensionConnected = false;
179
+ var lastHeartbeat = null;
180
+ function setExtensionConnected(connected) {
181
+ extensionConnected = connected;
182
+ if (connected) lastHeartbeat = Date.now();
183
+ }
184
+ function recordHeartbeat() {
185
+ lastHeartbeat = Date.now();
186
+ }
187
+ var HEARTBEAT_STALENESS_MS = 6e4;
188
+ function isExtensionConnected() {
189
+ if (!extensionConnected) return false;
190
+ if (lastHeartbeat !== null && Date.now() - lastHeartbeat > HEARTBEAT_STALENESS_MS) {
191
+ return false;
192
+ }
193
+ return true;
194
+ }
195
+
196
+ // src/native-host/compression.ts
197
+ import { gzipSync, gunzipSync } from "zlib";
198
+ var CHUNK_SIZE = 768 * 1024;
199
+ function compressPayload(data) {
200
+ const original = Buffer.from(data, "utf-8");
201
+ const gzipped = gzipSync(original);
202
+ if (gzipped.length < original.length) {
203
+ return { compressed: gzipped.toString("base64"), wasCompressed: true };
204
+ }
205
+ return { compressed: data, wasCompressed: false };
206
+ }
207
+ function decompressPayload(data, isCompressed) {
208
+ if (!isCompressed) return data;
209
+ const buf = Buffer.from(data, "base64");
210
+ return gunzipSync(buf).toString("utf-8");
211
+ }
212
+
213
+ // src/tools/index.ts
214
+ import { z } from "zod";
215
+
216
+ // src/tools/advanced/gif-creator.ts
217
+ var GIF_CREATOR_DESCRIPTION = `Record a GIF of browser activity.
218
+ Captures a sequence of screenshots over a specified duration
219
+ and assembles them into an animated GIF. Useful for documenting
220
+ visual interactions or creating shareable recordings of workflows.`;
221
+
222
+ // src/tools/advanced/resize-window.ts
223
+ var RESIZE_WINDOW_DESCRIPTION = `Resize the browser window to specified dimensions.
224
+ Sets the width and height of the browser viewport, useful for
225
+ testing responsive layouts or ensuring consistent screenshot
226
+ dimensions across different operations.`;
227
+
228
+ // src/tools/advanced/shortcuts-execute.ts
229
+ var SHORTCUTS_EXECUTE_DESCRIPTION = `Execute a shortcut or workflow by command name or ID.
230
+
231
+ Runs the specified shortcut in the side panel using the current tab.
232
+ Look up by command name or shortcutId. Use shortcuts_list first
233
+ to see available shortcuts.`;
234
+
235
+ // src/tools/advanced/shortcuts-list.ts
236
+ var SHORTCUTS_LIST_DESCRIPTION = `List all available shortcuts and workflows.
237
+
238
+ Returns registered shortcuts with their commands, descriptions,
239
+ and whether they are workflows. Shortcuts can be executed using
240
+ the shortcuts_execute tool.`;
241
+
242
+ // src/tools/advanced/switch-browser.ts
243
+ var SWITCH_BROWSER_DESCRIPTION = `Switch to a different Chrome browser instance.
244
+
245
+ Disconnects the current Extension connection and waits for a new
246
+ Chrome browser to connect (up to 60s). The user should click
247
+ "Connect" in the desired browser's extension.`;
248
+
249
+ // src/tools/advanced/update-plan.ts
250
+ var UPDATE_PLAN_DESCRIPTION = `Present a step-by-step plan to the user for review.
251
+ Displays the proposed sequence of actions the agent intends to take,
252
+ allowing the user to approve, modify, or reject the plan before
253
+ execution begins.`;
254
+
255
+ // src/tools/advanced/upload-image.ts
256
+ var UPLOAD_IMAGE_DESCRIPTION = `Upload an image to a file input element on the page.
257
+ Accepts a local file path or base64-encoded image data and
258
+ programmatically sets it on the target file input. Supports
259
+ common image formats including PNG, JPEG, GIF, and WebP.`;
260
+
261
+ // src/tools/core/click.ts
262
+ var CLICK_DESCRIPTION = `Click at coordinates or on a referenced element.
263
+
264
+ Provide either coordinate [x, y] or ref (element reference from read_page/find).
265
+ Actions: left_click (default), right_click, double_click, triple_click.
266
+ Supports modifier keys: ctrl, shift, alt, cmd (e.g., "ctrl+shift").`;
267
+
268
+ // src/tools/core/drag.ts
269
+ var DRAG_DESCRIPTION = `Drag from one coordinate to another.
270
+
271
+ Performs mousedown at start, mousemove to end, then mouseup.`;
272
+
273
+ // src/tools/core/find.ts
274
+ var FIND_DESCRIPTION = `Find elements on the page using a natural language query.
275
+ Searches the DOM for elements matching the given description,
276
+ returning references that can be used for subsequent interactions
277
+ such as clicking, typing, or extracting content.`;
278
+
279
+ // src/tools/core/form-input.ts
280
+ var FORM_INPUT_DESCRIPTION = `Set a value in a form element identified by its ref.
281
+ Supports text inputs, textareas, selects, checkboxes, and radio buttons.
282
+ The ref must be obtained from a prior find or snapshot operation
283
+ to ensure the correct element is targeted.`;
284
+
285
+ // src/tools/core/get-page-text.ts
286
+ var GET_PAGE_TEXT_DESCRIPTION = `Extract readable text content from the current page.
287
+ Strips away HTML tags, scripts, and styles to return a clean
288
+ text representation of the visible page content, suitable for
289
+ analysis, summarization, or further processing.`;
290
+
291
+ // src/tools/core/handle-dialog.ts
292
+ var HANDLE_DIALOG_DESCRIPTION = `Handle JavaScript dialogs such as alert, confirm, and prompt.
293
+ Allows accepting or dismissing the dialog, and optionally providing
294
+ input text for prompt dialogs. Must be called while a dialog
295
+ is actively displayed on the page.`;
296
+
297
+ // src/tools/core/hover.ts
298
+ var HOVER_DESCRIPTION = `Move mouse to coordinates or element without clicking.
299
+
300
+ Useful for revealing tooltips, dropdown menus, or triggering hover states.`;
301
+
302
+ // src/tools/core/javascript-exec.ts
303
+ var JAVASCRIPT_EXEC_DESCRIPTION = `Execute arbitrary JavaScript code in the page context.
304
+ The script runs in the main world of the active page and can access
305
+ the DOM, window object, and any page-level APIs. Returns the
306
+ serialized result of the last evaluated expression.`;
307
+
308
+ // src/tools/core/key.ts
309
+ var KEY_DESCRIPTION = `Press keyboard key(s).
310
+
311
+ Space-separated key names. Supports keyboard shortcuts.
312
+ Examples: "Enter", "Tab", "Backspace", "ctrl+a", "cmd+c".
313
+ Use repeat parameter for repeated presses (e.g., arrow keys).`;
314
+
315
+ // src/tools/core/navigate.ts
316
+ var NAVIGATE_DESCRIPTION = `Navigate to a URL, or go forward/back in browser history.
317
+
318
+ Use "back" to go back in history, "forward" to go forward.
319
+ Provide a full URL (with protocol) to navigate to a new page.
320
+ Waits for page load to complete before returning.`;
321
+
322
+ // src/tools/core/read-page.ts
323
+ var READ_PAGE_DESCRIPTION = `Get accessibility tree representation of page elements.
324
+
325
+ Supports:
326
+ - filter: "interactive" for buttons/links/inputs only, "all" for everything
327
+ - depth: max tree depth (1-20, default 8)
328
+ - refId: focus on a specific element subtree
329
+ - maxChars: limit output size (default 50000)
330
+
331
+ Returns elements with ref IDs that can be used with click, form_input, etc.`;
332
+
333
+ // src/tools/core/screenshot.ts
334
+ var SCREENSHOT_DESCRIPTION = `Take a screenshot of a tab.
335
+
336
+ Returns base64-encoded image data.
337
+ Default format is JPEG with quality 80 (optimized for Native Messaging 1MB limit).
338
+ Use PNG for lossless screenshots when needed.
339
+ Optionally capture a specific region [x0, y0, x1, y1].`;
340
+
341
+ // src/tools/core/scroll.ts
342
+ var SCROLL_DESCRIPTION = `Scroll in a direction at given coordinates, or scroll an element into view by ref.
343
+
344
+ Two modes:
345
+ 1. Directional scroll: provide coordinate + direction (+ optional amount 1-10, default 3).
346
+ 2. Scroll to element: provide ref (element reference ID from read_page/find). Scrolls the element into view using smooth scrolling.`;
347
+
348
+ // src/tools/core/type.ts
349
+ var TYPE_DESCRIPTION = `Type text into the currently focused element.
350
+
351
+ Each character is typed individually with keyDown/keyUp events.
352
+ For special keys (Enter, Tab, etc.), use the 'key' tool instead.`;
353
+
354
+ // src/tools/core/wait-for.ts
355
+ var WAIT_FOR_DESCRIPTION = `Wait for a specified condition before proceeding.
356
+ Supports waiting for a CSS selector to appear in the DOM,
357
+ for a navigation event to complete, or for a fixed timeout duration.
358
+ Useful for synchronizing with asynchronous page updates.`;
359
+
360
+ // src/tools/debug/read-console-messages.ts
361
+ var READ_CONSOLE_MESSAGES_DESCRIPTION = `Read console messages captured from the page.
362
+ Returns log, warning, error, and info messages that have been
363
+ emitted by page scripts. Supports filtering by log level
364
+ and limiting the number of returned entries.`;
365
+
366
+ // src/tools/debug/read-network-requests.ts
367
+ var READ_NETWORK_REQUESTS_DESCRIPTION = `Read network requests captured from the page.
368
+ Returns details of HTTP requests and responses including URL, method,
369
+ status code, headers, and timing information. Supports filtering
370
+ by URL pattern or resource type.`;
371
+
372
+ // src/tools/tabs/select-tab.ts
373
+ var SELECT_TAB_DESCRIPTION = `Switch focus to a specific tab by its identifier.
374
+ Makes the target tab the active tab in the agent's group,
375
+ bringing it to the foreground for subsequent operations
376
+ such as navigation, clicking, or content extraction.`;
377
+
378
+ // src/tools/tabs/tab-close.ts
379
+ var TAB_CLOSE_DESCRIPTION = `Close a tab by its identifier.
380
+ Removes the specified tab from the browser and the agent's tab group.
381
+ If the closed tab was active, focus shifts to the nearest remaining
382
+ tab in the group.`;
383
+
384
+ // src/tools/tabs/tabs-context.ts
385
+ var TABS_CONTEXT_DESCRIPTION = `Get information about the current agent tab group.
386
+ Returns the list of tabs belonging to the agent's assigned group,
387
+ including each tab's ID, URL, title, and active status.
388
+ Useful for understanding the current browsing context.`;
389
+
390
+ // src/tools/tabs/tabs-create.ts
391
+ var TABS_CREATE_DESCRIPTION = `Create a new tab within the agent's tab group.
392
+ Opens a new tab with an optional URL and automatically assigns it
393
+ to the current agent's group. The new tab becomes the active tab
394
+ unless otherwise specified.`;
395
+
396
+ // src/tools/viyv/agent-tab-assign.ts
397
+ var AGENT_TAB_ASSIGN_DESCRIPTION = `Assign a tab group to a specific agent.
398
+ Binds an existing Chrome tab group to the calling agent, granting
399
+ exclusive control over all tabs within the group. Prevents other
400
+ agents from interfering with the assigned tabs.`;
401
+
402
+ // src/tools/viyv/agent-tab-list.ts
403
+ var AGENT_TAB_LIST_DESCRIPTION = `List all agent-to-tab-group mappings.
404
+ Returns a summary of which agents are assigned to which tab groups,
405
+ including the group ID, agent identifier, and the number of tabs
406
+ in each group.`;
407
+
408
+ // src/tools/viyv/artifact-from-page.ts
409
+ var ARTIFACT_FROM_PAGE_DESCRIPTION = `Save the current page as a persistent artifact.
410
+ Captures the page content as HTML, PDF, or screenshot and stores it
411
+ as a named artifact for later reference. Artifacts can be retrieved
412
+ by other tools or returned to the user as output.`;
413
+
414
+ // src/tools/viyv/browser-event-subscribe.ts
415
+ var BROWSER_EVENT_SUBSCRIBE_DESCRIPTION = `Subscribe to browser events for real-time monitoring.
416
+ Registers a listener for specified event types such as navigation,
417
+ tab updates, DOM changes, or network activity. Events are queued
418
+ and delivered on subsequent polling or callback.`;
419
+
420
+ // src/tools/viyv/browser-event-unsubscribe.ts
421
+ var BROWSER_EVENT_UNSUBSCRIBE_DESCRIPTION = `Unsubscribe from previously registered browser events.
422
+ Removes the event listener for the specified subscription ID,
423
+ stopping further event delivery. Cleans up associated resources
424
+ and flushes any remaining queued events.`;
425
+
426
+ // src/tools/viyv/browser-health.ts
427
+ var BROWSER_HEALTH_DESCRIPTION = `Check the health status of the browser connection.
428
+ Verifies that the browser process is running, the CDP connection
429
+ is active, and the agent's tab group is accessible. Returns
430
+ diagnostic information useful for troubleshooting connectivity issues.`;
431
+
432
+ // src/tools/viyv/page-data-extract.ts
433
+ var PAGE_DATA_EXTRACT_DESCRIPTION = `Extract structured data from the current page.
434
+ Parses the page content according to a provided schema or natural
435
+ language description, returning well-formed JSON. Supports extracting
436
+ tables, lists, key-value pairs, and other structured patterns.`;
437
+
438
+ // src/tools/index.ts
439
+ var navigateTool = {
440
+ name: "navigate",
441
+ description: NAVIGATE_DESCRIPTION,
442
+ inputSchema: z.object({
443
+ tabId: z.number().describe("Tab ID to navigate"),
444
+ url: z.string().describe('URL to navigate to, or "back"/"forward" for history')
445
+ })
446
+ };
447
+ var screenshotTool = {
448
+ name: "screenshot",
449
+ description: SCREENSHOT_DESCRIPTION,
450
+ inputSchema: z.object({
451
+ tabId: z.number().describe("Tab ID to capture"),
452
+ format: z.enum(["jpeg", "png"]).optional().describe("Image format (default: jpeg)"),
453
+ quality: z.number().min(1).max(100).optional().describe("JPEG quality (default: 80)"),
454
+ region: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional().describe("Capture region [x0, y0, x1, y1]")
455
+ })
456
+ };
457
+ var clickTool = {
458
+ name: "click",
459
+ description: CLICK_DESCRIPTION,
460
+ inputSchema: z.object({
461
+ tabId: z.number().describe("Tab ID"),
462
+ coordinate: z.tuple([z.number(), z.number()]).optional().describe("Click position [x, y]"),
463
+ ref: z.string().optional().describe("Element reference ID"),
464
+ action: z.enum(["left_click", "right_click", "double_click", "triple_click"]).optional().describe("Click type (default: left_click)"),
465
+ modifiers: z.string().optional().describe('Modifier keys (e.g., "ctrl+shift")')
466
+ })
467
+ };
468
+ var typeTool = {
469
+ name: "type",
470
+ description: TYPE_DESCRIPTION,
471
+ inputSchema: z.object({
472
+ tabId: z.number().describe("Tab ID"),
473
+ text: z.string().describe("Text to type")
474
+ })
475
+ };
476
+ var keyTool = {
477
+ name: "key",
478
+ description: KEY_DESCRIPTION,
479
+ inputSchema: z.object({
480
+ tabId: z.number().describe("Tab ID"),
481
+ keys: z.string().describe('Space-separated keys (e.g., "Enter", "ctrl+a")'),
482
+ repeat: z.number().min(1).max(100).optional().describe("Repeat count")
483
+ })
484
+ };
485
+ var scrollTool = {
486
+ name: "scroll",
487
+ description: SCROLL_DESCRIPTION,
488
+ inputSchema: z.object({
489
+ tabId: z.number().describe("Tab ID"),
490
+ coordinate: z.tuple([z.number(), z.number()]).optional().describe("Scroll position [x, y] (required for directional scroll)"),
491
+ direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction (required for directional scroll)"),
492
+ amount: z.number().min(1).max(10).optional().describe("Scroll amount (default: 3)"),
493
+ ref: z.string().optional().describe("Element reference ID to scroll into view")
494
+ })
495
+ };
496
+ var hoverTool = {
497
+ name: "hover",
498
+ description: HOVER_DESCRIPTION,
499
+ inputSchema: z.object({
500
+ tabId: z.number().describe("Tab ID"),
501
+ coordinate: z.tuple([z.number(), z.number()]).optional().describe("Hover position [x, y]"),
502
+ ref: z.string().optional().describe("Element reference ID")
503
+ })
504
+ };
505
+ var dragTool = {
506
+ name: "drag",
507
+ description: DRAG_DESCRIPTION,
508
+ inputSchema: z.object({
509
+ tabId: z.number().describe("Tab ID"),
510
+ startCoordinate: z.tuple([z.number(), z.number()]).describe("Start position [x, y]"),
511
+ endCoordinate: z.tuple([z.number(), z.number()]).describe("End position [x, y]")
512
+ })
513
+ };
514
+ var readPageTool = {
515
+ name: "read_page",
516
+ description: READ_PAGE_DESCRIPTION,
517
+ inputSchema: z.object({
518
+ tabId: z.number().describe("Tab ID"),
519
+ filter: z.enum(["interactive", "all"]).optional().describe('Filter: "interactive" for buttons/links/inputs, "all" for everything'),
520
+ depth: z.number().min(1).max(20).optional().describe("Max tree depth (default: 8)"),
521
+ refId: z.string().optional().describe("Focus on a specific element by ref"),
522
+ maxChars: z.number().optional().describe("Max output characters (default: 50000)")
523
+ })
524
+ };
525
+ var findTool = {
526
+ name: "find",
527
+ description: FIND_DESCRIPTION,
528
+ inputSchema: z.object({
529
+ tabId: z.number().describe("Tab ID"),
530
+ query: z.string().describe("Natural language description of what to find")
531
+ })
532
+ };
533
+ var formInputTool = {
534
+ name: "form_input",
535
+ description: FORM_INPUT_DESCRIPTION,
536
+ inputSchema: z.object({
537
+ tabId: z.number().describe("Tab ID"),
538
+ ref: z.string().describe("Element reference ID"),
539
+ value: z.union([z.string(), z.boolean(), z.number()]).describe("Value to set")
540
+ })
541
+ };
542
+ var javascriptExecTool = {
543
+ name: "javascript_exec",
544
+ description: JAVASCRIPT_EXEC_DESCRIPTION,
545
+ inputSchema: z.object({
546
+ tabId: z.number().describe("Tab ID"),
547
+ code: z.string().describe("JavaScript code to execute")
548
+ })
549
+ };
550
+ var waitForTool = {
551
+ name: "wait_for",
552
+ description: WAIT_FOR_DESCRIPTION,
553
+ inputSchema: z.object({
554
+ tabId: z.number().describe("Tab ID"),
555
+ selector: z.string().optional().describe("CSS selector to wait for"),
556
+ navigation: z.boolean().optional().describe("Wait for navigation to complete"),
557
+ timeout: z.number().optional().describe("Timeout in ms (default: 30000)")
558
+ })
559
+ };
560
+ var getPageTextTool = {
561
+ name: "get_page_text",
562
+ description: GET_PAGE_TEXT_DESCRIPTION,
563
+ inputSchema: z.object({
564
+ tabId: z.number().describe("Tab ID")
565
+ })
566
+ };
567
+ var handleDialogTool = {
568
+ name: "handle_dialog",
569
+ description: HANDLE_DIALOG_DESCRIPTION,
570
+ inputSchema: z.object({
571
+ tabId: z.number().describe("Tab ID"),
572
+ action: z.enum(["accept", "dismiss"]).describe("Dialog action"),
573
+ text: z.string().optional().describe("Text for prompt dialog")
574
+ })
575
+ };
576
+ var tabsContextTool = {
577
+ name: "tabs_context",
578
+ description: TABS_CONTEXT_DESCRIPTION,
579
+ inputSchema: z.object({
580
+ createIfEmpty: z.boolean().optional().describe("Create a new tab group if none exists")
581
+ })
582
+ };
583
+ var tabsCreateTool = {
584
+ name: "tabs_create",
585
+ description: TABS_CREATE_DESCRIPTION,
586
+ inputSchema: z.object({
587
+ url: z.string().optional().describe("URL to open in the new tab")
588
+ })
589
+ };
590
+ var tabCloseTool = {
591
+ name: "tab_close",
592
+ description: TAB_CLOSE_DESCRIPTION,
593
+ inputSchema: z.object({
594
+ tabId: z.number().describe("Tab ID to close")
595
+ })
596
+ };
597
+ var selectTabTool = {
598
+ name: "select_tab",
599
+ description: SELECT_TAB_DESCRIPTION,
600
+ inputSchema: z.object({
601
+ tabId: z.number().describe("Tab ID to focus")
602
+ })
603
+ };
604
+ var readConsoleMessagesTool = {
605
+ name: "read_console_messages",
606
+ description: READ_CONSOLE_MESSAGES_DESCRIPTION,
607
+ inputSchema: z.object({
608
+ tabId: z.number().describe("Tab ID"),
609
+ pattern: z.string().optional().describe("Regex pattern to filter messages"),
610
+ onlyErrors: z.boolean().optional().describe("Only return errors"),
611
+ limit: z.number().optional().describe("Max messages to return (default: 100)"),
612
+ clear: z.boolean().optional().describe("Clear messages after reading")
613
+ })
614
+ };
615
+ var readNetworkRequestsTool = {
616
+ name: "read_network_requests",
617
+ description: READ_NETWORK_REQUESTS_DESCRIPTION,
618
+ inputSchema: z.object({
619
+ tabId: z.number().describe("Tab ID"),
620
+ urlPattern: z.string().optional().describe("URL pattern to filter requests"),
621
+ limit: z.number().optional().describe("Max requests to return (default: 100)"),
622
+ clear: z.boolean().optional().describe("Clear requests after reading")
623
+ })
624
+ };
625
+ var gifCreatorTool = {
626
+ name: "gif_creator",
627
+ description: GIF_CREATOR_DESCRIPTION,
628
+ inputSchema: z.object({
629
+ tabId: z.number().describe("Tab ID"),
630
+ action: z.enum(["start_recording", "stop_recording", "export", "clear"]).describe("GIF action"),
631
+ filename: z.string().optional().describe("Filename for export"),
632
+ options: z.object({
633
+ showClickIndicators: z.boolean().optional(),
634
+ showDragPaths: z.boolean().optional(),
635
+ showActionLabels: z.boolean().optional(),
636
+ showProgressBar: z.boolean().optional(),
637
+ showWatermark: z.boolean().optional(),
638
+ quality: z.number().min(1).max(30).optional()
639
+ }).optional().describe("GIF rendering options"),
640
+ download: z.boolean().optional().describe("Download the GIF")
641
+ })
642
+ };
643
+ var uploadImageTool = {
644
+ name: "upload_image",
645
+ description: UPLOAD_IMAGE_DESCRIPTION,
646
+ inputSchema: z.object({
647
+ tabId: z.number().describe("Tab ID"),
648
+ imageId: z.string().describe("Image ID from a previous screenshot"),
649
+ ref: z.string().optional().describe("Element reference for file input"),
650
+ coordinate: z.tuple([z.number(), z.number()]).optional().describe("Coordinates for drag & drop")
651
+ })
652
+ };
653
+ var updatePlanTool = {
654
+ name: "update_plan",
655
+ description: UPDATE_PLAN_DESCRIPTION,
656
+ inputSchema: z.object({
657
+ domains: z.array(z.string()).describe("Domains to visit"),
658
+ approach: z.array(z.string()).describe("Steps in the plan")
659
+ })
660
+ };
661
+ var resizeWindowTool = {
662
+ name: "resize_window",
663
+ description: RESIZE_WINDOW_DESCRIPTION,
664
+ inputSchema: z.object({
665
+ tabId: z.number().describe("Tab ID"),
666
+ width: z.number().describe("Window width in pixels"),
667
+ height: z.number().describe("Window height in pixels")
668
+ })
669
+ };
670
+ var shortcutsListTool = {
671
+ name: "shortcuts_list",
672
+ description: SHORTCUTS_LIST_DESCRIPTION,
673
+ inputSchema: z.object({
674
+ tabId: z.number().optional().describe("Tab ID (used to identify the tab group context)")
675
+ })
676
+ };
677
+ var shortcutsExecuteTool = {
678
+ name: "shortcuts_execute",
679
+ description: SHORTCUTS_EXECUTE_DESCRIPTION,
680
+ inputSchema: z.object({
681
+ tabId: z.number().describe("Tab ID to execute the shortcut on"),
682
+ command: z.string().optional().describe("Command name of the shortcut"),
683
+ shortcutId: z.string().optional().describe("ID of the shortcut")
684
+ })
685
+ };
686
+ var switchBrowserTool = {
687
+ name: "switch_browser",
688
+ description: SWITCH_BROWSER_DESCRIPTION,
689
+ inputSchema: z.object({})
690
+ };
691
+ var agentTabAssignTool = {
692
+ name: "agent_tab_assign",
693
+ description: AGENT_TAB_ASSIGN_DESCRIPTION,
694
+ inputSchema: z.object({
695
+ agentId: z.string().describe("Agent ID"),
696
+ agentName: z.string().describe("Display name"),
697
+ color: z.string().optional().describe("Tab group color")
698
+ })
699
+ };
700
+ var agentTabListTool = {
701
+ name: "agent_tab_list",
702
+ description: AGENT_TAB_LIST_DESCRIPTION,
703
+ inputSchema: z.object({})
704
+ };
705
+ var browserEventSubscribeTool = {
706
+ name: "browser_event_subscribe",
707
+ description: BROWSER_EVENT_SUBSCRIBE_DESCRIPTION,
708
+ inputSchema: z.object({
709
+ eventTypes: z.array(z.string()).describe("Event types to subscribe to"),
710
+ urlPattern: z.string().optional().describe("URL pattern filter"),
711
+ conditions: z.record(z.unknown()).optional().describe("Additional conditions")
712
+ })
713
+ };
714
+ var browserEventUnsubscribeTool = {
715
+ name: "browser_event_unsubscribe",
716
+ description: BROWSER_EVENT_UNSUBSCRIBE_DESCRIPTION,
717
+ inputSchema: z.object({
718
+ subscriptionId: z.string().describe("Subscription ID")
719
+ })
720
+ };
721
+ var artifactFromPageTool = {
722
+ name: "artifact_from_page",
723
+ description: ARTIFACT_FROM_PAGE_DESCRIPTION,
724
+ inputSchema: z.object({
725
+ tabId: z.number().describe("Tab ID"),
726
+ type: z.string().describe("Artifact type (text, html, screenshot)"),
727
+ title: z.string().optional().describe("Artifact title")
728
+ })
729
+ };
730
+ var pageDataExtractTool = {
731
+ name: "page_data_extract",
732
+ description: PAGE_DATA_EXTRACT_DESCRIPTION,
733
+ inputSchema: z.object({
734
+ tabId: z.number().describe("Tab ID"),
735
+ schema: z.record(z.unknown()).describe("Data extraction schema"),
736
+ selector: z.string().optional().describe("CSS selector to scope extraction")
737
+ })
738
+ };
739
+ var browserHealthTool = {
740
+ name: "browser_health",
741
+ description: BROWSER_HEALTH_DESCRIPTION,
742
+ inputSchema: z.object({})
743
+ };
744
+ var allTools = [
745
+ // Core (15)
746
+ navigateTool,
747
+ screenshotTool,
748
+ clickTool,
749
+ typeTool,
750
+ keyTool,
751
+ scrollTool,
752
+ hoverTool,
753
+ dragTool,
754
+ readPageTool,
755
+ findTool,
756
+ formInputTool,
757
+ javascriptExecTool,
758
+ waitForTool,
759
+ getPageTextTool,
760
+ handleDialogTool,
761
+ // Tabs (4)
762
+ tabsContextTool,
763
+ tabsCreateTool,
764
+ tabCloseTool,
765
+ selectTabTool,
766
+ // Debug (2)
767
+ readConsoleMessagesTool,
768
+ readNetworkRequestsTool,
769
+ // Advanced (7)
770
+ gifCreatorTool,
771
+ uploadImageTool,
772
+ updatePlanTool,
773
+ resizeWindowTool,
774
+ shortcutsListTool,
775
+ shortcutsExecuteTool,
776
+ switchBrowserTool,
777
+ // viyv Integration (7)
778
+ agentTabAssignTool,
779
+ agentTabListTool,
780
+ browserEventSubscribeTool,
781
+ browserEventUnsubscribeTool,
782
+ artifactFromPageTool,
783
+ pageDataExtractTool,
784
+ browserHealthTool
785
+ ];
786
+
787
+ // src/server.ts
788
+ var pendingRequests = /* @__PURE__ */ new Map();
789
+ var extensionSocket = null;
790
+ async function startMcpServer(socketPath, agentName) {
791
+ if (agentName) {
792
+ setDefaultAgentId(agentName);
793
+ }
794
+ const socketServer = createSocketServer(socketPath);
795
+ const server = new McpServer({
796
+ name: MCP_SERVER.NAME,
797
+ version: MCP_SERVER.VERSION
798
+ });
799
+ for (const tool of allTools) {
800
+ const shape = tool.inputSchema._def.shape?.() ?? {};
801
+ server.tool(tool.name, tool.description, shape, async (params) => {
802
+ const result = await callExtensionTool(tool.name, params);
803
+ if (tool.name === "browser_event_subscribe" && result.content[0]) {
804
+ try {
805
+ const parsed = JSON.parse(result.content[0].text);
806
+ if (parsed.subscriptionId) {
807
+ const p = params;
808
+ addSubscription({
809
+ id: parsed.subscriptionId,
810
+ agentId: getDefaultAgentId(),
811
+ eventTypes: p.eventTypes ?? [],
812
+ urlPattern: p.urlPattern,
813
+ createdAt: Date.now()
814
+ });
815
+ }
816
+ } catch {
817
+ }
818
+ } else if (tool.name === "browser_event_unsubscribe" && result.content[0]) {
819
+ try {
820
+ const parsed = JSON.parse(result.content[0].text);
821
+ if (parsed.subscriptionId) {
822
+ removeSubscription(parsed.subscriptionId);
823
+ }
824
+ } catch {
825
+ }
826
+ }
827
+ return result;
828
+ });
829
+ }
830
+ const transport = new StdioServerTransport();
831
+ await server.connect(transport);
832
+ setEventCallback((event) => {
833
+ server.sendLoggingMessage({
834
+ level: "info",
835
+ data: event
836
+ }).catch(() => {
837
+ });
838
+ });
839
+ process.stderr.write(`[viyv-browser:mcp] MCP Server started, socket: ${socketPath}
840
+ `);
841
+ process.on("exit", () => {
842
+ socketServer.close();
843
+ cleanupSocket(socketPath);
844
+ });
845
+ process.on("SIGINT", () => process.exit(0));
846
+ process.on("SIGTERM", () => process.exit(0));
847
+ }
848
+ function createSocketServer(socketPath) {
849
+ cleanupSocket(socketPath);
850
+ const server = createServer((socket) => {
851
+ if (extensionSocket && !extensionSocket.destroyed) {
852
+ process.stderr.write("[viyv-browser:mcp] Replacing existing extension connection\n");
853
+ extensionSocket.destroy();
854
+ }
855
+ process.stderr.write("[viyv-browser:mcp] Extension connected via Unix socket\n");
856
+ extensionSocket = socket;
857
+ setExtensionConnected(true);
858
+ const agentId = getDefaultAgentId();
859
+ createSession(agentId);
860
+ const initMsg = {
861
+ id: randomUUID2(),
862
+ type: "session_init",
863
+ agentId,
864
+ protocolVersion: PROTOCOL_VERSION,
865
+ timestamp: Date.now()
866
+ };
867
+ socket.write(`${JSON.stringify(initMsg)}
868
+ `);
869
+ let lineBuffer = "";
870
+ socket.on("data", (data) => {
871
+ lineBuffer += data.toString("utf-8");
872
+ const lines = lineBuffer.split("\n");
873
+ lineBuffer = lines.pop() ?? "";
874
+ for (const line of lines) {
875
+ if (!line) continue;
876
+ try {
877
+ let parsed = JSON.parse(line);
878
+ if (parsed.type === "compressed" && typeof parsed.data === "string") {
879
+ const decompressed = decompressPayload(parsed.data, true);
880
+ parsed = JSON.parse(decompressed);
881
+ }
882
+ handleExtensionMessage(parsed);
883
+ } catch (error) {
884
+ process.stderr.write(`[viyv-browser:mcp] Parse error: ${error.message}
885
+ `);
886
+ }
887
+ }
888
+ });
889
+ socket.on("close", () => {
890
+ process.stderr.write("[viyv-browser:mcp] Extension disconnected\n");
891
+ if (extensionSocket === socket) {
892
+ extensionSocket = null;
893
+ setExtensionConnected(false);
894
+ }
895
+ for (const [id, pending] of pendingRequests) {
896
+ clearTimeout(pending.timer);
897
+ pendingRequests.delete(id);
898
+ pending.resolve({
899
+ error: {
900
+ code: "EXTENSION_NOT_CONNECTED",
901
+ message: "Extension disconnected while request was pending"
902
+ }
903
+ });
904
+ }
905
+ });
906
+ socket.on("error", (error) => {
907
+ process.stderr.write(`[viyv-browser:mcp] Socket error: ${error.message}
908
+ `);
909
+ });
910
+ });
911
+ server.listen(socketPath, () => {
912
+ process.stderr.write(`[viyv-browser:mcp] Unix socket listening on ${socketPath}
913
+ `);
914
+ });
915
+ return server;
916
+ }
917
+ function handleExtensionMessage(message) {
918
+ if (!message || typeof message !== "object") return;
919
+ const msg = message;
920
+ const type = typeof msg.type === "string" ? msg.type : null;
921
+ const id = typeof msg.id === "string" ? msg.id : null;
922
+ if (!type) return;
923
+ if (type === "tool_result" && id) {
924
+ const pending = pendingRequests.get(id);
925
+ if (pending) {
926
+ clearTimeout(pending.timer);
927
+ pendingRequests.delete(id);
928
+ if (msg.success) {
929
+ pending.resolve(msg.result ?? {});
930
+ } else {
931
+ const err = msg.error;
932
+ const code = typeof err?.code === "string" ? err.code : "UNKNOWN";
933
+ const errMsg = typeof err?.message === "string" ? err.message : "Unknown error";
934
+ pending.reject(new Error(`[${code}] ${errMsg}`));
935
+ }
936
+ }
937
+ } else if (type === "session_heartbeat") {
938
+ recordHeartbeat();
939
+ const hbAgentId = typeof msg.agentId === "string" ? msg.agentId : null;
940
+ if (hbAgentId) touchSession(hbAgentId);
941
+ } else if (type === "session_close") {
942
+ const closeAgentId = typeof msg.agentId === "string" ? msg.agentId : null;
943
+ if (closeAgentId) {
944
+ closeSession(closeAgentId);
945
+ removeSubscriptionsByAgent(closeAgentId);
946
+ process.stderr.write(`[viyv-browser:mcp] Session closed and cleaned up: ${closeAgentId}
947
+ `);
948
+ }
949
+ } else if (type === "session_recovery") {
950
+ const recoveryAgentId = typeof msg.agentId === "string" ? msg.agentId : null;
951
+ if (recoveryAgentId) {
952
+ createSession(recoveryAgentId);
953
+ process.stderr.write(`[viyv-browser:mcp] Session recovered: ${recoveryAgentId}
954
+ `);
955
+ }
956
+ } else if (type === "session_init") {
957
+ const remoteVersion = typeof msg.protocolVersion === "string" ? msg.protocolVersion : null;
958
+ if (remoteVersion && remoteVersion !== PROTOCOL_VERSION) {
959
+ process.stderr.write(
960
+ `[viyv-browser:mcp] Protocol version mismatch: local=${PROTOCOL_VERSION}, remote=${remoteVersion}
961
+ `
962
+ );
963
+ }
964
+ } else if (type === "browser_event") {
965
+ process.stderr.write(`[viyv-browser:mcp] Browser event: ${String(msg.eventType)}
966
+ `);
967
+ processEvent({
968
+ eventType: msg.eventType,
969
+ agentId: String(msg.agentId ?? ""),
970
+ tabId: Number(msg.tabId ?? 0),
971
+ url: String(msg.url ?? ""),
972
+ payload: msg.payload ?? {},
973
+ sequenceNumber: Number(msg.sequenceNumber ?? 0)
974
+ });
975
+ }
976
+ }
977
+ async function callExtensionTool(tool, input) {
978
+ if (tool === "switch_browser") {
979
+ return handleSwitchBrowser();
980
+ }
981
+ if (!extensionSocket || extensionSocket.destroyed || !isExtensionConnected()) {
982
+ return {
983
+ content: [
984
+ {
985
+ type: "text",
986
+ text: JSON.stringify({
987
+ error: {
988
+ code: "EXTENSION_NOT_CONNECTED",
989
+ message: "Chrome Extension is not connected. Please open Chrome and click the Viyv Browser extension icon."
990
+ }
991
+ })
992
+ }
993
+ ]
994
+ };
995
+ }
996
+ const requestId = randomUUID2();
997
+ const agentId = getDefaultAgentId();
998
+ touchSession(agentId);
999
+ const sock = extensionSocket;
1000
+ let toolTimeout = TIMEOUTS.MCP_TOOL;
1001
+ if (tool === "wait_for" && typeof input.timeout === "number") {
1002
+ toolTimeout = input.timeout + 5e3;
1003
+ }
1004
+ return new Promise((resolve2) => {
1005
+ const onError = () => {
1006
+ const pending = pendingRequests.get(requestId);
1007
+ if (pending) {
1008
+ clearTimeout(pending.timer);
1009
+ pendingRequests.delete(requestId);
1010
+ resolve2({
1011
+ content: [
1012
+ {
1013
+ type: "text",
1014
+ text: JSON.stringify({
1015
+ error: {
1016
+ code: "EXTENSION_NOT_CONNECTED",
1017
+ message: "Socket write failed"
1018
+ }
1019
+ })
1020
+ }
1021
+ ]
1022
+ });
1023
+ }
1024
+ };
1025
+ const removeErrorListener = () => {
1026
+ sock.removeListener("error", onError);
1027
+ };
1028
+ const timer = setTimeout(() => {
1029
+ pendingRequests.delete(requestId);
1030
+ removeErrorListener();
1031
+ resolve2({
1032
+ content: [
1033
+ {
1034
+ type: "text",
1035
+ text: JSON.stringify({
1036
+ error: {
1037
+ code: "TIMEOUT",
1038
+ message: `Tool '${tool}' timed out after ${toolTimeout}ms`
1039
+ }
1040
+ })
1041
+ }
1042
+ ]
1043
+ });
1044
+ }, toolTimeout);
1045
+ pendingRequests.set(requestId, {
1046
+ resolve: (result) => {
1047
+ removeErrorListener();
1048
+ resolve2({
1049
+ content: [{ type: "text", text: JSON.stringify(result) }]
1050
+ });
1051
+ },
1052
+ reject: (error) => {
1053
+ removeErrorListener();
1054
+ resolve2({
1055
+ content: [
1056
+ {
1057
+ type: "text",
1058
+ text: JSON.stringify({
1059
+ error: { code: "CDP_ERROR", message: error.message }
1060
+ })
1061
+ }
1062
+ ]
1063
+ });
1064
+ },
1065
+ timer
1066
+ });
1067
+ const request = {
1068
+ id: requestId,
1069
+ type: "tool_call",
1070
+ agentId,
1071
+ tool,
1072
+ input,
1073
+ timestamp: Date.now()
1074
+ };
1075
+ const written = sock.write(`${JSON.stringify(request)}
1076
+ `);
1077
+ if (!written) {
1078
+ sock.once("drain", () => {
1079
+ });
1080
+ }
1081
+ sock.once("error", onError);
1082
+ });
1083
+ }
1084
+ async function handleSwitchBrowser() {
1085
+ const SWITCH_TIMEOUT = 6e4;
1086
+ if (extensionSocket && !extensionSocket.destroyed) {
1087
+ process.stderr.write("[viyv-browser:mcp] switch_browser: closing current connection\n");
1088
+ extensionSocket.destroy();
1089
+ extensionSocket = null;
1090
+ setExtensionConnected(false);
1091
+ }
1092
+ process.stderr.write("[viyv-browser:mcp] switch_browser: waiting for new browser connection...\n");
1093
+ return new Promise((resolve2) => {
1094
+ const checkInterval = setInterval(() => {
1095
+ if (extensionSocket && !extensionSocket.destroyed && isExtensionConnected()) {
1096
+ clearInterval(checkInterval);
1097
+ clearTimeout(timer);
1098
+ resolve2({
1099
+ content: [
1100
+ {
1101
+ type: "text",
1102
+ text: JSON.stringify({
1103
+ switched: true,
1104
+ message: "Successfully connected to new browser instance."
1105
+ })
1106
+ }
1107
+ ]
1108
+ });
1109
+ }
1110
+ }, 500);
1111
+ const timer = setTimeout(() => {
1112
+ clearInterval(checkInterval);
1113
+ resolve2({
1114
+ content: [
1115
+ {
1116
+ type: "text",
1117
+ text: JSON.stringify({
1118
+ error: {
1119
+ code: "TIMEOUT",
1120
+ message: `No new browser connected within ${SWITCH_TIMEOUT / 1e3}s. Please open Chrome and click the Viyv Browser extension icon.`
1121
+ }
1122
+ })
1123
+ }
1124
+ ]
1125
+ });
1126
+ }, SWITCH_TIMEOUT);
1127
+ });
1128
+ }
1129
+ function cleanupSocket(socketPath) {
1130
+ if (existsSync(socketPath)) {
1131
+ try {
1132
+ unlinkSync(socketPath);
1133
+ } catch {
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ // src/native-host/bridge.ts
1139
+ import { createConnection } from "net";
1140
+
1141
+ // src/native-host/transport.ts
1142
+ var MAX_MESSAGE_SIZE = 1024 * 1024;
1143
+ function encodeMessage(message) {
1144
+ const json = JSON.stringify(message);
1145
+ const body = Buffer.from(json, "utf-8");
1146
+ if (body.length > MAX_MESSAGE_SIZE) {
1147
+ throw new Error(`Message too large: ${body.length} bytes (max ${MAX_MESSAGE_SIZE})`);
1148
+ }
1149
+ const header = Buffer.alloc(4);
1150
+ header.writeUInt32LE(body.length, 0);
1151
+ return Buffer.concat([header, body]);
1152
+ }
1153
+ function createMessageReader(stream, onMessage, onError, onClose) {
1154
+ let buffer = Buffer.alloc(0);
1155
+ let expectedLength = null;
1156
+ stream.on("data", (chunk) => {
1157
+ buffer = Buffer.concat([buffer, chunk]);
1158
+ while (true) {
1159
+ if (expectedLength === null) {
1160
+ if (buffer.length < 4) break;
1161
+ expectedLength = buffer.readUInt32LE(0);
1162
+ buffer = buffer.subarray(4);
1163
+ if (expectedLength > MAX_MESSAGE_SIZE) {
1164
+ onError?.(new Error(`Message too large: ${expectedLength} bytes`));
1165
+ expectedLength = null;
1166
+ buffer = Buffer.alloc(0);
1167
+ break;
1168
+ }
1169
+ }
1170
+ if (buffer.length < expectedLength) break;
1171
+ const jsonBuffer = buffer.subarray(0, expectedLength);
1172
+ buffer = buffer.subarray(expectedLength);
1173
+ expectedLength = null;
1174
+ try {
1175
+ const message = JSON.parse(jsonBuffer.toString("utf-8"));
1176
+ onMessage(message);
1177
+ } catch (error) {
1178
+ onError?.(new Error(`Invalid JSON: ${error.message}`));
1179
+ }
1180
+ }
1181
+ });
1182
+ stream.on("error", (error) => {
1183
+ onError?.(new Error(`Stream error: ${error.message}`));
1184
+ });
1185
+ stream.on("end", () => {
1186
+ onClose?.();
1187
+ });
1188
+ stream.on("close", () => {
1189
+ onClose?.();
1190
+ });
1191
+ }
1192
+ function writeMessage(stream, message) {
1193
+ const encoded = encodeMessage(message);
1194
+ stream.write(encoded);
1195
+ }
1196
+
1197
+ // src/native-host/bridge.ts
1198
+ var MAX_BUFFER_SIZE = 1e3;
1199
+ function startBridge(options) {
1200
+ const { socketPath, onError } = options;
1201
+ let socket = null;
1202
+ let reconnecting = false;
1203
+ let retryCount = 0;
1204
+ const pendingMessages = [];
1205
+ function flushBuffer() {
1206
+ if (!socket || socket.destroyed) return;
1207
+ while (pendingMessages.length > 0) {
1208
+ const msg = pendingMessages[0];
1209
+ try {
1210
+ const written = socket.write(`${JSON.stringify(msg)}
1211
+ `);
1212
+ pendingMessages.shift();
1213
+ if (!written) {
1214
+ socket.once("drain", () => flushBuffer());
1215
+ return;
1216
+ }
1217
+ } catch (error) {
1218
+ onError?.(error);
1219
+ return;
1220
+ }
1221
+ }
1222
+ }
1223
+ function connectSocket() {
1224
+ socket = createConnection(socketPath);
1225
+ socket.on("connect", () => {
1226
+ process.stderr.write(
1227
+ `[viyv-browser:native-host] Connected to MCP server at ${socketPath}
1228
+ `
1229
+ );
1230
+ retryCount = 0;
1231
+ flushBuffer();
1232
+ });
1233
+ let lineBuffer = "";
1234
+ socket.on("data", (data) => {
1235
+ lineBuffer += data.toString("utf-8");
1236
+ const lines = lineBuffer.split("\n");
1237
+ lineBuffer = lines.pop() ?? "";
1238
+ for (const line of lines) {
1239
+ if (!line) continue;
1240
+ try {
1241
+ let message = JSON.parse(line);
1242
+ if (message.type === "compressed" && typeof message.data === "string") {
1243
+ const decompressed = decompressPayload(message.data, true);
1244
+ message = JSON.parse(decompressed);
1245
+ }
1246
+ writeMessage(process.stdout, message);
1247
+ } catch (error) {
1248
+ onError?.(error);
1249
+ }
1250
+ }
1251
+ });
1252
+ socket.on("error", (error) => {
1253
+ process.stderr.write(
1254
+ `[viyv-browser:native-host] Socket error: ${error.message}
1255
+ `
1256
+ );
1257
+ onError?.(error);
1258
+ });
1259
+ socket.on("close", () => {
1260
+ process.stderr.write("[viyv-browser:native-host] Socket closed\n");
1261
+ socket = null;
1262
+ if (!reconnecting) {
1263
+ reconnecting = true;
1264
+ const delay = Math.min(
1265
+ RECONNECT.INITIAL_DELAY * Math.pow(RECONNECT.MULTIPLIER, retryCount),
1266
+ RECONNECT.MAX_DELAY
1267
+ );
1268
+ retryCount++;
1269
+ process.stderr.write(
1270
+ `[viyv-browser:native-host] Reconnecting in ${delay}ms (attempt ${retryCount})
1271
+ `
1272
+ );
1273
+ setTimeout(() => {
1274
+ reconnecting = false;
1275
+ connectSocket();
1276
+ }, delay);
1277
+ }
1278
+ });
1279
+ }
1280
+ createMessageReader(
1281
+ process.stdin,
1282
+ (message) => {
1283
+ if (socket && !socket.destroyed) {
1284
+ const json = JSON.stringify(message);
1285
+ if (json.length > LIMITS.CHUNK_SIZE) {
1286
+ const { compressed, wasCompressed } = compressPayload(json);
1287
+ if (wasCompressed) {
1288
+ socket.write(`${JSON.stringify({ type: "compressed", data: compressed })}
1289
+ `);
1290
+ } else {
1291
+ socket.write(`${json}
1292
+ `);
1293
+ }
1294
+ } else {
1295
+ socket.write(`${json}
1296
+ `);
1297
+ }
1298
+ } else {
1299
+ if (pendingMessages.length < MAX_BUFFER_SIZE) {
1300
+ pendingMessages.push(message);
1301
+ } else {
1302
+ process.stderr.write(
1303
+ "[viyv-browser:native-host] Message buffer full, dropping message\n"
1304
+ );
1305
+ }
1306
+ }
1307
+ },
1308
+ onError
1309
+ );
1310
+ connectSocket();
1311
+ process.on("SIGINT", () => {
1312
+ socket?.destroy();
1313
+ process.exit(0);
1314
+ });
1315
+ process.on("SIGTERM", () => {
1316
+ socket?.destroy();
1317
+ process.exit(0);
1318
+ });
1319
+ process.stdin.on("end", () => {
1320
+ process.stderr.write("[viyv-browser:native-host] stdin closed, shutting down\n");
1321
+ socket?.destroy();
1322
+ process.exit(0);
1323
+ });
1324
+ }
1325
+
1326
+ // src/setup.ts
1327
+ import { writeFileSync, mkdirSync, chmodSync, existsSync as existsSync2 } from "fs";
1328
+ import { resolve, dirname } from "path";
1329
+ import { homedir, platform } from "os";
1330
+ import { execSync } from "child_process";
1331
+ function runSetup(options = {}) {
1332
+ const os = platform();
1333
+ const binaryPath = getBinaryPath();
1334
+ console.log("Viyv Browser MCP - Native Messaging Host Setup");
1335
+ console.log("================================================");
1336
+ console.log(`Platform: ${os}`);
1337
+ console.log(`Binary: ${binaryPath}`);
1338
+ if (!existsSync2(binaryPath)) {
1339
+ console.error(`WARNING: Binary not found at ${binaryPath}`);
1340
+ console.error("The Native Messaging Host may not work until the binary is available.");
1341
+ }
1342
+ const allowedOrigins = options.extensionId ? [`chrome-extension://${options.extensionId}/`] : ["chrome-extension://*/"];
1343
+ if (!options.extensionId) {
1344
+ process.stderr.write(
1345
+ "WARNING: Using wildcard allowed_origins (chrome-extension://*/). This allows any Chrome extension to connect. For production, specify --extension-id to restrict access.\n"
1346
+ );
1347
+ }
1348
+ const wrapperPath = createNativeHostWrapper(os, binaryPath);
1349
+ const manifest = {
1350
+ name: NATIVE_HOST_NAME,
1351
+ description: "Viyv Browser MCP Native Messaging Host",
1352
+ path: wrapperPath,
1353
+ type: "stdio",
1354
+ allowed_origins: allowedOrigins
1355
+ };
1356
+ const manifestPath = getManifestPath(os);
1357
+ const manifestDir = dirname(manifestPath);
1358
+ console.log(`Wrapper: ${wrapperPath}`);
1359
+ console.log(`Manifest path: ${manifestPath}`);
1360
+ mkdirSync(manifestDir, { recursive: true });
1361
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1362
+ chmodSync(manifestPath, 420);
1363
+ console.log("\nNative Messaging Host registered successfully!");
1364
+ console.log("\nNext steps:");
1365
+ console.log("1. Start the MCP Server: node <path>/dist/index.js");
1366
+ console.log("2. Install the Viyv Browser Chrome Extension");
1367
+ console.log("3. Click the extension icon to connect");
1368
+ }
1369
+ function getBinaryPath() {
1370
+ const whichCmd = process.platform === "win32" ? "where" : "which";
1371
+ try {
1372
+ const found = execSync(`${whichCmd} viyv-browser-mcp`, { encoding: "utf-8" }).trim();
1373
+ if (found) return found;
1374
+ } catch {
1375
+ }
1376
+ const currentScript = process.argv[1];
1377
+ return resolve(currentScript);
1378
+ }
1379
+ function createNativeHostWrapper(os, binaryPath) {
1380
+ const manifestDir = dirname(getManifestPath(os));
1381
+ mkdirSync(manifestDir, { recursive: true });
1382
+ const nodePath = getNodePath();
1383
+ if (os === "win32") {
1384
+ const wrapperPath2 = resolve(manifestDir, `${NATIVE_HOST_NAME}.bat`);
1385
+ writeFileSync(wrapperPath2, `@echo off\r
1386
+ "${nodePath}" "${binaryPath}" --native-host\r
1387
+ `);
1388
+ return wrapperPath2;
1389
+ }
1390
+ const wrapperPath = resolve(manifestDir, `${NATIVE_HOST_NAME}.sh`);
1391
+ writeFileSync(
1392
+ wrapperPath,
1393
+ `#!/bin/bash
1394
+ exec "${nodePath}" "${binaryPath}" --native-host
1395
+ `
1396
+ );
1397
+ chmodSync(wrapperPath, 493);
1398
+ return wrapperPath;
1399
+ }
1400
+ function getNodePath() {
1401
+ try {
1402
+ return execSync("which node", { encoding: "utf-8" }).trim();
1403
+ } catch {
1404
+ return process.execPath;
1405
+ }
1406
+ }
1407
+ function getManifestPath(os) {
1408
+ const home = homedir();
1409
+ switch (os) {
1410
+ case "darwin":
1411
+ return resolve(
1412
+ home,
1413
+ "Library/Application Support/Google/Chrome/NativeMessagingHosts",
1414
+ `${NATIVE_HOST_NAME}.json`
1415
+ );
1416
+ case "linux":
1417
+ return resolve(
1418
+ home,
1419
+ ".config/google-chrome/NativeMessagingHosts",
1420
+ `${NATIVE_HOST_NAME}.json`
1421
+ );
1422
+ case "win32":
1423
+ return resolve(
1424
+ home,
1425
+ "AppData/Local/Google/Chrome/User Data/NativeMessagingHosts",
1426
+ `${NATIVE_HOST_NAME}.json`
1427
+ );
1428
+ default:
1429
+ throw new Error(`Unsupported platform: ${os}`);
1430
+ }
1431
+ }
1432
+
1433
+ // src/index.ts
1434
+ var args = process.argv.slice(2);
1435
+ if (args.includes("setup")) {
1436
+ const extensionIdIdx = args.indexOf("--extension-id");
1437
+ const extensionId = extensionIdIdx >= 0 ? args[extensionIdIdx + 1] : void 0;
1438
+ runSetup({ extensionId });
1439
+ } else if (args.includes("--native-host")) {
1440
+ const socketPath = findSocketPath();
1441
+ if (!socketPath) {
1442
+ process.stderr.write(
1443
+ "[viyv-browser:native-host] No MCP server socket found. Is the MCP server running?\n"
1444
+ );
1445
+ process.exit(1);
1446
+ }
1447
+ startBridge({
1448
+ socketPath,
1449
+ onError: (error) => {
1450
+ process.stderr.write(`[viyv-browser:native-host] Error: ${error.message}
1451
+ `);
1452
+ }
1453
+ });
1454
+ } else {
1455
+ const socketPath = "/tmp/viyv-browser.sock";
1456
+ const agentNameIdx = args.indexOf("--agent-name");
1457
+ const agentName = agentNameIdx >= 0 ? args[agentNameIdx + 1] : void 0;
1458
+ startMcpServer(socketPath, agentName);
1459
+ }
1460
+ function findSocketPath() {
1461
+ const envSocket = process.env.VIYV_BROWSER_SOCKET;
1462
+ if (envSocket) return envSocket;
1463
+ const fixedPath = "/tmp/viyv-browser.sock";
1464
+ if (existsSync3(fixedPath)) return fixedPath;
1465
+ try {
1466
+ const tmpFiles = readdirSync("/tmp");
1467
+ const socketFile = tmpFiles.find((f) => f.startsWith("viyv-browser-") && f.endsWith(".sock"));
1468
+ if (socketFile) return `/tmp/${socketFile}`;
1469
+ } catch {
1470
+ }
1471
+ return null;
1472
+ }
1473
+ //# sourceMappingURL=index.js.map