viyv-browser-mcp 0.7.2 → 0.7.4
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 +269 -64
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -88,6 +88,57 @@ var MCP_SERVER = {
|
|
|
88
88
|
SOCKET_PATH_TEMPLATE: "/tmp/viyv-browser-{pid}.sock"
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
// src/native-host-updater.ts
|
|
92
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from "fs";
|
|
93
|
+
import { homedir } from "os";
|
|
94
|
+
import { resolve } from "path";
|
|
95
|
+
import { fileURLToPath } from "url";
|
|
96
|
+
var NATIVE_HOST_DIR = resolve(homedir(), ".viyv-browser", "native-host");
|
|
97
|
+
function getSourceBinaryPath() {
|
|
98
|
+
return fileURLToPath(import.meta.url);
|
|
99
|
+
}
|
|
100
|
+
function getNativeHostBinaryPath() {
|
|
101
|
+
return resolve(NATIVE_HOST_DIR, "index.js");
|
|
102
|
+
}
|
|
103
|
+
function isNativeHostDeployed() {
|
|
104
|
+
return existsSync(getNativeHostBinaryPath());
|
|
105
|
+
}
|
|
106
|
+
function syncNativeHostBinary() {
|
|
107
|
+
const source = getSourceBinaryPath();
|
|
108
|
+
if (!existsSync(source)) {
|
|
109
|
+
throw new Error(`Source binary not found: ${source}`);
|
|
110
|
+
}
|
|
111
|
+
mkdirSync(NATIVE_HOST_DIR, { recursive: true });
|
|
112
|
+
const target = getNativeHostBinaryPath();
|
|
113
|
+
const tmp = resolve(NATIVE_HOST_DIR, `index.js.tmp.${process.pid}`);
|
|
114
|
+
try {
|
|
115
|
+
copyFileSync(source, tmp);
|
|
116
|
+
chmodSync(tmp, 493);
|
|
117
|
+
renameSync(tmp, target);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
try {
|
|
120
|
+
unlinkSync(tmp);
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function shouldUpdateBridge(bridgeVersion) {
|
|
127
|
+
return compareSemver("0.7.4", bridgeVersion) > 0;
|
|
128
|
+
}
|
|
129
|
+
function getPackageVersion() {
|
|
130
|
+
return "0.7.4";
|
|
131
|
+
}
|
|
132
|
+
function compareSemver(a, b) {
|
|
133
|
+
const pa = a.split(".").map(Number);
|
|
134
|
+
const pb = b.split(".").map(Number);
|
|
135
|
+
for (let i = 0; i < 3; i++) {
|
|
136
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
137
|
+
if (diff !== 0) return diff;
|
|
138
|
+
}
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
91
142
|
// src/native-host/bridge-relay.ts
|
|
92
143
|
import { createConnection } from "net";
|
|
93
144
|
|
|
@@ -382,6 +433,7 @@ function startBridge(options) {
|
|
|
382
433
|
const msg = {
|
|
383
434
|
type: "bridge_status",
|
|
384
435
|
connected: true,
|
|
436
|
+
bridgeVersion: getPackageVersion(),
|
|
385
437
|
chromeProfiles: getChromeProfilesList(),
|
|
386
438
|
timestamp: Date.now()
|
|
387
439
|
};
|
|
@@ -460,6 +512,28 @@ function startBridge(options) {
|
|
|
460
512
|
} else if (type === "bridge_status" || type === "bridge_closing") {
|
|
461
513
|
process.stderr.write(`${LOG_PREFIX2} Ignoring ${type} from MCP Server ${connId}
|
|
462
514
|
`);
|
|
515
|
+
} else if (type === "bridge_shutdown_request") {
|
|
516
|
+
process.stderr.write(`${LOG_PREFIX2} Shutdown requested by MCP Server ${connId}
|
|
517
|
+
`);
|
|
518
|
+
const DRAIN_TIMEOUT = 5e3;
|
|
519
|
+
const drainAndExit = () => {
|
|
520
|
+
if (requestOrigin.size === 0) {
|
|
521
|
+
gracefulExit("Bridge shutdown requested for version update");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
process.stderr.write(
|
|
525
|
+
`${LOG_PREFIX2} Waiting for ${requestOrigin.size} pending request(s) to drain...
|
|
526
|
+
`
|
|
527
|
+
);
|
|
528
|
+
const start = Date.now();
|
|
529
|
+
const check = setInterval(() => {
|
|
530
|
+
if (requestOrigin.size === 0 || Date.now() - start > DRAIN_TIMEOUT) {
|
|
531
|
+
clearInterval(check);
|
|
532
|
+
gracefulExit("Bridge shutdown requested for version update");
|
|
533
|
+
}
|
|
534
|
+
}, 200);
|
|
535
|
+
};
|
|
536
|
+
drainAndExit();
|
|
463
537
|
} else {
|
|
464
538
|
sendToChrome(agentId ? getChromeForAgent(agentId) : primaryChromeId, message);
|
|
465
539
|
}
|
|
@@ -684,6 +758,7 @@ function startBridge(options) {
|
|
|
684
758
|
sendToMcp(connId, {
|
|
685
759
|
type: "bridge_status",
|
|
686
760
|
connected: true,
|
|
761
|
+
bridgeVersion: getPackageVersion(),
|
|
687
762
|
chromeProfiles: getChromeProfilesList(),
|
|
688
763
|
timestamp: Date.now()
|
|
689
764
|
});
|
|
@@ -770,7 +845,7 @@ function startBridge(options) {
|
|
|
770
845
|
|
|
771
846
|
// src/server.ts
|
|
772
847
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
773
|
-
import { existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
848
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, statSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
|
|
774
849
|
import http from "http";
|
|
775
850
|
import { createConnection as createConnection2 } from "net";
|
|
776
851
|
import { dirname } from "path";
|
|
@@ -949,8 +1024,15 @@ var TOPICS = {
|
|
|
949
1024
|
- Don't use for: checking text values, verifying form state
|
|
950
1025
|
- Do use for: layout verification, image content, charts
|
|
951
1026
|
|
|
1027
|
+
6. indexed_page \u2192 Alternative text-based DOM representation with numbered interactive elements.
|
|
1028
|
+
- Returns [0]<button>Submit</>, [1]<input placeholder=Search/>, etc.
|
|
1029
|
+
- Use indexed_action(snapshotId, index, action) to interact by number
|
|
1030
|
+
- No screenshots needed \u2014 works with text-only LLMs, lower token cost
|
|
1031
|
+
- snapshotId expires on page change \u2014 call indexed_page again if stale
|
|
1032
|
+
|
|
952
1033
|
Recommended workflow:
|
|
953
|
-
page_outline \u2192 read_page(section) \u2192 find(specific element) \u2192 click/form_input
|
|
1034
|
+
page_outline \u2192 read_page(section) \u2192 find(specific element) \u2192 click/form_input
|
|
1035
|
+
OR: indexed_page \u2192 indexed_action(click/type/select_option) for lower-cost automation`
|
|
954
1036
|
},
|
|
955
1037
|
interaction: {
|
|
956
1038
|
title: "Interaction \u2014 How to interact with elements",
|
|
@@ -998,6 +1080,14 @@ file_upload \u2014 For uploading files or dropping files onto elements
|
|
|
998
1080
|
- Don't click file inputs \u2014 use file_upload instead
|
|
999
1081
|
- Best for: file inputs, canvas apps (Canva, Figma), custom drop zones
|
|
1000
1082
|
|
|
1083
|
+
indexed_action \u2014 Index-based interaction (alternative to ref-based tools above)
|
|
1084
|
+
- Requires snapshotId from indexed_page
|
|
1085
|
+
- Actions: click, type, select_option, hover, scroll_to
|
|
1086
|
+
- click/hover: CDP dispatch (trusted events, SPA compatible)
|
|
1087
|
+
- type: CDP click to focus + content script value setting
|
|
1088
|
+
- select_option: content script DOM manipulation
|
|
1089
|
+
- Best for: batch automation with text-only LLMs, lower token cost
|
|
1090
|
+
|
|
1001
1091
|
Common patterns:
|
|
1002
1092
|
Single field: find(input) \u2192 form_input(ref, value, submit: true)
|
|
1003
1093
|
Multi-field form: read_page \u2192 form_fill([{ref, value}, ...], submit: true)
|
|
@@ -1006,7 +1096,8 @@ Common patterns:
|
|
|
1006
1096
|
Dropdown: click(select ref) \u2192 key("ArrowDown ArrowDown Enter")
|
|
1007
1097
|
Search: form_input(search input ref, query, submit: true)
|
|
1008
1098
|
File upload: find(file input) \u2192 file_upload(ref, paths)
|
|
1009
|
-
File drop: find(canvas) \u2192 file_upload(ref, paths) or file_upload(coordinate, paths)
|
|
1099
|
+
File drop: find(canvas) \u2192 file_upload(ref, paths) or file_upload(coordinate, paths)
|
|
1100
|
+
Index-based: indexed_page \u2192 indexed_action(click, index=N) \u2192 wait_for(navigation)`
|
|
1010
1101
|
},
|
|
1011
1102
|
navigation: {
|
|
1012
1103
|
title: "Navigation \u2014 How to navigate between pages",
|
|
@@ -1437,7 +1528,7 @@ var CLICK_RETURNS = `{
|
|
|
1437
1528
|
clicked: true
|
|
1438
1529
|
newTab?: { tabId, url } // only if target="_blank" link detected
|
|
1439
1530
|
}`;
|
|
1440
|
-
var CLICK_RELATED = ["form_input", "hover", "drag", "javascript_exec"];
|
|
1531
|
+
var CLICK_RELATED = ["form_input", "hover", "drag", "javascript_exec", "indexed_action"];
|
|
1441
1532
|
|
|
1442
1533
|
// src/tools/core/drag.ts
|
|
1443
1534
|
var DRAG_DESCRIPTION = `Drag from start coordinate to end coordinate.
|
|
@@ -1533,7 +1624,7 @@ var FORM_INPUT_RETURNS = `{
|
|
|
1533
1624
|
navigated?: boolean // if page navigated after submit
|
|
1534
1625
|
url?: string // new URL if navigated
|
|
1535
1626
|
}`;
|
|
1536
|
-
var FORM_INPUT_RELATED = ["click", "type", "find", "read_page"];
|
|
1627
|
+
var FORM_INPUT_RELATED = ["click", "type", "find", "read_page", "indexed_action"];
|
|
1537
1628
|
|
|
1538
1629
|
// src/tools/core/get-page-text.ts
|
|
1539
1630
|
var GET_PAGE_TEXT_DESCRIPTION = `Extract readable text from page. Use query for semantic filtering.
|
|
@@ -1553,6 +1644,66 @@ var GET_PAGE_TEXT_RETURNS = `// Without query:
|
|
|
1553
1644
|
{ sections: [{ index, tag, role, ariaLabel, heading, text, truncated }], truncated: boolean, charCount: number }`;
|
|
1554
1645
|
var GET_PAGE_TEXT_RELATED = ["read_page", "find", "screenshot"];
|
|
1555
1646
|
|
|
1647
|
+
// src/tools/core/indexed-action.ts
|
|
1648
|
+
var INDEXED_ACTION_DESCRIPTION = `Interact with an element by its [index] from indexed_page. Actions: click, type, select_option, hover, scroll_to.
|
|
1649
|
+
Returns: { success, action, index }`;
|
|
1650
|
+
var INDEXED_ACTION_DETAIL = `Act on an indexed element from indexed_page output.
|
|
1651
|
+
|
|
1652
|
+
Actions:
|
|
1653
|
+
- click: Click the element. Supports modifier keys (ctrl, shift, alt, meta).
|
|
1654
|
+
- type: Set text value on input/textarea/contenteditable. Clears existing content.
|
|
1655
|
+
- select_option: Select a dropdown option by text match.
|
|
1656
|
+
- hover: Hover over the element.
|
|
1657
|
+
- scroll_to: Scroll the element into view.
|
|
1658
|
+
|
|
1659
|
+
The snapshotId from indexed_page is required. If the page has changed since indexed_page was called, this tool returns an error \u2014 call indexed_page again to get a fresh snapshot.
|
|
1660
|
+
|
|
1661
|
+
Examples:
|
|
1662
|
+
indexed_action(tabId=1, snapshotId=3, index=2, action="click")
|
|
1663
|
+
indexed_action(tabId=1, snapshotId=3, index=1, action="type", text="hello")
|
|
1664
|
+
indexed_action(tabId=1, snapshotId=3, index=5, action="select_option", option="Option A")`;
|
|
1665
|
+
var INDEXED_ACTION_RETURNS = `{
|
|
1666
|
+
action: string // the action performed
|
|
1667
|
+
index: number // the element index
|
|
1668
|
+
success: boolean // whether the action succeeded
|
|
1669
|
+
value?: string // set value (for type/select_option)
|
|
1670
|
+
tagName?: string // element tag name
|
|
1671
|
+
}`;
|
|
1672
|
+
var INDEXED_ACTION_RELATED = ["indexed_page", "click", "form_input"];
|
|
1673
|
+
|
|
1674
|
+
// src/tools/core/indexed-page.ts
|
|
1675
|
+
var INDEXED_PAGE_DESCRIPTION = `Get page DOM as indexed text. Interactive elements marked [0], [1], etc. Use indexed_action to act by index.
|
|
1676
|
+
Returns: { tree, snapshotId, elementCount, interactiveCount }`;
|
|
1677
|
+
var INDEXED_PAGE_DETAIL = `Alternative to read_page that returns a text-based DOM representation with numbered interactive elements.
|
|
1678
|
+
|
|
1679
|
+
Format example:
|
|
1680
|
+
[0]<a aria-label=Home>Home />
|
|
1681
|
+
[1]<input placeholder=Search />
|
|
1682
|
+
[2]<button>Submit />
|
|
1683
|
+
Some text content
|
|
1684
|
+
[3]<a>Learn more />
|
|
1685
|
+
|
|
1686
|
+
Each [N] marks an interactive element. Use indexed_action with the index number to click, type, or interact.
|
|
1687
|
+
|
|
1688
|
+
Options:
|
|
1689
|
+
- viewportOnly: only include elements visible in the viewport (default: false = full page)
|
|
1690
|
+
- maxChars: limit output size (default 50000)
|
|
1691
|
+
- includeAttributes: additional HTML attributes to include in output
|
|
1692
|
+
|
|
1693
|
+
The returned snapshotId must be passed to indexed_action. It expires after page changes \u2014 call indexed_page again if indexed_action reports a stale snapshot.
|
|
1694
|
+
|
|
1695
|
+
Advantages over read_page:
|
|
1696
|
+
- Lower token cost (no screenshots needed)
|
|
1697
|
+
- Works with text-only LLMs
|
|
1698
|
+
- Direct index-based interaction without ref resolution`;
|
|
1699
|
+
var INDEXED_PAGE_RETURNS = `{
|
|
1700
|
+
tree: string // indexed DOM text
|
|
1701
|
+
snapshotId: number // pass to indexed_action
|
|
1702
|
+
elementCount: number // total elements
|
|
1703
|
+
interactiveCount: number // indexed interactive elements
|
|
1704
|
+
}`;
|
|
1705
|
+
var INDEXED_PAGE_RELATED = ["indexed_action", "read_page", "screenshot"];
|
|
1706
|
+
|
|
1556
1707
|
// src/tools/core/handle-dialog.ts
|
|
1557
1708
|
var HANDLE_DIALOG_DESCRIPTION = `Handle JS dialog (alert/confirm/prompt) or set next auto-handle policy.
|
|
1558
1709
|
Returns: { handled } or { policy_set }`;
|
|
@@ -1687,7 +1838,7 @@ var PAGE_OUTLINE_RETURNS = `{
|
|
|
1687
1838
|
}]
|
|
1688
1839
|
note?: string // only if no landmarks found
|
|
1689
1840
|
}`;
|
|
1690
|
-
var PAGE_OUTLINE_RELATED = ["read_page", "find", "get_page_text"];
|
|
1841
|
+
var PAGE_OUTLINE_RELATED = ["read_page", "find", "get_page_text", "indexed_page"];
|
|
1691
1842
|
|
|
1692
1843
|
// src/tools/core/read-page.ts
|
|
1693
1844
|
var READ_PAGE_DESCRIPTION = `Get page accessibility tree with element refs for interaction. Call page_outline first on unfamiliar pages.
|
|
@@ -1712,7 +1863,7 @@ var READ_PAGE_RETURNS = `{
|
|
|
1712
1863
|
elementCount: number // total elements in tree
|
|
1713
1864
|
truncated: boolean // whether output hit maxChars limit
|
|
1714
1865
|
}`;
|
|
1715
|
-
var READ_PAGE_RELATED = ["page_outline", "find", "get_page_text", "screenshot"];
|
|
1866
|
+
var READ_PAGE_RELATED = ["page_outline", "find", "get_page_text", "screenshot", "indexed_page"];
|
|
1716
1867
|
|
|
1717
1868
|
// src/tools/core/read-table.ts
|
|
1718
1869
|
var READ_TABLE_DESCRIPTION = `Extract HTML table data from web page by ref. Supports <table> and ARIA role="table".
|
|
@@ -3361,6 +3512,37 @@ var readPageTool = {
|
|
|
3361
3512
|
)
|
|
3362
3513
|
})
|
|
3363
3514
|
};
|
|
3515
|
+
var indexedPageTool = {
|
|
3516
|
+
name: "indexed_page",
|
|
3517
|
+
description: INDEXED_PAGE_DESCRIPTION,
|
|
3518
|
+
detail: INDEXED_PAGE_DETAIL,
|
|
3519
|
+
returns: INDEXED_PAGE_RETURNS,
|
|
3520
|
+
category: "core",
|
|
3521
|
+
related: INDEXED_PAGE_RELATED,
|
|
3522
|
+
inputSchema: z.object({
|
|
3523
|
+
tabId: z.coerce.number().describe("Tab ID"),
|
|
3524
|
+
viewportOnly: z.boolean().optional().describe("Only include viewport-visible elements (default: false)"),
|
|
3525
|
+
maxChars: z.coerce.number().optional().describe("Max output characters (default: 50000)"),
|
|
3526
|
+
includeAttributes: z.array(z.string()).optional().describe("Additional HTML attributes to include in output")
|
|
3527
|
+
})
|
|
3528
|
+
};
|
|
3529
|
+
var indexedActionTool = {
|
|
3530
|
+
name: "indexed_action",
|
|
3531
|
+
description: INDEXED_ACTION_DESCRIPTION,
|
|
3532
|
+
detail: INDEXED_ACTION_DETAIL,
|
|
3533
|
+
returns: INDEXED_ACTION_RETURNS,
|
|
3534
|
+
category: "core",
|
|
3535
|
+
related: INDEXED_ACTION_RELATED,
|
|
3536
|
+
inputSchema: z.object({
|
|
3537
|
+
tabId: z.coerce.number().describe("Tab ID"),
|
|
3538
|
+
snapshotId: z.coerce.number().describe("Snapshot ID from indexed_page"),
|
|
3539
|
+
index: z.coerce.number().min(0).describe("Element index from indexed_page output"),
|
|
3540
|
+
action: z.enum(["click", "type", "select_option", "hover", "scroll_to"]).describe("Action to perform on the element"),
|
|
3541
|
+
text: z.string().optional().describe('Text to type (required for action: "type")'),
|
|
3542
|
+
option: z.string().optional().describe('Option text to select (required for action: "select_option")'),
|
|
3543
|
+
modifiers: z.string().optional().describe('Modifier keys for click (e.g. "ctrl+shift")')
|
|
3544
|
+
})
|
|
3545
|
+
};
|
|
3364
3546
|
var findTool = {
|
|
3365
3547
|
name: "find",
|
|
3366
3548
|
description: FIND_DESCRIPTION,
|
|
@@ -4711,6 +4893,8 @@ var allTools = [
|
|
|
4711
4893
|
hoverTool,
|
|
4712
4894
|
dragTool,
|
|
4713
4895
|
readPageTool,
|
|
4896
|
+
indexedPageTool,
|
|
4897
|
+
indexedActionTool,
|
|
4714
4898
|
findTool,
|
|
4715
4899
|
inspectTool,
|
|
4716
4900
|
formInputTool,
|
|
@@ -4846,6 +5030,7 @@ function computeToolTimeout(tool, input) {
|
|
|
4846
5030
|
// src/server.ts
|
|
4847
5031
|
var pendingRequests = /* @__PURE__ */ new Map();
|
|
4848
5032
|
var extensionSocket = null;
|
|
5033
|
+
var bridgeUpdateInfo = null;
|
|
4849
5034
|
var configuredChromeProfile;
|
|
4850
5035
|
function coerceValue(v) {
|
|
4851
5036
|
if (typeof v !== "string") return v;
|
|
@@ -4867,7 +5052,7 @@ function coerceShape(shape) {
|
|
|
4867
5052
|
return result;
|
|
4868
5053
|
}
|
|
4869
5054
|
function handleFileExport(filePath, result) {
|
|
4870
|
-
|
|
5055
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
4871
5056
|
let content;
|
|
4872
5057
|
let metadata;
|
|
4873
5058
|
if (typeof result.data === "string") {
|
|
@@ -4931,6 +5116,14 @@ function createConfiguredMcpServer() {
|
|
|
4931
5116
|
} catch {
|
|
4932
5117
|
}
|
|
4933
5118
|
}
|
|
5119
|
+
if (bridgeUpdateInfo) {
|
|
5120
|
+
const info = bridgeUpdateInfo;
|
|
5121
|
+
bridgeUpdateInfo = null;
|
|
5122
|
+
result.content.unshift({
|
|
5123
|
+
type: "text",
|
|
5124
|
+
text: `[INFO] Bridge was restarted for version update (${info.from} \u2192 ${info.to}). Connection re-established.`
|
|
5125
|
+
});
|
|
5126
|
+
}
|
|
4934
5127
|
return result;
|
|
4935
5128
|
});
|
|
4936
5129
|
}
|
|
@@ -5221,7 +5414,7 @@ async function handleStreamableHttpRequest(req, res, sessions2) {
|
|
|
5221
5414
|
}
|
|
5222
5415
|
var MAX_BODY_SIZE = 4 * 1024 * 1024;
|
|
5223
5416
|
function parseJsonBody(req) {
|
|
5224
|
-
return new Promise((
|
|
5417
|
+
return new Promise((resolve3, reject) => {
|
|
5225
5418
|
const chunks = [];
|
|
5226
5419
|
let size = 0;
|
|
5227
5420
|
let destroyed = false;
|
|
@@ -5239,7 +5432,7 @@ function parseJsonBody(req) {
|
|
|
5239
5432
|
req.on("end", () => {
|
|
5240
5433
|
if (destroyed) return;
|
|
5241
5434
|
try {
|
|
5242
|
-
|
|
5435
|
+
resolve3(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
|
5243
5436
|
} catch (error) {
|
|
5244
5437
|
reject(new Error(`Invalid JSON: ${error.message}`));
|
|
5245
5438
|
}
|
|
@@ -5447,6 +5640,21 @@ function handleExtensionMessage(message) {
|
|
|
5447
5640
|
}
|
|
5448
5641
|
}
|
|
5449
5642
|
}
|
|
5643
|
+
const bridgeVersion = typeof msg.bridgeVersion === "string" ? msg.bridgeVersion : null;
|
|
5644
|
+
if (bridgeVersion && shouldUpdateBridge(bridgeVersion)) {
|
|
5645
|
+
const serverVersion = getPackageVersion();
|
|
5646
|
+
process.stderr.write(
|
|
5647
|
+
`[viyv-browser:mcp] Bridge version mismatch (bridge=${bridgeVersion}, server=${serverVersion}), requesting restart
|
|
5648
|
+
`
|
|
5649
|
+
);
|
|
5650
|
+
bridgeUpdateInfo = { from: bridgeVersion, to: serverVersion };
|
|
5651
|
+
if (extensionSocket && !extensionSocket.destroyed) {
|
|
5652
|
+
extensionSocket.write(
|
|
5653
|
+
`${JSON.stringify({ type: "bridge_shutdown_request", timestamp: Date.now() })}
|
|
5654
|
+
`
|
|
5655
|
+
);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5450
5658
|
} else if (type === "browser_event") {
|
|
5451
5659
|
process.stderr.write(`[viyv-browser:mcp] Browser event: ${String(msg.eventType)}
|
|
5452
5660
|
`);
|
|
@@ -5584,7 +5792,7 @@ async function callExtensionTool(tool, input) {
|
|
|
5584
5792
|
if (paths) {
|
|
5585
5793
|
for (const p of paths) {
|
|
5586
5794
|
try {
|
|
5587
|
-
if (!
|
|
5795
|
+
if (!existsSync2(p) || !statSync(p).isFile()) {
|
|
5588
5796
|
return {
|
|
5589
5797
|
content: [
|
|
5590
5798
|
{
|
|
@@ -5616,7 +5824,7 @@ async function callExtensionTool(tool, input) {
|
|
|
5616
5824
|
let csvData;
|
|
5617
5825
|
if (typeof input.file_path === "string") {
|
|
5618
5826
|
const fp = input.file_path;
|
|
5619
|
-
if (!
|
|
5827
|
+
if (!existsSync2(fp) || !statSync(fp).isFile()) {
|
|
5620
5828
|
return {
|
|
5621
5829
|
content: [
|
|
5622
5830
|
{
|
|
@@ -5674,13 +5882,13 @@ async function callExtensionTool(tool, input) {
|
|
|
5674
5882
|
touchSession(agentId);
|
|
5675
5883
|
const sock = extensionSocket;
|
|
5676
5884
|
const toolTimeout = computeToolTimeout(tool, input);
|
|
5677
|
-
return new Promise((
|
|
5885
|
+
return new Promise((resolve3) => {
|
|
5678
5886
|
const onError = () => {
|
|
5679
5887
|
const pending = pendingRequests.get(requestId);
|
|
5680
5888
|
if (pending) {
|
|
5681
5889
|
clearTimeout(pending.timer);
|
|
5682
5890
|
pendingRequests.delete(requestId);
|
|
5683
|
-
|
|
5891
|
+
resolve3({
|
|
5684
5892
|
content: [
|
|
5685
5893
|
{
|
|
5686
5894
|
type: "text",
|
|
@@ -5701,7 +5909,7 @@ async function callExtensionTool(tool, input) {
|
|
|
5701
5909
|
const timer = setTimeout(() => {
|
|
5702
5910
|
pendingRequests.delete(requestId);
|
|
5703
5911
|
removeErrorListener();
|
|
5704
|
-
|
|
5912
|
+
resolve3({
|
|
5705
5913
|
content: [
|
|
5706
5914
|
{
|
|
5707
5915
|
type: "text",
|
|
@@ -5726,12 +5934,12 @@ async function callExtensionTool(tool, input) {
|
|
|
5726
5934
|
if (FILE_EXPORT_TOOLS.has(tool) && typeof filePath === "string" && ("data" in result || "pages" in result)) {
|
|
5727
5935
|
try {
|
|
5728
5936
|
const metadata = handleFileExport(filePath, result);
|
|
5729
|
-
|
|
5937
|
+
resolve3({
|
|
5730
5938
|
content: [{ type: "text", text: JSON.stringify(metadata) }]
|
|
5731
5939
|
});
|
|
5732
5940
|
} catch (e) {
|
|
5733
5941
|
const msg = e instanceof Error ? e.message : String(e);
|
|
5734
|
-
|
|
5942
|
+
resolve3({
|
|
5735
5943
|
content: [
|
|
5736
5944
|
{
|
|
5737
5945
|
type: "text",
|
|
@@ -5750,9 +5958,9 @@ async function callExtensionTool(tool, input) {
|
|
|
5750
5958
|
if (RESULT_EXPORT_TOOLS.has(tool) && typeof filePath === "string") {
|
|
5751
5959
|
try {
|
|
5752
5960
|
const content = JSON.stringify(result, null, 2);
|
|
5753
|
-
|
|
5961
|
+
mkdirSync2(dirname(filePath), { recursive: true });
|
|
5754
5962
|
writeFileSync(filePath, content, "utf-8");
|
|
5755
|
-
|
|
5963
|
+
resolve3({
|
|
5756
5964
|
content: [
|
|
5757
5965
|
{
|
|
5758
5966
|
type: "text",
|
|
@@ -5765,7 +5973,7 @@ async function callExtensionTool(tool, input) {
|
|
|
5765
5973
|
});
|
|
5766
5974
|
} catch (e) {
|
|
5767
5975
|
const msg = e instanceof Error ? e.message : String(e);
|
|
5768
|
-
|
|
5976
|
+
resolve3({
|
|
5769
5977
|
content: [
|
|
5770
5978
|
{
|
|
5771
5979
|
type: "text",
|
|
@@ -5792,21 +6000,21 @@ async function callExtensionTool(tool, input) {
|
|
|
5792
6000
|
}
|
|
5793
6001
|
if (tempImportFile) {
|
|
5794
6002
|
try {
|
|
5795
|
-
|
|
6003
|
+
unlinkSync2(tempImportFile);
|
|
5796
6004
|
} catch {
|
|
5797
6005
|
}
|
|
5798
6006
|
}
|
|
5799
|
-
|
|
6007
|
+
resolve3({ content: buildResponseContent(tool, finalResult) });
|
|
5800
6008
|
},
|
|
5801
6009
|
reject: (error) => {
|
|
5802
6010
|
removeErrorListener();
|
|
5803
6011
|
if (tempImportFile) {
|
|
5804
6012
|
try {
|
|
5805
|
-
|
|
6013
|
+
unlinkSync2(tempImportFile);
|
|
5806
6014
|
} catch {
|
|
5807
6015
|
}
|
|
5808
6016
|
}
|
|
5809
|
-
|
|
6017
|
+
resolve3({
|
|
5810
6018
|
content: [
|
|
5811
6019
|
{
|
|
5812
6020
|
type: "text",
|
|
@@ -5858,12 +6066,12 @@ async function handleSwitchBrowser() {
|
|
|
5858
6066
|
setExtensionConnected(false);
|
|
5859
6067
|
}
|
|
5860
6068
|
process.stderr.write("[viyv-browser:mcp] switch_browser: waiting for bridge reconnection...\n");
|
|
5861
|
-
return new Promise((
|
|
6069
|
+
return new Promise((resolve3) => {
|
|
5862
6070
|
const checkInterval = setInterval(() => {
|
|
5863
6071
|
if (extensionSocket && !extensionSocket.destroyed && isExtensionConnected()) {
|
|
5864
6072
|
clearInterval(checkInterval);
|
|
5865
6073
|
clearTimeout(timer);
|
|
5866
|
-
|
|
6074
|
+
resolve3({
|
|
5867
6075
|
content: [
|
|
5868
6076
|
{
|
|
5869
6077
|
type: "text",
|
|
@@ -5878,7 +6086,7 @@ async function handleSwitchBrowser() {
|
|
|
5878
6086
|
}, 500);
|
|
5879
6087
|
const timer = setTimeout(() => {
|
|
5880
6088
|
clearInterval(checkInterval);
|
|
5881
|
-
|
|
6089
|
+
resolve3({
|
|
5882
6090
|
content: [
|
|
5883
6091
|
{
|
|
5884
6092
|
type: "text",
|
|
@@ -5897,27 +6105,24 @@ async function handleSwitchBrowser() {
|
|
|
5897
6105
|
|
|
5898
6106
|
// src/setup.ts
|
|
5899
6107
|
import { execSync } from "child_process";
|
|
5900
|
-
import { chmodSync, existsSync as
|
|
5901
|
-
import { homedir, platform } from "os";
|
|
5902
|
-
import { dirname as dirname2, resolve } from "path";
|
|
6108
|
+
import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6109
|
+
import { homedir as homedir2, platform } from "os";
|
|
6110
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
5903
6111
|
function runSetup(options = {}) {
|
|
5904
6112
|
const os = platform();
|
|
5905
|
-
const binaryPath = getBinaryPath();
|
|
5906
6113
|
console.log("Viyv Browser MCP - Native Messaging Host Setup");
|
|
5907
6114
|
console.log("================================================");
|
|
5908
6115
|
console.log(`Platform: ${os}`);
|
|
5909
|
-
console.log(`Binary: ${binaryPath}`);
|
|
5910
|
-
if (!existsSync2(binaryPath)) {
|
|
5911
|
-
console.error(`WARNING: Binary not found at ${binaryPath}`);
|
|
5912
|
-
console.error("The Native Messaging Host may not work until the binary is available.");
|
|
5913
|
-
}
|
|
5914
6116
|
const allowedOrigins = options.extensionId ? [`chrome-extension://${options.extensionId}/`] : ["chrome-extension://*/"];
|
|
5915
6117
|
if (!options.extensionId) {
|
|
5916
6118
|
process.stderr.write(
|
|
5917
6119
|
"WARNING: Using wildcard allowed_origins (chrome-extension://*/). This allows any Chrome extension to connect. For production, specify --extension-id to restrict access.\n"
|
|
5918
6120
|
);
|
|
5919
6121
|
}
|
|
5920
|
-
|
|
6122
|
+
syncNativeHostBinary();
|
|
6123
|
+
const nativeHostPath = getNativeHostBinaryPath();
|
|
6124
|
+
console.log(`Native host binary: ${nativeHostPath}`);
|
|
6125
|
+
const wrapperPath = createNativeHostWrapper(os, nativeHostPath);
|
|
5921
6126
|
const manifest = {
|
|
5922
6127
|
name: NATIVE_HOST_NAME,
|
|
5923
6128
|
description: "Viyv Browser MCP Native Messaging Host",
|
|
@@ -5929,9 +6134,9 @@ function runSetup(options = {}) {
|
|
|
5929
6134
|
const manifestDir = dirname2(manifestPath);
|
|
5930
6135
|
console.log(`Wrapper: ${wrapperPath}`);
|
|
5931
6136
|
console.log(`Manifest path: ${manifestPath}`);
|
|
5932
|
-
|
|
6137
|
+
mkdirSync3(manifestDir, { recursive: true });
|
|
5933
6138
|
writeFileSync2(manifestPath, JSON.stringify(manifest, null, 2));
|
|
5934
|
-
|
|
6139
|
+
chmodSync2(manifestPath, 420);
|
|
5935
6140
|
console.log("\nNative Messaging Host registered successfully!");
|
|
5936
6141
|
console.log("\nNext steps:");
|
|
5937
6142
|
console.log("1. Start the MCP Server: node <path>/dist/index.js");
|
|
@@ -5941,32 +6146,22 @@ function runSetup(options = {}) {
|
|
|
5941
6146
|
setupClaudeDesktopConfig();
|
|
5942
6147
|
}
|
|
5943
6148
|
}
|
|
5944
|
-
function getBinaryPath() {
|
|
5945
|
-
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
5946
|
-
try {
|
|
5947
|
-
const found = execSync(`${whichCmd} viyv-browser-mcp`, { encoding: "utf-8" }).trim();
|
|
5948
|
-
if (found) return found;
|
|
5949
|
-
} catch {
|
|
5950
|
-
}
|
|
5951
|
-
const currentScript = process.argv[1];
|
|
5952
|
-
return resolve(currentScript);
|
|
5953
|
-
}
|
|
5954
6149
|
function createNativeHostWrapper(os, binaryPath) {
|
|
5955
6150
|
const manifestDir = dirname2(getManifestPath(os));
|
|
5956
|
-
|
|
6151
|
+
mkdirSync3(manifestDir, { recursive: true });
|
|
5957
6152
|
const nodePath = getNodePath();
|
|
5958
6153
|
if (os === "win32") {
|
|
5959
|
-
const wrapperPath2 =
|
|
6154
|
+
const wrapperPath2 = resolve2(manifestDir, `${NATIVE_HOST_NAME}.bat`);
|
|
5960
6155
|
writeFileSync2(wrapperPath2, `@echo off\r
|
|
5961
6156
|
"${nodePath}" "${binaryPath}" --native-host\r
|
|
5962
6157
|
`);
|
|
5963
6158
|
return wrapperPath2;
|
|
5964
6159
|
}
|
|
5965
|
-
const wrapperPath =
|
|
6160
|
+
const wrapperPath = resolve2(manifestDir, `${NATIVE_HOST_NAME}.sh`);
|
|
5966
6161
|
writeFileSync2(wrapperPath, `#!/bin/bash
|
|
5967
6162
|
exec "${nodePath}" "${binaryPath}" --native-host
|
|
5968
6163
|
`);
|
|
5969
|
-
|
|
6164
|
+
chmodSync2(wrapperPath, 493);
|
|
5970
6165
|
return wrapperPath;
|
|
5971
6166
|
}
|
|
5972
6167
|
function getNodePath() {
|
|
@@ -5977,18 +6172,18 @@ function getNodePath() {
|
|
|
5977
6172
|
}
|
|
5978
6173
|
}
|
|
5979
6174
|
function getManifestPath(os) {
|
|
5980
|
-
const home =
|
|
6175
|
+
const home = homedir2();
|
|
5981
6176
|
switch (os) {
|
|
5982
6177
|
case "darwin":
|
|
5983
|
-
return
|
|
6178
|
+
return resolve2(
|
|
5984
6179
|
home,
|
|
5985
6180
|
"Library/Application Support/Google/Chrome/NativeMessagingHosts",
|
|
5986
6181
|
`${NATIVE_HOST_NAME}.json`
|
|
5987
6182
|
);
|
|
5988
6183
|
case "linux":
|
|
5989
|
-
return
|
|
6184
|
+
return resolve2(home, ".config/google-chrome/NativeMessagingHosts", `${NATIVE_HOST_NAME}.json`);
|
|
5990
6185
|
case "win32":
|
|
5991
|
-
return
|
|
6186
|
+
return resolve2(
|
|
5992
6187
|
home,
|
|
5993
6188
|
"AppData/Local/Google/Chrome/User Data/NativeMessagingHosts",
|
|
5994
6189
|
`${NATIVE_HOST_NAME}.json`
|
|
@@ -6008,7 +6203,7 @@ function setupClaudeDesktopConfig() {
|
|
|
6008
6203
|
const configPath = getClaudeDesktopConfigPath();
|
|
6009
6204
|
console.log(`Config path: ${configPath}`);
|
|
6010
6205
|
let config = {};
|
|
6011
|
-
if (
|
|
6206
|
+
if (existsSync3(configPath)) {
|
|
6012
6207
|
try {
|
|
6013
6208
|
config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
6014
6209
|
} catch {
|
|
@@ -6030,7 +6225,7 @@ function setupClaudeDesktopConfig() {
|
|
|
6030
6225
|
env: { PATH: envPath }
|
|
6031
6226
|
};
|
|
6032
6227
|
const configDir = dirname2(configPath);
|
|
6033
|
-
|
|
6228
|
+
mkdirSync3(configDir, { recursive: true });
|
|
6034
6229
|
writeFileSync2(configPath, JSON.stringify(config, null, 2));
|
|
6035
6230
|
console.log(`npx path: ${npxPath}`);
|
|
6036
6231
|
console.log("\nClaude Desktop config updated successfully!");
|
|
@@ -6045,16 +6240,16 @@ function getNpxPath() {
|
|
|
6045
6240
|
}
|
|
6046
6241
|
}
|
|
6047
6242
|
function getClaudeDesktopConfigPath() {
|
|
6048
|
-
const home =
|
|
6243
|
+
const home = homedir2();
|
|
6049
6244
|
switch (process.platform) {
|
|
6050
6245
|
case "darwin":
|
|
6051
|
-
return
|
|
6246
|
+
return resolve2(home, "Library/Application Support/Claude/claude_desktop_config.json");
|
|
6052
6247
|
case "linux":
|
|
6053
|
-
return
|
|
6248
|
+
return resolve2(home, ".config/Claude/claude_desktop_config.json");
|
|
6054
6249
|
case "win32":
|
|
6055
|
-
return
|
|
6250
|
+
return resolve2(home, "AppData/Roaming/Claude/claude_desktop_config.json");
|
|
6056
6251
|
default:
|
|
6057
|
-
return
|
|
6252
|
+
return resolve2(home, ".config/Claude/claude_desktop_config.json");
|
|
6058
6253
|
}
|
|
6059
6254
|
}
|
|
6060
6255
|
|
|
@@ -6089,6 +6284,16 @@ if (args.includes("setup")) {
|
|
|
6089
6284
|
const port = portIdx >= 0 ? Number(args[portIdx + 1]) : void 0;
|
|
6090
6285
|
const chromeProfileIdx = args.indexOf("--chrome-profile");
|
|
6091
6286
|
const chromeProfile = chromeProfileIdx >= 0 ? args[chromeProfileIdx + 1] : process.env.VIYV_CHROME_PROFILE;
|
|
6287
|
+
if (isNativeHostDeployed()) {
|
|
6288
|
+
try {
|
|
6289
|
+
syncNativeHostBinary();
|
|
6290
|
+
} catch (err) {
|
|
6291
|
+
process.stderr.write(
|
|
6292
|
+
`[viyv-browser:mcp] WARNING: Failed to update native host binary: ${err.message}
|
|
6293
|
+
`
|
|
6294
|
+
);
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6092
6297
|
startMcpServer(agentName, { transport: transportMode, port, chromeProfile });
|
|
6093
6298
|
}
|
|
6094
6299
|
//# sourceMappingURL=index.js.map
|