mcp-fs-shell-windows 0.2.20 → 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.
- package/LICENSE +2 -1
- package/README.md +155 -4
- package/dist/analyze_project/handler.js +62 -0
- package/dist/analyze_project/schema.js +5 -0
- package/dist/browser/browserActions.js +128 -0
- package/dist/browser/fuzzySearch.js +49 -0
- package/dist/browser/handler.js +227 -0
- package/dist/browser/launcher.js +103 -0
- package/dist/browser/schema.js +37 -0
- package/dist/browser/session.js +14 -0
- package/dist/compat/handler.js +254 -0
- package/dist/compat/schema.js +97 -0
- package/dist/gh/handler.js +406 -0
- package/dist/gh/schema.js +36 -0
- package/dist/git/handler.js +209 -0
- package/dist/git/schema.js +23 -0
- package/dist/launch_file/handler.js +3 -1
- package/dist/query_database/handler.js +27 -0
- package/dist/query_database/schema.js +5 -0
- package/dist/rag/handler.js +103 -0
- package/dist/rag/helpers.js +126 -0
- package/dist/rag/schema.js +16 -0
- package/dist/read_document/handler.js +61 -0
- package/dist/read_document/schema.js +4 -0
- package/dist/server.js +864 -1
- package/dist/shell/handler.js +8 -0
- package/dist/subagent/handler.js +752 -0
- package/dist/subagent/handoffMessage.js +56 -0
- package/dist/subagent/schema.js +18 -0
- package/dist/subagent/subAgentToolCallParser.js +491 -0
- package/dist/subagent/toolCallValidator.js +97 -0
- package/dist/system/handler.js +239 -0
- package/dist/system/schema.js +21 -0
- package/dist/web/ddgParse.js +51 -0
- package/dist/web/handler.js +286 -0
- package/dist/web/schema.js +18 -0
- package/package.json +22 -2
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// system/handler.ts — the 6 Beledarian-parity system tools (MCP filesystem fork).
|
|
2
|
+
//
|
|
3
|
+
// Ported from the reference Beledarian toolsProvider (get_system_info,
|
|
4
|
+
// read_clipboard, write_clipboard, send_notification, open_file,
|
|
5
|
+
// preview_html) and registered under the same names with the reference
|
|
6
|
+
// parameter shapes and return shapes:
|
|
7
|
+
//
|
|
8
|
+
// - get_system_info: the reference's fields verbatim ({platform, arch,
|
|
9
|
+
// release, hostname, total_memory, free_memory, cpus, node_version}).
|
|
10
|
+
// - read_clipboard / write_clipboard: the reference's platform branches
|
|
11
|
+
// (PowerShell Get-Clipboard / Set-Clipboard on Windows, pbpaste / pbcopy
|
|
12
|
+
// on macOS, xclip elsewhere), the reference's Promise.race 5-second
|
|
13
|
+
// timeout, and the reference's error strings. An EMPTY clipboard reads
|
|
14
|
+
// back as content "" (not an error), exactly as the reference.
|
|
15
|
+
// write_clipboard keeps the reference's base64 Set-Clipboard approach
|
|
16
|
+
// (the content is base64-encoded into the PowerShell command line, so
|
|
17
|
+
// quotes, newlines, and Unicode need no escaping).
|
|
18
|
+
// - send_notification: node-notifier (the reference's dependency, the only
|
|
19
|
+
// new one in this version) with the reference's options (title, message,
|
|
20
|
+
// sound: true, wait: false), dispatched fire-and-forget; returns the
|
|
21
|
+
// reference's {success: true, message}.
|
|
22
|
+
// - open_file: the Beledarian-named equivalent of this fork's launch_file —
|
|
23
|
+
// the same `target` parameter, the same policy, and the same internal
|
|
24
|
+
// launch helper (handleLaunchFile -> launchDetached -> explorer.exe),
|
|
25
|
+
// with the reference's {success: true, message} shape on success.
|
|
26
|
+
// - preview_html: writes the HTML to a temp .html file in the system temp
|
|
27
|
+
// dir and launches it in the default browser with the same detached
|
|
28
|
+
// opener launch_file uses; returns the reference's {success, path,
|
|
29
|
+
// message}. The user-supplied file_name is reduced to its last path
|
|
30
|
+
// segment (the file always lands in the temp dir); when omitted the
|
|
31
|
+
// reference's `preview_<timestamp>.html` default is used.
|
|
32
|
+
import os from "os";
|
|
33
|
+
import path from "path";
|
|
34
|
+
import fs from "fs/promises";
|
|
35
|
+
import { spawn } from "child_process";
|
|
36
|
+
import notifier from "node-notifier";
|
|
37
|
+
import { launchDetached, openerPath, handleLaunchFile } from "../launch_file/handler.js";
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// get_system_info
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* Get information about the system (OS, CPU, Memory). The reference's fields
|
|
43
|
+
* verbatim: platform, arch, release, hostname, total_memory, free_memory,
|
|
44
|
+
* cpus (logical CPU count), node_version.
|
|
45
|
+
*/
|
|
46
|
+
export async function handleGetSystemInfo() {
|
|
47
|
+
return JSON.stringify({
|
|
48
|
+
platform: os.platform(),
|
|
49
|
+
arch: os.arch(),
|
|
50
|
+
release: os.release(),
|
|
51
|
+
hostname: os.hostname(),
|
|
52
|
+
total_memory: os.totalmem(),
|
|
53
|
+
free_memory: os.freemem(),
|
|
54
|
+
cpus: os.cpus().length,
|
|
55
|
+
node_version: process.version,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Race a clipboard child-process op against the reference's 5-second
|
|
60
|
+
* timeout (the timeout rejects; the op itself resolves with an error
|
|
61
|
+
* object on failure — same settlement shape as the reference).
|
|
62
|
+
*/
|
|
63
|
+
function clipboardRace(op) {
|
|
64
|
+
return Promise.race([
|
|
65
|
+
op(),
|
|
66
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("Clipboard operation timeout")), 5000)),
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Read text content from the system clipboard (Windows: `powershell
|
|
71
|
+
* -command Get-Clipboard`, macOS: `pbpaste`, other: `xclip -selection
|
|
72
|
+
* clipboard -o`). An empty clipboard yields content "" (NOT an error).
|
|
73
|
+
* 5-second race timeout; a failed read or timeout yields a clear error.
|
|
74
|
+
*/
|
|
75
|
+
export async function handleReadClipboard() {
|
|
76
|
+
let command = "";
|
|
77
|
+
let args = [];
|
|
78
|
+
if (process.platform === "win32") {
|
|
79
|
+
command = "powershell";
|
|
80
|
+
args = ["-command", "Get-Clipboard"];
|
|
81
|
+
}
|
|
82
|
+
else if (process.platform === "darwin") {
|
|
83
|
+
command = "pbpaste";
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
// Linux fallback (might fail if tools missing)
|
|
87
|
+
command = "xclip";
|
|
88
|
+
args = ["-selection", "clipboard", "-o"];
|
|
89
|
+
}
|
|
90
|
+
const result = await clipboardRace(() => new Promise((resolve) => {
|
|
91
|
+
const child = spawn(command, args);
|
|
92
|
+
let output = "";
|
|
93
|
+
let error = "";
|
|
94
|
+
child.stdout.on("data", (data) => (output += data.toString()));
|
|
95
|
+
child.stderr.on("data", (data) => (error += data.toString()));
|
|
96
|
+
child.on("close", (code) => {
|
|
97
|
+
if (code === 0) {
|
|
98
|
+
resolve({ content: output.trim() });
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
resolve({ error: `Failed to read clipboard. Exit code: ${code}. Error: ${error}` });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
child.on("error", (err) => {
|
|
105
|
+
resolve({ error: `Failed to spawn clipboard command: ${err.message}` });
|
|
106
|
+
});
|
|
107
|
+
})).catch((err) => ({ error: err instanceof Error ? err.message : String(err) }));
|
|
108
|
+
if (result.error) {
|
|
109
|
+
throw new Error(result.error);
|
|
110
|
+
}
|
|
111
|
+
return JSON.stringify({ content: result.content ?? "" });
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Write text content to the system clipboard (Windows: the reference's
|
|
115
|
+
* base64 approach — the content is UTF-8 base64-encoded into the
|
|
116
|
+
* PowerShell command line and `Set-Clipboard` stores the decoded text, so
|
|
117
|
+
* arbitrary text (quotes, newlines, Unicode) passes through with no
|
|
118
|
+
* escaping; macOS: `pbcopy`, other: `xclip -selection clipboard -i` with
|
|
119
|
+
* the content on stdin). 5-second race timeout.
|
|
120
|
+
*/
|
|
121
|
+
export async function handleWriteClipboard(content) {
|
|
122
|
+
let command = "";
|
|
123
|
+
let args = [];
|
|
124
|
+
let input = content;
|
|
125
|
+
if (process.platform === "win32") {
|
|
126
|
+
command = "powershell";
|
|
127
|
+
// Use base64 to avoid complex escaping issues in PowerShell
|
|
128
|
+
const base64Content = Buffer.from(content, "utf8").toString("base64");
|
|
129
|
+
// Command decodes base64 and sets clipboard
|
|
130
|
+
args = [
|
|
131
|
+
"-command",
|
|
132
|
+
`$str = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64Content}')); Set-Clipboard -Value $str`,
|
|
133
|
+
];
|
|
134
|
+
input = ""; // Input handled via args
|
|
135
|
+
}
|
|
136
|
+
else if (process.platform === "darwin") {
|
|
137
|
+
command = "pbcopy";
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
command = "xclip";
|
|
141
|
+
args = ["-selection", "clipboard", "-i"];
|
|
142
|
+
}
|
|
143
|
+
const result = await clipboardRace(() => new Promise((resolve) => {
|
|
144
|
+
const child = spawn(command, args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
145
|
+
if (input && process.platform !== "win32") {
|
|
146
|
+
child.stdin.write(input);
|
|
147
|
+
child.stdin.end();
|
|
148
|
+
}
|
|
149
|
+
else if (process.platform === "win32") {
|
|
150
|
+
child.stdin.end();
|
|
151
|
+
}
|
|
152
|
+
let error = "";
|
|
153
|
+
child.stderr.on("data", (data) => (error += data.toString()));
|
|
154
|
+
child.on("close", (code) => {
|
|
155
|
+
if (code === 0) {
|
|
156
|
+
resolve({ success: true });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
resolve({ error: `Failed to write to clipboard. Exit code: ${code}. Error: ${error}` });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
child.on("error", (err) => {
|
|
163
|
+
resolve({ error: `Failed to spawn clipboard command: ${err.message}` });
|
|
164
|
+
});
|
|
165
|
+
})).catch((err) => ({ error: err instanceof Error ? err.message : String(err) }));
|
|
166
|
+
if (result.error) {
|
|
167
|
+
throw new Error(result.error);
|
|
168
|
+
}
|
|
169
|
+
return JSON.stringify({ success: true });
|
|
170
|
+
}
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
// send_notification
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
/**
|
|
175
|
+
* Send a system notification to the user. node-notifier picks the platform
|
|
176
|
+
* backend (on Windows: the bundled snoretoast WinRT toast; a balloon via
|
|
177
|
+
* notifu as fallback) — dispatch is fire-and-forget exactly as the
|
|
178
|
+
* reference, which passes the reference's options (title, message,
|
|
179
|
+
* sound: true, wait: false) and returns success immediately. The `sound`
|
|
180
|
+
* option is part of the reference call but not in the DefinitelyTyped
|
|
181
|
+
* declaration, so the options object is cast to the notify() parameter
|
|
182
|
+
* type to keep the reference call verbatim.
|
|
183
|
+
*/
|
|
184
|
+
export async function handleSendNotification(title, message) {
|
|
185
|
+
notifier.notify({
|
|
186
|
+
title: title,
|
|
187
|
+
message: message,
|
|
188
|
+
sound: true,
|
|
189
|
+
wait: false,
|
|
190
|
+
});
|
|
191
|
+
return JSON.stringify({ success: true, message: "Notification sent." });
|
|
192
|
+
}
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
// open_file
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
/**
|
|
197
|
+
* Open a file, folder, or http(s) URL in the OS default app. The
|
|
198
|
+
* Beledarian-named equivalent of launch_file: same `target` parameter and
|
|
199
|
+
* same policy, delegating to the exact same internal launch helper chain
|
|
200
|
+
* (handleLaunchFile -> launchDetached -> explorer.exe), with the
|
|
201
|
+
* reference's success shape. Errors (unsupported URL scheme, path outside
|
|
202
|
+
* the allowed roots, missing file, failed opener spawn) propagate from
|
|
203
|
+
* handleLaunchFile as clear thrown errors.
|
|
204
|
+
*/
|
|
205
|
+
export async function handleOpenFile(target, allowedDirectories) {
|
|
206
|
+
await handleLaunchFile(target, allowedDirectories);
|
|
207
|
+
return JSON.stringify({
|
|
208
|
+
success: true,
|
|
209
|
+
message: `Opened ${(target ?? "").trim()}`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// preview_html
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
/**
|
|
216
|
+
* Render and preview HTML content in the system's default browser. Writes
|
|
217
|
+
* the content to a temp .html file in the system temp dir and launches it
|
|
218
|
+
* with the same detached opener launch_file uses. The user-supplied
|
|
219
|
+
* file_name is reduced to its last path segment (the file always lands in
|
|
220
|
+
* the temp dir; an existing file of that name is overwritten); when
|
|
221
|
+
* omitted, the reference's `preview_<timestamp>.html` default is used.
|
|
222
|
+
* The temp file is left on disk and its path is returned.
|
|
223
|
+
*/
|
|
224
|
+
export async function handlePreviewHtml(htmlContent, fileName) {
|
|
225
|
+
const name = path.basename((fileName && fileName.trim()) || `preview_${Date.now()}.html`);
|
|
226
|
+
const filePath = path.join(os.tmpdir(), name);
|
|
227
|
+
await fs.writeFile(filePath, htmlContent, "utf-8");
|
|
228
|
+
try {
|
|
229
|
+
await launchDetached(openerPath(), filePath);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
throw new Error(`Failed to launch preview: ${err instanceof Error ? err.message : String(err)}`);
|
|
233
|
+
}
|
|
234
|
+
return JSON.stringify({
|
|
235
|
+
success: true,
|
|
236
|
+
path: filePath,
|
|
237
|
+
message: "HTML preview launched in browser.",
|
|
238
|
+
});
|
|
239
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// system/schema.ts — argument schemas for the 6 system tools.
|
|
2
|
+
// (MCP filesystem fork; Beledarian-parity system utilities: OS/CPU/memory
|
|
3
|
+
// info, clipboard read/write, desktop notifications, open file/URL, HTML
|
|
4
|
+
// preview. Ported from the reference Beledarian toolsProvider.ts.)
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const GetSystemInfoArgsSchema = z.object({});
|
|
7
|
+
export const ReadClipboardArgsSchema = z.object({});
|
|
8
|
+
export const WriteClipboardArgsSchema = z.object({
|
|
9
|
+
content: z.string(),
|
|
10
|
+
});
|
|
11
|
+
export const SendNotificationArgsSchema = z.object({
|
|
12
|
+
title: z.string(),
|
|
13
|
+
message: z.string(),
|
|
14
|
+
});
|
|
15
|
+
export const OpenFileArgsSchema = z.object({
|
|
16
|
+
target: z.string().describe("File path or URL"),
|
|
17
|
+
});
|
|
18
|
+
export const PreviewHtmlArgsSchema = z.object({
|
|
19
|
+
html_content: z.string(),
|
|
20
|
+
file_name: z.string().optional().describe("Optional filename (default: preview.html)"),
|
|
21
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// DuckDuckGo HTML parsing helpers — ported verbatim from the Beledarian plugin's
|
|
2
|
+
// web_search implementation (toolsProvider.ts, web_search section) so results match.
|
|
3
|
+
export const decodeHtmlEntities = (value) => value
|
|
4
|
+
.replace(/"/g, "\"")
|
|
5
|
+
.replace(/'/g, "'")
|
|
6
|
+
.replace(/'/g, "'")
|
|
7
|
+
.replace(/</g, "<")
|
|
8
|
+
.replace(/>/g, ">")
|
|
9
|
+
.replace(/ /g, " ")
|
|
10
|
+
.replace(/&/g, "&");
|
|
11
|
+
export const stripHtml = (value) => decodeHtmlEntities(value)
|
|
12
|
+
.replace(/<[^>]+>/g, " ")
|
|
13
|
+
.replace(/\s+/g, " ")
|
|
14
|
+
.trim();
|
|
15
|
+
export const normalizeDuckDuckGoLink = (link) => {
|
|
16
|
+
const decoded = decodeHtmlEntities(link);
|
|
17
|
+
const absolute = decoded.startsWith("//")
|
|
18
|
+
? `https:${decoded}`
|
|
19
|
+
: decoded.startsWith("/")
|
|
20
|
+
? `https://duckduckgo.com${decoded}`
|
|
21
|
+
: decoded;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(absolute);
|
|
24
|
+
const redirect = parsed.searchParams.get("uddg");
|
|
25
|
+
if (redirect) {
|
|
26
|
+
return decodeURIComponent(redirect);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Return original normalized URL below.
|
|
31
|
+
}
|
|
32
|
+
return absolute;
|
|
33
|
+
};
|
|
34
|
+
export const parseDuckDuckGoHtml = (html, provider) => {
|
|
35
|
+
const parsedResults = [];
|
|
36
|
+
const titleRegex = /<a[^>]*class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
37
|
+
let match;
|
|
38
|
+
while ((match = titleRegex.exec(html)) !== null) {
|
|
39
|
+
const link = normalizeDuckDuckGoLink(match[1]);
|
|
40
|
+
const title = stripHtml(match[2]);
|
|
41
|
+
const nearbyHtml = html.slice(match.index, Math.min(html.length, match.index + 1800));
|
|
42
|
+
const snippetMatch = nearbyHtml.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/(?:a|div)>/i);
|
|
43
|
+
const snippet = snippetMatch ? stripHtml(snippetMatch[1]) : "";
|
|
44
|
+
if (title && link) {
|
|
45
|
+
parsedResults.push({ title, link, snippet, provider });
|
|
46
|
+
}
|
|
47
|
+
if (parsedResults.length >= 10)
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
return parsedResults;
|
|
51
|
+
};
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import { closeSharedBrowser, getSharedBrowser, } from "../browser/launcher.js";
|
|
2
|
+
import { parseDuckDuckGoHtml, } from "./ddgParse.js";
|
|
3
|
+
// The browser legs (duckduckgo-html / google / bing) launch through the shared
|
|
4
|
+
// browser launcher (browser/launcher.ts): Chrome-for-Testing 142.0.7444.175 via
|
|
5
|
+
// puppeteer-core (pure JS, no browser download). Launch is lazy and provider
|
|
6
|
+
// errors are collected (not fatal), so a missing browser never breaks the
|
|
7
|
+
// non-puppeteer legs. getSharedBrowser/closeSharedBrowser preserve the
|
|
8
|
+
// per-leg close semantics (each leg gets a clean instance).
|
|
9
|
+
const getBrowser = getSharedBrowser;
|
|
10
|
+
export async function handleWebSearch(query, providers) {
|
|
11
|
+
const results = [];
|
|
12
|
+
const errors = [];
|
|
13
|
+
const logs = [];
|
|
14
|
+
const searchFunctions = {
|
|
15
|
+
"duckduckgo-api": async (q) => {
|
|
16
|
+
const ddg = await import("duck-duck-scrape");
|
|
17
|
+
let attempt = 0;
|
|
18
|
+
let lastError = null;
|
|
19
|
+
while (attempt < 2) {
|
|
20
|
+
try {
|
|
21
|
+
const r = await ddg.search(q, { safeSearch: ddg.SafeSearchType.OFF });
|
|
22
|
+
if (r.results && r.results.length > 0) {
|
|
23
|
+
return r.results.slice(0, 10).map((result) => ({
|
|
24
|
+
title: result.title,
|
|
25
|
+
link: result.url,
|
|
26
|
+
snippet: result.description,
|
|
27
|
+
provider: "duckduckgo-api",
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
lastError = e;
|
|
34
|
+
attempt++;
|
|
35
|
+
await new Promise((res) => setTimeout(res, 1000));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (lastError) {
|
|
39
|
+
throw lastError;
|
|
40
|
+
}
|
|
41
|
+
throw new Error("DuckDuckGo API returned no results");
|
|
42
|
+
},
|
|
43
|
+
"duckduckgo-fetch": async (q) => {
|
|
44
|
+
const response = await fetch(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(q)}`, {
|
|
45
|
+
headers: {
|
|
46
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
|
47
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
throw new Error(`HTTP ${response.status}`);
|
|
52
|
+
}
|
|
53
|
+
const html = await response.text();
|
|
54
|
+
const extracted = parseDuckDuckGoHtml(html, "duckduckgo-fetch");
|
|
55
|
+
if (extracted.length > 0)
|
|
56
|
+
return extracted;
|
|
57
|
+
throw new Error("No results parsed from DuckDuckGo HTML");
|
|
58
|
+
},
|
|
59
|
+
"duckduckgo-html": async (q) => {
|
|
60
|
+
const browser = await getBrowser(); // shared launcher (browser/launcher.ts)
|
|
61
|
+
try {
|
|
62
|
+
const page = await browser.newPage();
|
|
63
|
+
await page.goto(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(q)}`, {
|
|
64
|
+
waitUntil: "networkidle2",
|
|
65
|
+
timeout: 15000,
|
|
66
|
+
});
|
|
67
|
+
const html = await page.content();
|
|
68
|
+
const extracted = parseDuckDuckGoHtml(html, "duckduckgo-html");
|
|
69
|
+
if (extracted.length > 0)
|
|
70
|
+
return extracted;
|
|
71
|
+
throw new Error("No results found");
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await closeSharedBrowser();
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"google": async (q) => {
|
|
78
|
+
const browser = await getBrowser();
|
|
79
|
+
try {
|
|
80
|
+
const page = await browser.newPage();
|
|
81
|
+
await page.goto(`https://www.google.com/search?q=${encodeURIComponent(q)}`, {
|
|
82
|
+
waitUntil: "networkidle2",
|
|
83
|
+
timeout: 15000,
|
|
84
|
+
});
|
|
85
|
+
const extracted = await page.evaluate(() => {
|
|
86
|
+
const items = document.querySelectorAll("div.g");
|
|
87
|
+
const data = [];
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
const titleEl = item.querySelector("h3");
|
|
90
|
+
const linkEl = item.querySelector("a");
|
|
91
|
+
const snippetEl = item.querySelector('div[style*="-webkit-line-clamp"]') ||
|
|
92
|
+
item.querySelector("div.VwiC3b");
|
|
93
|
+
if (titleEl && linkEl) {
|
|
94
|
+
data.push({
|
|
95
|
+
title: titleEl.innerText,
|
|
96
|
+
link: linkEl.getAttribute("href") || "",
|
|
97
|
+
snippet: snippetEl ? snippetEl.innerText : "",
|
|
98
|
+
provider: "google",
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return data;
|
|
103
|
+
});
|
|
104
|
+
if (extracted.length > 0)
|
|
105
|
+
return extracted.slice(0, 10);
|
|
106
|
+
throw new Error("No results found");
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await closeSharedBrowser();
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"bing": async (q) => {
|
|
113
|
+
const browser = await getBrowser();
|
|
114
|
+
try {
|
|
115
|
+
const page = await browser.newPage();
|
|
116
|
+
await page.goto(`https://www.bing.com/search?q=${encodeURIComponent(q)}`, {
|
|
117
|
+
waitUntil: "networkidle2",
|
|
118
|
+
timeout: 15000,
|
|
119
|
+
});
|
|
120
|
+
const extracted = await page.evaluate(() => {
|
|
121
|
+
const items = document.querySelectorAll("li.b_algo");
|
|
122
|
+
const data = [];
|
|
123
|
+
for (const item of items) {
|
|
124
|
+
const titleEl = item.querySelector("h2 a");
|
|
125
|
+
const linkEl = item.querySelector("h2 a");
|
|
126
|
+
const snippetEl = item.querySelector("p");
|
|
127
|
+
if (titleEl && linkEl) {
|
|
128
|
+
data.push({
|
|
129
|
+
title: titleEl.innerText,
|
|
130
|
+
link: linkEl.getAttribute("href") || "",
|
|
131
|
+
snippet: snippetEl ? snippetEl.innerText : "",
|
|
132
|
+
provider: "bing",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return data;
|
|
137
|
+
});
|
|
138
|
+
if (extracted.length > 0)
|
|
139
|
+
return extracted.slice(0, 10);
|
|
140
|
+
throw new Error("No results found");
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await closeSharedBrowser();
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
if (providers && providers.length > 0) {
|
|
148
|
+
for (const providerKey of providers) {
|
|
149
|
+
try {
|
|
150
|
+
logs.push(`[Manual] Attempting ${providerKey}...`);
|
|
151
|
+
const pResults = await searchFunctions[providerKey](query);
|
|
152
|
+
results.push(...pResults);
|
|
153
|
+
logs.push(`[Manual] Success: ${providerKey} found ${pResults.length} results.`);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
157
|
+
errors.push(`${providerKey}: ${errMsg}`);
|
|
158
|
+
logs.push(`[Manual] Failed: ${providerKey} - ${errMsg}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const chain = [
|
|
164
|
+
"duckduckgo-fetch",
|
|
165
|
+
"duckduckgo-api",
|
|
166
|
+
"duckduckgo-html",
|
|
167
|
+
"google",
|
|
168
|
+
"bing",
|
|
169
|
+
];
|
|
170
|
+
const browserProviders = ["duckduckgo-html", "google", "bing"];
|
|
171
|
+
let chromeUnavailable = false;
|
|
172
|
+
for (let i = 0; i < chain.length; i++) {
|
|
173
|
+
const providerKey = chain[i];
|
|
174
|
+
if (chromeUnavailable && browserProviders.includes(providerKey)) {
|
|
175
|
+
logs.push(`[Fallback Chain] Skipping ${providerKey}: Chrome not available on this system.`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const nextProvider = chain[i + 1];
|
|
179
|
+
try {
|
|
180
|
+
logs.push(`[Fallback Chain] Attempting ${providerKey}...`);
|
|
181
|
+
const pResults = await searchFunctions[providerKey](query);
|
|
182
|
+
results.push(...pResults);
|
|
183
|
+
logs.push(`[Fallback Chain] Success: ${providerKey} found ${pResults.length} results. Stopping chain.`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
catch (e) {
|
|
187
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
188
|
+
errors.push(`${providerKey}: ${errMsg}`);
|
|
189
|
+
if (/chrome|chromium/i.test(errMsg) && browserProviders.includes(providerKey)) {
|
|
190
|
+
chromeUnavailable = true;
|
|
191
|
+
logs.push(`[Fallback Chain] Failed: ${providerKey} - Chrome not available. Skipping all browser-based providers.`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const nextMsg = nextProvider ? `Falling back to ${nextProvider}...` : "No more providers.";
|
|
195
|
+
logs.push(`[Fallback Chain] Failed: ${providerKey} - ${errMsg}. ${nextMsg}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (results.length === 0) {
|
|
201
|
+
return JSON.stringify({
|
|
202
|
+
error: "All attempted search providers failed.",
|
|
203
|
+
attempts: errors,
|
|
204
|
+
trace: logs,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const seenLinks = new Set();
|
|
208
|
+
const dedupedResults = results.filter((r) => {
|
|
209
|
+
const key = r.link.trim();
|
|
210
|
+
if (!key || seenLinks.has(key))
|
|
211
|
+
return false;
|
|
212
|
+
seenLinks.add(key);
|
|
213
|
+
return true;
|
|
214
|
+
});
|
|
215
|
+
return JSON.stringify({
|
|
216
|
+
results: dedupedResults,
|
|
217
|
+
meta: {
|
|
218
|
+
total_found: dedupedResults.length,
|
|
219
|
+
providers_used: [...new Set(dedupedResults.map((r) => r.provider))],
|
|
220
|
+
no_api_key_required: true,
|
|
221
|
+
trace: logs,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
export async function handleFetchWebContent(url) {
|
|
226
|
+
try {
|
|
227
|
+
const response = await fetch(url);
|
|
228
|
+
if (!response.ok) {
|
|
229
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
230
|
+
}
|
|
231
|
+
let text = await response.text();
|
|
232
|
+
const result = {
|
|
233
|
+
url,
|
|
234
|
+
status: response.status,
|
|
235
|
+
};
|
|
236
|
+
const titleMatch = text.match(/<title[^>]*>([^<]+)<\/title>/i);
|
|
237
|
+
if (titleMatch)
|
|
238
|
+
result.title = titleMatch[1];
|
|
239
|
+
const { compile } = await import("html-to-text");
|
|
240
|
+
const compiledConvert = compile({
|
|
241
|
+
wordwrap: false,
|
|
242
|
+
selectors: [
|
|
243
|
+
{ selector: "a", options: { ignoreHref: true } },
|
|
244
|
+
{ selector: "img", format: "skip" },
|
|
245
|
+
],
|
|
246
|
+
});
|
|
247
|
+
text = compiledConvert(text);
|
|
248
|
+
result.content =
|
|
249
|
+
text.substring(0, 40000) + (text.length > 40000 ? " (truncated)" : "");
|
|
250
|
+
return JSON.stringify(result);
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
return JSON.stringify({
|
|
254
|
+
error: `Failed to fetch URL: ${error instanceof Error ? error.message : String(error)}`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export async function handleWikipediaSearch(query, lang = "en") {
|
|
259
|
+
try {
|
|
260
|
+
const searchUrl = `https://${lang}.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodeURIComponent(query)}&format=json`;
|
|
261
|
+
const searchResponse = await fetch(searchUrl);
|
|
262
|
+
const searchData = await searchResponse.json();
|
|
263
|
+
if (!searchData.query || !searchData.query.search || searchData.query.search.length === 0) {
|
|
264
|
+
return JSON.stringify({ results: "No Wikipedia articles found." });
|
|
265
|
+
}
|
|
266
|
+
const results = [];
|
|
267
|
+
for (const item of searchData.query.search.slice(0, 3)) {
|
|
268
|
+
// Top 3
|
|
269
|
+
const pageUrl = `https://${lang}.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&pageids=${item.pageid}&format=json`;
|
|
270
|
+
const pageResponse = await fetch(pageUrl);
|
|
271
|
+
const pageData = await pageResponse.json();
|
|
272
|
+
const page = pageData.query.pages[item.pageid];
|
|
273
|
+
results.push({
|
|
274
|
+
title: item.title,
|
|
275
|
+
summary: page.extract.substring(0, 2000) + (page.extract.length > 2000 ? "..." : ""),
|
|
276
|
+
url: `https://${lang}.wikipedia.org/wiki/${encodeURIComponent(item.title.replace(/ /g, "_"))}`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return JSON.stringify({ results });
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
return JSON.stringify({
|
|
283
|
+
error: `Wikipedia search failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export const WebSearchArgsSchema = z.object({
|
|
3
|
+
query: z.string().describe("The search query"),
|
|
4
|
+
providers: z
|
|
5
|
+
.array(z.enum(["duckduckgo-api", "duckduckgo-fetch", "duckduckgo-html", "google", "bing"]))
|
|
6
|
+
.optional()
|
|
7
|
+
.describe("Optional: List of specific providers. If omitted, fallback chain is: DDG Fetch (no Chrome) -> DDG API -> DDG browser -> Google -> Bing."),
|
|
8
|
+
});
|
|
9
|
+
export const FetchWebContentArgsSchema = z.object({
|
|
10
|
+
url: z.string().describe("The URL to fetch"),
|
|
11
|
+
});
|
|
12
|
+
export const WikipediaSearchArgsSchema = z.object({
|
|
13
|
+
query: z.string().describe("The search query"),
|
|
14
|
+
lang: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe("Language code (default: en)"),
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-fs-shell-windows",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "MCP
|
|
3
|
+
"version": "0.2.29",
|
|
4
|
+
"description": "MCP server with 82 tools: batched filesystem access, standalone Windows shell, and a Beledarian-parity tool set (git, GitHub, system, document, database, web, RAG, browser, sub-agent). Extended fork of mcp-filesystem-extended.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"mcp",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"filesystem",
|
|
10
|
+
"windows",
|
|
11
|
+
"shell",
|
|
12
|
+
"lm-studio",
|
|
13
|
+
"claude",
|
|
14
|
+
"deno",
|
|
15
|
+
"puppeteer"
|
|
16
|
+
],
|
|
6
17
|
"author": "Gerard DeLuca",
|
|
7
18
|
"repository": {
|
|
8
19
|
"type": "git",
|
|
@@ -24,21 +35,30 @@
|
|
|
24
35
|
],
|
|
25
36
|
"scripts": {
|
|
26
37
|
"build": "tsc && shx chmod +x dist/*.js",
|
|
38
|
+
"test": "node --test tests/*.test.js",
|
|
27
39
|
"prepare": "npm run build",
|
|
28
40
|
"watch": "tsc --watch"
|
|
29
41
|
},
|
|
30
42
|
"dependencies": {
|
|
31
43
|
"@modelcontextprotocol/sdk": "0.5.0",
|
|
32
44
|
"diff": "^5.1.0",
|
|
45
|
+
"duck-duck-scrape": "^2.2.7",
|
|
33
46
|
"glob": "^10.3.10",
|
|
47
|
+
"html-to-text": "^9.0.5",
|
|
48
|
+
"mammoth": "^1.12.3",
|
|
34
49
|
"minimatch": "^10.0.1",
|
|
50
|
+
"node-notifier": "^10.0.1",
|
|
51
|
+
"pdf-parse": "^2.4.5",
|
|
52
|
+
"puppeteer-core": "^24.31.0",
|
|
35
53
|
"zod": "^3.23.8",
|
|
36
54
|
"zod-to-json-schema": "^3.23.5"
|
|
37
55
|
},
|
|
38
56
|
"devDependencies": {
|
|
39
57
|
"@types/diff": "^5.0.9",
|
|
58
|
+
"@types/html-to-text": "^9.0.4",
|
|
40
59
|
"@types/minimatch": "^5.1.2",
|
|
41
60
|
"@types/node": "^22",
|
|
61
|
+
"@types/node-notifier": "^8.0.5",
|
|
42
62
|
"shx": "^0.3.4",
|
|
43
63
|
"typescript": "^5.3.3"
|
|
44
64
|
}
|