mcp-fs-shell-windows 0.2.19 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +2 -1
  2. package/README.md +352 -190
  3. package/dist/analyze_project/handler.js +62 -0
  4. package/dist/analyze_project/schema.js +5 -0
  5. package/dist/browser/browserActions.js +128 -0
  6. package/dist/browser/fuzzySearch.js +49 -0
  7. package/dist/browser/handler.js +227 -0
  8. package/dist/browser/launcher.js +103 -0
  9. package/dist/browser/schema.js +37 -0
  10. package/dist/browser/session.js +14 -0
  11. package/dist/compat/handler.js +254 -0
  12. package/dist/compat/schema.js +97 -0
  13. package/dist/gh/handler.js +406 -0
  14. package/dist/gh/schema.js +36 -0
  15. package/dist/git/handler.js +209 -0
  16. package/dist/git/schema.js +23 -0
  17. package/dist/launch_file/handler.js +3 -1
  18. package/dist/query_database/handler.js +27 -0
  19. package/dist/query_database/schema.js +5 -0
  20. package/dist/rag/handler.js +103 -0
  21. package/dist/rag/helpers.js +126 -0
  22. package/dist/rag/schema.js +16 -0
  23. package/dist/read_document/handler.js +61 -0
  24. package/dist/read_document/schema.js +4 -0
  25. package/dist/run_javascript/handler.js +124 -0
  26. package/dist/run_javascript/schema.js +28 -0
  27. package/dist/server.js +885 -1
  28. package/dist/shell/handler.js +14 -6
  29. package/dist/subagent/handler.js +752 -0
  30. package/dist/subagent/handoffMessage.js +56 -0
  31. package/dist/subagent/schema.js +18 -0
  32. package/dist/subagent/subAgentToolCallParser.js +491 -0
  33. package/dist/subagent/toolCallValidator.js +97 -0
  34. package/dist/system/handler.js +239 -0
  35. package/dist/system/schema.js +21 -0
  36. package/dist/web/ddgParse.js +51 -0
  37. package/dist/web/handler.js +286 -0
  38. package/dist/web/schema.js +18 -0
  39. package/package.json +22 -2
