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.
@@ -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(/&quot;/g, "\"")
5
+ .replace(/&#39;/g, "'")
6
+ .replace(/&apos;/g, "'")
7
+ .replace(/&lt;/g, "<")
8
+ .replace(/&gt;/g, ">")
9
+ .replace(/&nbsp;/g, " ")
10
+ .replace(/&amp;/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.20",
4
- "description": "MCP filesystem server: batched file tools + standalone Windows shell tools (extended fork)",
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
  }