@@ -0,0 +1,62 @@
1
+ // analyze_project/handler.ts — project-wide linting (MCP filesystem fork).
2
+ //
3
+ // Port of the Beledarian analyze_project tool (beledarians-lm-studio-tools
4
+ // src/toolsProvider.ts). Detection ladder, exactly as the reference:
5
+ // 1. package.json with scripts.lint -> npm run lint (npm-script)
6
+ // 2. else eslint in (dev)dependencies -> npx eslint . --format json (eslint)
7
+ // 3. else (only when package.json is absent/unreadable) any .py file -> pylint . (python-lint)
8
+ // The working directory is the shell_cwd default CWD (fork equivalent of the
9
+ // reference's currentWorkingDirectory).
10
+ import { readFile, readdir } from "fs/promises";
11
+ import { join } from "path";
12
+ import { spawn } from "child_process";
13
+ import { getDefaultShellCwd } from "../shell/handler.js";
14
+ export async function handleAnalyzeProject() {
15
+ // Try to detect available linters
16
+ const cwd = getDefaultShellCwd();
17
+ const packageJsonPath = join(cwd, "package.json");
18
+ let command = "";
19
+ let type = "unknown";
20
+ try {
21
+ const pkg = JSON.parse(await readFile(packageJsonPath, "utf-8"));
22
+ if (pkg.scripts && pkg.scripts.lint) {
23
+ command = "npm run lint";
24
+ type = "npm-script";
25
+ }
26
+ else if (pkg.devDependencies?.eslint || pkg.dependencies?.eslint) {
27
+ command = "npx eslint . --format json"; // JSON for easier parsing? Or just text.
28
+ type = "eslint";
29
+ }
30
+ }
31
+ catch (e) {
32
+ // check for python?
33
+ const entries = await readdir(cwd);
34
+ if (entries.some(f => f.endsWith(".py"))) {
35
+ command = "pylint ."; // Assuming pylint is in path
36
+ type = "python-lint";
37
+ }
38
+ }
39
+ if (!command) {
40
+ return JSON.stringify({ error: "Could not detect a supported linter (ESLint script or Python)." });
41
+ }
42
+ try {
43
+ const child = spawn(command, {
44
+ shell: true,
45
+ cwd,
46
+ timeout: 60000
47
+ });
48
+ let stdout = "";
49
+ let stderr = "";
50
+ child.stdout.on("data", d => stdout += d);
51
+ child.stderr.on("data", d => stderr += d);
52
+ await new Promise((resolve) => child.on("close", () => resolve()));
53
+ return JSON.stringify({
54
+ tool: command,
55
+ type,
56
+ report: (stdout + stderr).substring(0, 10000) // Limit size
57
+ });
58
+ }
59
+ catch (e) {
60
+ return JSON.stringify({ error: `Analysis failed: ${e instanceof Error ? e.message : String(e)}` });
61
+ }
62
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ // analyze_project takes no parameters: it operates on the current working
3
+ // directory (the shell_cwd default CWD — the fork's equivalent of the
4
+ // reference's currentWorkingDirectory).
5
+ export const AnalyzeProjectArgsSchema = z.object({});
@@ -0,0 +1,128 @@
1
+ export async function executeBrowserActions(page, actions) {
2
+ const actionLog = [];
3
+ const clickRetryDelayMs = 300;
4
+ const safeClick = async (selector, clickCount) => {
5
+ await page.waitForSelector(selector, { timeout: 15000 });
6
+ const performClick = async () => {
7
+ if (clickCount) {
8
+ await page.click(selector, { clickCount });
9
+ }
10
+ else {
11
+ await page.click(selector);
12
+ }
13
+ };
14
+ try {
15
+ await performClick();
16
+ return;
17
+ }
18
+ catch {
19
+ try {
20
+ await new Promise(resolve => setTimeout(resolve, clickRetryDelayMs));
21
+ await performClick();
22
+ return;
23
+ }
24
+ catch {
25
+ // Continue to DOM fallback.
26
+ }
27
+ const fallbackResult = await page.evaluate(({ targetSelector, targetClickCount }) => {
28
+ const target = document.querySelector(targetSelector);
29
+ if (!(target instanceof HTMLElement)) {
30
+ return { ok: false, reason: "not-an-element" };
31
+ }
32
+ target.scrollIntoView({ behavior: "auto", block: "center", inline: "center" });
33
+ const rect = target.getBoundingClientRect();
34
+ if (rect.width <= 0 || rect.height <= 0) {
35
+ return { ok: false, reason: "not-clickable" };
36
+ }
37
+ if (targetClickCount && targetClickCount >= 3 && (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) {
38
+ target.select();
39
+ }
40
+ target.click();
41
+ return { ok: true };
42
+ }, { targetSelector: selector, targetClickCount: clickCount ?? 1 });
43
+ if (!fallbackResult?.ok) {
44
+ const reason = fallbackResult?.reason || "unknown";
45
+ throw new Error(`Action 'click' failed for selector '${selector}' (${reason}).`);
46
+ }
47
+ }
48
+ };
49
+ for (const action of actions) {
50
+ if (action.type === "wait_for_selector") {
51
+ if (!action.selector)
52
+ throw new Error("Action 'wait_for_selector' requires 'selector'.");
53
+ await page.waitForSelector(action.selector, { timeout: 15000 });
54
+ actionLog.push(`wait_for_selector:${action.selector}`);
55
+ }
56
+ else if (action.type === "wait") {
57
+ const ms = action.milliseconds ?? 500;
58
+ await new Promise(resolve => setTimeout(resolve, ms));
59
+ actionLog.push(`wait:${ms}`);
60
+ }
61
+ else if (action.type === "click") {
62
+ if (!action.selector)
63
+ throw new Error("Action 'click' requires 'selector'.");
64
+ await safeClick(action.selector);
65
+ actionLog.push(`click:${action.selector}`);
66
+ }
67
+ else if (action.type === "type") {
68
+ if (!action.selector)
69
+ throw new Error("Action 'type' requires 'selector'.");
70
+ if (typeof action.text !== "string")
71
+ throw new Error("Action 'type' requires 'text'.");
72
+ await safeClick(action.selector, 3);
73
+ await page.keyboard.press("Backspace");
74
+ await page.type(action.selector, action.text, { delay: 20 });
75
+ actionLog.push(`type:${action.selector}`);
76
+ }
77
+ else if (action.type === "press") {
78
+ if (!action.key)
79
+ throw new Error("Action 'press' requires 'key'.");
80
+ await page.keyboard.press(action.key);
81
+ actionLog.push(`press:${action.key}`);
82
+ }
83
+ else if (action.type === "select") {
84
+ if (!action.selector)
85
+ throw new Error("Action 'select' requires 'selector'.");
86
+ if (typeof action.value !== "string")
87
+ throw new Error("Action 'select' requires 'value'.");
88
+ const selected = await page.select(action.selector, action.value);
89
+ if (selected.length === 0)
90
+ throw new Error(`Action 'select' found no matching option for '${action.value}'.`);
91
+ actionLog.push(`select:${action.selector}`);
92
+ }
93
+ else if (action.type === "hover") {
94
+ if (!action.selector)
95
+ throw new Error("Action 'hover' requires 'selector'.");
96
+ await page.hover(action.selector);
97
+ actionLog.push(`hover:${action.selector}`);
98
+ }
99
+ else if (action.type === "scroll") {
100
+ if (action.selector) {
101
+ await page.evaluate((selector) => {
102
+ const target = document.querySelector(selector);
103
+ if (!target)
104
+ throw new Error(`Selector not found: ${selector}`);
105
+ target.scrollIntoView({ behavior: "auto", block: "center", inline: "nearest" });
106
+ }, action.selector);
107
+ actionLog.push(`scroll_into_view:${action.selector}`);
108
+ }
109
+ else {
110
+ const x = action.x ?? 0;
111
+ const y = action.y ?? 600;
112
+ await page.evaluate(({ dx, dy }) => { window.scrollBy(dx, dy); }, { dx: x, dy: y });
113
+ actionLog.push(`scroll:${x},${y}`);
114
+ }
115
+ }
116
+ else if (action.type === "evaluate") {
117
+ if (!action.script)
118
+ throw new Error("Action 'evaluate' requires 'script'.");
119
+ await page.evaluate((script) => {
120
+ // Intentionally executes custom page-side code for advanced automation.
121
+ // eslint-disable-next-line no-new-func
122
+ return Function(`"use strict";\n${script}`)();
123
+ }, action.script);
124
+ actionLog.push("evaluate");
125
+ }
126
+ }
127
+ return actionLog;
128
+ }
@@ -0,0 +1,49 @@
1
+ function normalize(input) {
2
+ return input.toLowerCase().replace(/\s+/g, " ").trim();
3
+ }
4
+ export function levenshteinDistance(a, b) {
5
+ const left = normalize(a);
6
+ const right = normalize(b);
7
+ const m = left.length;
8
+ const n = right.length;
9
+ if (m === 0)
10
+ return n;
11
+ if (n === 0)
12
+ return m;
13
+ const prev = new Array(n + 1);
14
+ const curr = new Array(n + 1);
15
+ for (let j = 0; j <= n; j++)
16
+ prev[j] = j;
17
+ for (let i = 1; i <= m; i++) {
18
+ curr[0] = i;
19
+ for (let j = 1; j <= n; j++) {
20
+ const cost = left[i - 1] === right[j - 1] ? 0 : 1;
21
+ curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
22
+ }
23
+ for (let j = 0; j <= n; j++)
24
+ prev[j] = curr[j];
25
+ }
26
+ return prev[n];
27
+ }
28
+ export function computeFuzzyScore(query, candidate) {
29
+ const q = normalize(query);
30
+ const c = normalize(candidate);
31
+ if (!q || !c)
32
+ return 0;
33
+ if (q === c)
34
+ return 1;
35
+ if (c.includes(q)) {
36
+ const coverage = q.length / c.length;
37
+ return Math.min(1, 0.85 + coverage * 0.15);
38
+ }
39
+ const distance = levenshteinDistance(q, c);
40
+ const maxLen = Math.max(q.length, c.length);
41
+ return Math.max(0, 1 - distance / maxLen);
42
+ }
43
+ export function rankFuzzyMatches(query, candidates, limit = 5) {
44
+ const ranked = candidates
45
+ .map(value => ({ value, score: computeFuzzyScore(query, value) }))
46
+ .sort((a, b) => b.score - a.score || a.value.length - b.value.length)
47
+ .slice(0, limit);
48
+ return ranked;
49
+ }
@@ -0,0 +1,227 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { launchBrowser, closeBrowserQuiet } from "./launcher.js";
3
+ import { closeSession, getSession, setSession } from "./session.js";
4
+ import { executeBrowserActions } from "./browserActions.js";
5
+ import { rankFuzzyMatches } from "./fuzzySearch.js";
6
+ // In-page candidate collection for fuzzy find (ported from the Beledarian
7
+ // reference): interactive + text nodes, deduped on (text||selector), capped
8
+ // at 400 candidates. Runs inside the page context.
9
+ const collectFuzzyCandidates = (page) => page.evaluate(() => {
10
+ const dedup = new Map();
11
+ const nodes = document.querySelectorAll("a,button,input,textarea,select,[role='button'],[aria-label],h1,h2,h3,h4,h5,h6,p,span");
12
+ const clean = (value) => value.replace(/\s+/g, " ").trim();
13
+ const classSelector = (el) => {
14
+ const classes = Array.from(el.classList).slice(0, 2).map(c => c.replace(/[^a-zA-Z0-9_-]/g, ""));
15
+ return classes.length > 0 ? `.${classes.join(".")}` : "";
16
+ };
17
+ const buildSelector = (el) => {
18
+ if (el.id)
19
+ return `#${el.id}`;
20
+ const name = el.getAttribute("name");
21
+ if (name)
22
+ return `${el.tagName.toLowerCase()}[name="${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
23
+ return `${el.tagName.toLowerCase()}${classSelector(el)}`;
24
+ };
25
+ for (const node of nodes) {
26
+ const element = node;
27
+ const text = clean(element.innerText ||
28
+ element.value ||
29
+ element.getAttribute("aria-label") ||
30
+ "");
31
+ if (!text)
32
+ continue;
33
+ const selector = buildSelector(element);
34
+ const key = `${text}||${selector}`;
35
+ if (!dedup.has(key)) {
36
+ dedup.set(key, { text: text.substring(0, 200), selector });
37
+ }
38
+ if (dedup.size >= 400)
39
+ break;
40
+ }
41
+ return Array.from(dedup.values());
42
+ });
43
+ // browser_session_open: launch (replacing any existing session), navigate,
44
+ // optionally wait for a selector, and optionally return full page text.
45
+ export async function handleBrowserSessionOpen(url, waitForSelector, includePageText) {
46
+ try {
47
+ if (getSession()) {
48
+ await closeSession();
49
+ }
50
+ const browser = await launchBrowser();
51
+ const page = await browser.newPage();
52
+ try {
53
+ await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
54
+ if (waitForSelector) {
55
+ await page.waitForSelector(waitForSelector, { timeout: 15000 });
56
+ }
57
+ }
58
+ catch (error) {
59
+ await closeBrowserQuiet(browser);
60
+ throw error;
61
+ }
62
+ setSession({ browser, page, currentUrl: page.url() });
63
+ const pageText = includePageText !== false
64
+ ? await page.evaluate(() => document.body.innerText || "")
65
+ : undefined;
66
+ return JSON.stringify({
67
+ success: true,
68
+ session_active: true,
69
+ url: page.url(),
70
+ title: await page.title(),
71
+ text_content: pageText,
72
+ text_length: pageText ? pageText.length : 0,
73
+ message: "Browser session opened.",
74
+ });
75
+ }
76
+ catch (error) {
77
+ return JSON.stringify({
78
+ error: `Failed to open browser session: ${error instanceof Error ? error.message : String(error)}`,
79
+ });
80
+ }
81
+ }
82
+ // browser_session_control: run actions on the active page, optionally capture
83
+ // a screenshot, return page metadata (full text only on URL change or with
84
+ // full_read), and optionally fuzzy-find in-page text/selectors.
85
+ export async function handleBrowserSessionControl(args, allowedDirectories) {
86
+ const session = getSession();
87
+ if (!session) {
88
+ return JSON.stringify({
89
+ error: "No active browser session. Call 'browser_session_open' first.",
90
+ });
91
+ }
92
+ try {
93
+ const beforeUrl = session.page.url();
94
+ const actionLog = await executeBrowserActions(session.page, args.actions || []);
95
+ const afterUrl = session.page.url();
96
+ const urlChanged = beforeUrl !== afterUrl;
97
+ session.currentUrl = afterUrl;
98
+ let screenshotSaved = false;
99
+ if (args.screenshot_path) {
100
+ const screenshotFilePath = await validatePath(args.screenshot_path, allowedDirectories);
101
+ await session.page.screenshot({ path: screenshotFilePath, fullPage: args.full_page_screenshot ?? false });
102
+ screenshotSaved = true;
103
+ }
104
+ let pageSnapshot = undefined;
105
+ if (args.read_page !== false) {
106
+ const title = await session.page.title();
107
+ if (urlChanged || args.full_read) {
108
+ const textContent = await session.page.evaluate(() => document.body.innerText || "");
109
+ pageSnapshot = {
110
+ url: afterUrl,
111
+ title,
112
+ text_content: textContent,
113
+ text_length: textContent.length,
114
+ };
115
+ }
116
+ else {
117
+ pageSnapshot = {
118
+ url: afterUrl,
119
+ title,
120
+ note: "Full page text omitted (URL unchanged). Set full_read=true to force full output.",
121
+ };
122
+ }
123
+ }
124
+ let fuzzyResults = [];
125
+ if (typeof args.fuzzy_find === "string" && args.fuzzy_find.trim()) {
126
+ const query = args.fuzzy_find;
127
+ const candidates = await collectFuzzyCandidates(session.page);
128
+ const ranked = candidates
129
+ .map(candidate => ({
130
+ ...candidate,
131
+ score: Math.max(rankFuzzyMatches(query, [candidate.text], 1)[0]?.score ?? 0, rankFuzzyMatches(query, [candidate.selector], 1)[0]?.score ?? 0),
132
+ }))
133
+ .sort((a, b) => b.score - a.score)
134
+ .slice(0, args.max_results ?? 5);
135
+ fuzzyResults = ranked.map(item => ({
136
+ text: item.text,
137
+ selector: item.selector,
138
+ score: item.score,
139
+ }));
140
+ }
141
+ return JSON.stringify({
142
+ success: true,
143
+ session_active: true,
144
+ actions_executed: actionLog,
145
+ screenshot_saved: screenshotSaved,
146
+ url_changed: urlChanged,
147
+ url_change_notice: urlChanged ? `Url changed to -> [${afterUrl}]` : undefined,
148
+ page: pageSnapshot,
149
+ fuzzy_find_results: fuzzyResults,
150
+ });
151
+ }
152
+ catch (error) {
153
+ return JSON.stringify({
154
+ error: `Browser session control failed: ${error instanceof Error ? error.message : String(error)}`,
155
+ });
156
+ }
157
+ }
158
+ // browser_session_close: tear down the active session (idempotent).
159
+ export async function handleBrowserSessionClose() {
160
+ if (!getSession()) {
161
+ return JSON.stringify({
162
+ success: true,
163
+ session_active: false,
164
+ message: "No active browser session.",
165
+ });
166
+ }
167
+ try {
168
+ await closeSession();
169
+ return JSON.stringify({
170
+ success: true,
171
+ session_active: false,
172
+ message: "Browser session closed.",
173
+ });
174
+ }
175
+ catch (error) {
176
+ return JSON.stringify({
177
+ error: `Failed to close browser session: ${error instanceof Error ? error.message : String(error)}`,
178
+ });
179
+ }
180
+ }
181
+ // browser_open_page: stateless one-shot render (own browser instance, always
182
+ // closed on return; never touches the persistent session).
183
+ export async function handleBrowserOpenPage(args, allowedDirectories) {
184
+ let browser;
185
+ try {
186
+ browser = await launchBrowser();
187
+ const page = await browser.newPage();
188
+ try {
189
+ await page.goto(args.url, { waitUntil: "networkidle0", timeout: 30000 });
190
+ if (args.wait_for_selector) {
191
+ await page.waitForSelector(args.wait_for_selector, { timeout: 10000 });
192
+ }
193
+ const beforeActionUrl = page.url();
194
+ const actionLog = await executeBrowserActions(page, args.actions || []);
195
+ const currentUrl = page.url();
196
+ const urlChanged = currentUrl !== beforeActionUrl;
197
+ const title = await page.title();
198
+ const textContent = await page.evaluate(() => document.body.innerText || "");
199
+ let screenshotSaved = false;
200
+ if (args.screenshot_path) {
201
+ const screenshotFilePath = await validatePath(args.screenshot_path, allowedDirectories);
202
+ await page.screenshot({ path: screenshotFilePath, fullPage: args.full_page_screenshot ?? false });
203
+ screenshotSaved = true;
204
+ }
205
+ return JSON.stringify({
206
+ url: currentUrl,
207
+ title,
208
+ text_content: textContent.substring(0, 5000),
209
+ screenshot_saved: screenshotSaved,
210
+ actions_executed: actionLog,
211
+ url_changed: urlChanged,
212
+ url_change_notice: urlChanged ? `Url changed to -> [${currentUrl}]` : undefined,
213
+ });
214
+ }
215
+ finally {
216
+ await closeBrowserQuiet(browser);
217
+ }
218
+ }
219
+ catch (error) {
220
+ if (browser) {
221
+ await closeBrowserQuiet(browser);
222
+ }
223
+ return JSON.stringify({
224
+ error: `Browser operation failed: ${error instanceof Error ? error.message : String(error)}`,
225
+ });
226
+ }
227
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "fs";
2
+ import nodePath from "path";
3
+ import os from "os";
4
+ // Chrome-for-Testing build 142.0.7444.175, preinstalled at
5
+ // C:\Users\Gerar\.cache\puppeteer (installed for the Beledarian browser tools;
6
+ // pairs with puppeteer 24.31). puppeteer-core is a pure-JS dependency (no
7
+ // browser download); ALL browser launches in this repo go through this single
8
+ // module so web_search's browser legs (duckduckgo-html / google / bing) and
9
+ // the browser_session_* / browser_open_page tools share one launcher:
10
+ //
11
+ // * getSharedBrowser / closeSharedBrowser - the web_search shared browser
12
+ // (module-level; one search call reuses a single instance across legs and
13
+ // each leg closes it afterwards).
14
+ // * launchBrowser - a fresh standalone instance (used by the persistent
15
+ // browser session and the stateless browser_open_page one-shot).
16
+ //
17
+ // The persistent session (browser/session.ts) deliberately holds its OWN
18
+ // browser instance so web_search legs can never close it mid-session.
19
+ //
20
+ // Executable resolution (v0.2.29 publish portability):
21
+ // 1. env MCP_CHROME_PATH (explicit; must exist)
22
+ // 2. the standard puppeteer cache <home>/.cache/puppeteer/chrome/<build>/,
23
+ // highest version wins (e.g. win64-142.0.7444.175/chrome-win64/chrome.exe)
24
+ // 3. the legacy hardcoded path (the reference machine's preinstall)
25
+ export const CHROME_PATH = "C:\\Users\\Gerar\\.cache\\puppeteer\\chrome\\win64-142.0.7444.175\\chrome-win64\\chrome.exe";
26
+ // Compare two "win64-<a.b.c.d>" build dirs numerically (ascending).
27
+ function compareBuildDirs(a, b) {
28
+ const pa = a.replace(/^win64-/, "").split(".").map((n) => parseInt(n, 10) || 0);
29
+ const pb = b.replace(/^win64-/, "").split(".").map((n) => parseInt(n, 10) || 0);
30
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
31
+ const d = (pb[i] || 0) - (pa[i] || 0);
32
+ if (d !== 0)
33
+ return d;
34
+ }
35
+ return 0;
36
+ }
37
+ // Resolve the Chrome-for-Testing executable (null when nothing is found).
38
+ export function resolveChromePath() {
39
+ const env = process.env.MCP_CHROME_PATH;
40
+ if (env && fs.existsSync(env))
41
+ return env;
42
+ const base = nodePath.join(os.homedir(), ".cache", "puppeteer", "chrome");
43
+ if (fs.existsSync(base)) {
44
+ const builds = fs
45
+ .readdirSync(base, { withFileTypes: true })
46
+ .filter((e) => e.isDirectory() && e.name.startsWith("win64-"))
47
+ .map((e) => e.name)
48
+ .sort(compareBuildDirs);
49
+ for (let i = builds.length - 1; i >= 0; i--) {
50
+ const p = nodePath.join(base, builds[i], "chrome-win64", "chrome.exe");
51
+ if (fs.existsSync(p))
52
+ return p;
53
+ }
54
+ }
55
+ if (fs.existsSync(CHROME_PATH))
56
+ return CHROME_PATH;
57
+ return null;
58
+ }
59
+ const LAUNCH_ARGS = ["--no-sandbox", "--disable-setuid-sandbox"];
60
+ // Launch a fresh headless browser with the standard options.
61
+ export const launchBrowser = async () => {
62
+ const puppeteer = await import("puppeteer-core");
63
+ const chromePath = resolveChromePath();
64
+ if (!chromePath) {
65
+ throw new Error("No Chrome-for-Testing executable found. Set MCP_CHROME_PATH to the path of chrome.exe, " +
66
+ `or install a build under ${nodePath.join(os.homedir(), ".cache", "puppeteer", "chrome")} ` +
67
+ "(standard layout: win64-<version>/chrome-win64/chrome.exe).");
68
+ }
69
+ return puppeteer.launch({
70
+ headless: true,
71
+ executablePath: chromePath,
72
+ args: LAUNCH_ARGS,
73
+ });
74
+ };
75
+ // Close a browser, swallowing already-closed errors.
76
+ export const closeBrowserQuiet = async (browser) => {
77
+ try {
78
+ await browser.close();
79
+ }
80
+ catch {
81
+ // already closed
82
+ }
83
+ };
84
+ let sharedBrowser = null;
85
+ // Shared browser used by the web_search browser legs. Lazily launched so a
86
+ // missing browser never breaks the non-puppeteer legs (errors surface per
87
+ // provider and are collected, not fatal).
88
+ export const getSharedBrowser = async () => {
89
+ if (!sharedBrowser) {
90
+ sharedBrowser = await launchBrowser();
91
+ }
92
+ return sharedBrowser;
93
+ };
94
+ // Close and drop the shared browser so a later provider can relaunch cleanly
95
+ // (the reference kept a stale closed instance; nulling here is strictly safer
96
+ // for manual multi-provider mode).
97
+ export const closeSharedBrowser = async () => {
98
+ if (sharedBrowser) {
99
+ const browser = sharedBrowser;
100
+ sharedBrowser = null;
101
+ await closeBrowserQuiet(browser);
102
+ }
103
+ };
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ // Shared actions schema: scripted browser steps executed in order on a page.
3
+ // (Ported from the Beledarian reference; per-action required fields are
4
+ // enforced at execution time by executeBrowserActions with clear errors.)
5
+ export const BrowserActionSchema = z.object({
6
+ type: z.enum(["wait_for_selector", "wait", "click", "type", "press", "select", "hover", "scroll", "evaluate"]),
7
+ selector: z.string().optional().describe("CSS selector used by selector-based actions."),
8
+ text: z.string().optional().describe("Text payload for type action."),
9
+ value: z.string().optional().describe("Value payload for select action."),
10
+ key: z.string().optional().describe("Keyboard key for press action (e.g., Enter, Tab)."),
11
+ milliseconds: z.number().int().min(0).max(30000).optional().describe("Delay in milliseconds for wait action."),
12
+ x: z.number().optional().describe("Horizontal scroll delta for scroll action."),
13
+ y: z.number().optional().describe("Vertical scroll delta for scroll action."),
14
+ script: z.string().optional().describe("JavaScript snippet for evaluate action (executed in page context)."),
15
+ });
16
+ export const BrowserSessionOpenArgsSchema = z.object({
17
+ url: z.string(),
18
+ wait_for_selector: z.string().optional().describe("Optional selector to wait for after navigation."),
19
+ include_page_text: z.boolean().optional().describe("If true (default), returns full page text content after opening."),
20
+ });
21
+ export const BrowserSessionControlArgsSchema = z.object({
22
+ actions: z.array(BrowserActionSchema).optional().describe("Optional scripted browser actions to execute on the active page."),
23
+ read_page: z.boolean().optional().describe("If true (default), returns page metadata. Full text is returned only on URL change or when full_read=true."),
24
+ full_read: z.boolean().optional().describe("If true, forces full page text output even when URL has not changed."),
25
+ screenshot_path: z.string().optional().describe("Optional screenshot output path (must be within an allowed directory)."),
26
+ full_page_screenshot: z.boolean().optional().describe("If true, captures full page screenshot."),
27
+ fuzzy_find: z.string().optional().describe("Optional fuzzy-find query for in-page content/selectors."),
28
+ max_results: z.number().int().min(1).max(20).optional().describe("Max fuzzy results to return (default: 5)."),
29
+ });
30
+ export const BrowserSessionCloseArgsSchema = z.object({});
31
+ export const BrowserOpenPageArgsSchema = z.object({
32
+ url: z.string(),
33
+ screenshot_path: z.string().optional().describe("Path to save a screenshot (e.g., 'screenshot.png'); must be within an allowed directory."),
34
+ wait_for_selector: z.string().optional().describe("CSS selector to wait for before returning."),
35
+ full_page_screenshot: z.boolean().optional().describe("If true, captures the full page when taking a screenshot."),
36
+ actions: z.array(BrowserActionSchema).optional().describe("Optional scripted browser steps to run after navigation."),
37
+ });
@@ -0,0 +1,14 @@
1
+ import { closeBrowserQuiet } from "./launcher.js";
2
+ let session = null;
3
+ export const getSession = () => session;
4
+ export const setSession = (s) => {
5
+ session = s;
6
+ };
7
+ // Close the active session's browser and drop the state. No-op when none.
8
+ export const closeSession = async () => {
9
+ if (session) {
10
+ const s = session;
11
+ session = null;
12
+ await closeBrowserQuiet(s.browser);
13
+ }
14
+ };