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 CHANGED
@@ -2,7 +2,8 @@ MIT License
2
2
 
3
3
  Copyright (c) 2024-2025 Anthropic, PBC (original @modelcontextprotocol/server-filesystem)
4
4
  Copyright (c) 2025 fabienvauchelles (mcp-filesystem-extended base fork)
5
- Copyright (c) 2026 Gerard DeLuca (robustness patches, shell tools, Windows hardening)
5
+ Copyright (c) 2025 Laurin Feulner (Beledarians_LM_Studio_Toolbox Beledarian-parity tool ports)
6
+ Copyright (c) 2026 Gerard DeLuca (robustness patches, shell tools, Windows hardening, Beledarian-parity tool set)
6
7
 
7
8
  Permission is hereby granted, free of charge, to any person obtaining a copy
8
9
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mcp-fs-shell-windows — filesystem + standalone Windows shell MCP server
2
2
 
3
- A Windows-first MCP server that gives AI clients **comprehensive filesystem access** (batched read/write/search/patch across your chosen drives and UNC shares) plus **reliable local shell execution** (synchronous, background, and interactive) — in a single Node.js process with no client-side permission toggles.
3
+ A Windows-first MCP server that gives AI clients **comprehensive filesystem access** (30 tools), **reliable local shell execution** (8 tools), and a full **Beledarian-parity tool set** git, GitHub, system, document, database, web, RAG, browser, and sub-agent tools (82 tools total) — in a single Node.js process with no client-side permission toggles.
4
4
 
5
5
  Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended), which itself extends the official [`@modelcontextprotocol/server-filesystem`](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem). Developed and tested primarily on **Windows**: drive-letter roots (`C:\`, `D:\`) and UNC network-share roots (`\\server\\share`) are first-class, and the shell tools run commands through `cmd.exe` byte-for-byte.
6
6
 
@@ -12,6 +12,7 @@ Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://gith
12
12
  |---|---|
13
13
  | Official base | `@modelcontextprotocol/server-filesystem` (Anthropic, PBC; MIT) |
14
14
  | Base fork | [fabienvauchelles/mcp-filesystem-extended](https://github.com/fabienvauchelles/mcp-filesystem-extended) @ `b6c317a` (MIT) |
15
+ | Parity layer (v0.2.21–v0.2.29) | [Beledarian/Beledarians_LM_Studio_Toolbox](https://github.com/Beledarian/Beledarians_LM_Studio_Toolbox) — the “Beledarian” LM Studio plugin (Laurin Feulner; MIT): 42-tool port (git, GitHub, system, document/database, web, RAG, browser, Beledarian-named compatibility aliases, sub-agent) |
15
16
  | This fork | v0.2.1 tool parity + `transfer_files` rename, robustness patches, 8 shell tools (v0.2.2), verbatim argv (v0.2.4), `launch_file` launcher (v0.2.7), raw-byte access + `dir_diff` + text encodings (v0.2.9–v0.2.11), `wait_for_file` (v0.2.12), `replace_regex` (v0.2.14), reparse-point & long-path hardening (v0.2.15–v0.2.16), Recycle-Bin deletes (v0.2.17), `create_link` (v0.2.18), `run_javascript_free` unrestricted code execution (v0.2.20) — see [Differences](#differences-from-the-base-fork) |
16
17
 
17
18
  ## Features
@@ -19,6 +20,15 @@ Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://gith
19
20
  - **30 filesystem tools** — batched read/write/append/delete (including raw-byte access, per-file text encodings, Recycle-Bin deletes, and link creation), move/copy, line-number patching, search (name / glob / regex / fuzzy / wait-for-file), in-place regex replacement, tree, counts, checksums, file and directory diffs, exact-match text editing. Confined to the allowed directories passed on the command line.
20
21
  - **8 shell tools** — synchronous, background, and interactive command execution implemented *inside the MCP server process*: no client-side permission toggles, survives client restarts, works with any MCP client.
21
22
  - **1 code-execution tool** (`run_javascript_free`) — run a JavaScript/TypeScript snippet on the Deno runtime with **unrestricted** permissions (full filesystem, network, env, child processes, arbitrary imports): the "free" counterpart to sandboxed `run_javascript` tools that confine code to a working directory.
23
+ - **6 git tools** (`git_status`, `git_diff`, `git_commit`, `git_log`, `git_add`, `git_checkout`) — spawn the git CLI against the repo at the server process's current working directory (no new dependency, no working-directory parameter — the same "current working directory context" as the shell tools).
24
+ - **8 GitHub tools** (`gh_auth`, `gh_create_issue`, `gh_list_issues`, `gh_view_comments`, `gh_create_pr`, `gh_list_prs`, `gh_view_pr_diff`, `gh_push`) — spawn the `gh` CLI (and `git push` for `gh_push`) against the repo at the server process's current working directory (no new dependency, no working-directory parameter — the same "current working directory context" as the git/shell tools).
25
+ - **6 system tools** (`get_system_info`, `read_clipboard`, `write_clipboard`, `send_notification`, `open_file`, `preview_html`) — Beledarian-parity system utilities: OS/CPU/memory info, clipboard read/write (5-second timeout; empty clipboard = empty content, not an error), desktop notifications, opening files/URLs, and HTML preview. New dependency: `node-notifier` (notifications only; Windows toasts via the bundled snoretoast binary). `open_file` is the Beledarian-named equivalent of `launch_file` and delegates to the same internal launch helper.
26
+ - **3 document & database tools** (`read_document`, `query_database`, `analyze_project`) — PDF/DOCX text extraction, read-only SQLite queries (built-in `node:sqlite`, no native dependency), and project-wide lint detection (npm lint script → eslint → pylint).
27
+ - **3 web tools** (`web_search`, `fetch_web_content`, `wikipedia_search`) — multi-provider web search (DuckDuckGo → Google → Bing fallback chain), clean page-text extraction, and Wikipedia extracts.
28
+ - **2 RAG tools** (`rag_local_files`, `rag_web_content`) — retrieval-augmented generation over local text files or a fetched URL, via a local OpenAI-compatible `/v1/embeddings` endpoint (default: LM Studio on `127.0.0.1:1234`, bundled nomic-embed-text v1.5 model).
29
+ - **4 browser tools** (`browser_session_open`, `browser_session_control`, `browser_session_close`, `browser_open_page`) — persistent headless Chrome sessions (scripted click/type/press/select/hover/scroll/evaluate actions, screenshots, fuzzy in-page find) plus a stateless one-shot page renderer; Chrome-for-Testing via puppeteer-core.
30
+ - **9 Beledarian-compatible aliases** (`execute_command`, `run_in_terminal`, `run_test_command`, `run_background_command`, `check_background_command`, `cancel_background_command`, `run_python`, `run_javascript`, `save_memory`) — drop-in names, parameter shapes, and return shapes for the reference Beledarian plugin's tools (60 s sync cap; `run_javascript` = the confined Deno variant, NOT `run_javascript_free`).
31
+ - **1 sub-agent tool** (`consult_secondary_agent`) — delegates a task to a secondary agent loop running against a local model endpoint (default: LM Studio on `http://localhost:1234/v1`); the sub-agent's tool calls execute in-process against this server's own handlers.
22
32
  - **1 launcher tool** (`launch_file`, Windows-only) — opens files, folders, and http(s) links in their OS default app, fully detached: returns the moment the opener is spawned (never waits for the app), no console window, no stdio inheritance; filesystem targets are confined to the allowed directories.
23
33
  - **Windows-first** — drive roots, UNC roots, long paths beyond the 260-char `MAX_PATH` limit, `cmd.exe /d /c` with verbatim argv, whole-process-tree kills (`taskkill /T /F`), detached interactive console windows.
24
34
  - **NAS-safe startup** — an allowed directory that is missing or offline (e.g., a powered-down network share) logs a warning and the server continues instead of exiting.
@@ -28,7 +38,24 @@ Heavily extended fork of [fabienvauchelles/mcp-filesystem-extended](https://gith
28
38
  - Node.js >= 20 (developed and tested on Node 22).
29
39
  - Windows for the shell tools (they spawn `cmd.exe`, `taskkill`, and `powershell`). The filesystem tools work on any platform Node runs on.
30
40
  - `shell_python` requires Python on the system PATH.
31
- - `run_javascript_free` requires a Deno runtime: the `$DENO_PATH` env var, LM Studio's bundled binary (`<home>/.internal/utils/deno(.exe)` — auto-located by walking up from the server CWD or the user home), or `deno` on the PATH.
41
+ - `run_javascript_free` (unrestricted) and `run_javascript` (confined compat alias) require a Deno runtime: the `$DENO_PATH` env var, LM Studio's bundled binary (`<home>/.internal/utils/deno(.exe)` — auto-located by walking up from the server CWD or the user home), or `deno` on the PATH.
42
+ - The browser tools (`browser_*`) and `web_search`'s browser legs require a **Chrome-for-Testing** executable: set `MCP_CHROME_PATH`, or place a build under the standard puppeteer cache `<home>/.cache/puppeteer/chrome/win64-<version>/chrome-win64/chrome.exe` (auto-detected, highest version wins).
43
+ - `rag_local_files` / `rag_web_content` require a local OpenAI-compatible embeddings endpoint (default: `http://127.0.0.1:1234/v1/embeddings` — LM Studio), and `consult_secondary_agent` requires a local chat-completions endpoint (default: `http://localhost:1234/v1` — LM Studio).
44
+
45
+ Optional environment variables (all optional; when unset, behavior on the reference machine is preserved):
46
+
47
+ | Variable | Effect | Default |
48
+ |---|---|---|
49
+ | `MCP_CHROME_PATH` | Explicit Chrome-for-Testing executable | auto-detect the standard puppeteer cache, then the legacy path |
50
+ | `MCP_MEMORY_FILE` | `save_memory` memory-file path | reference workspace `memory.md`, else `<cwd>/memory.md` |
51
+ | `MCP_RAG_EMBED_URL` | RAG embeddings endpoint | `http://127.0.0.1:1234/v1/embeddings` |
52
+ | `MCP_RAG_EMBED_MODEL` | Embeddings model id | `text-embedding-nomic-embed-text-v1.5` |
53
+ | `MCP_API_TOKEN_FILE` | API-token file for RAG + sub-agent Bearer auth | reference overnight token file (keyless when missing) |
54
+ | `MCP_SUBAGENT_ENDPOINT` | Sub-agent chat-completions endpoint | `http://localhost:1234/v1` |
55
+ | `MCP_SUBAGENT_MODEL` | Sub-agent model id | `local-model` |
56
+ | `MCP_SUBAGENT_CWD` | Sub-agent working dir (its file tools are confined to it) | server CWD |
57
+ | `MCP_SUBAGENT_PROFILES` | JSON string `{role: personaText}` agent profiles | built-ins |
58
+ | `MCP_SUBAGENT_DEBUG` | `"1"` = `[Sub-Agent]` console logging | off |
32
59
 
33
60
  ## Quick start (npm)
34
61
 
@@ -67,6 +94,8 @@ node dist\index.js <allowed-dir> [more-dirs...]
67
94
 
68
95
  Then point your MCP config at `node <path-to-repo>\dist\index.js` with the allowed directories as the remaining args (same shape as the npx example above).
69
96
 
97
+ Unit tests cover the ported pure-function modules (browser actions, fuzzy search, sub-agent call parsing / validation / handoff): `npm test`.
98
+
70
99
  ### Filesystem tools (30)
71
100
 
72
101
  | Tool | Purpose |
@@ -129,12 +158,113 @@ Practical notes:
129
158
  |---|---|
130
159
  | `run_javascript_free` | Run a JS/TS snippet on the Deno runtime with **unrestricted permissions**: full filesystem read/write (any drive or UNC path the server user can reach), network, environment, child processes, and arbitrary imports (`node:*`, `npm:*`, `https:*`). Temp `.ts` file + `deno run --allow-read --allow-write --allow-net --allow-env --allow-sys --allow-run --allow-ffi --allow-import`. Default 5 s timeout, max 60 s; optional `cwd` (defaults to the `shell_cwd` default). Non-zero exit or timeout returns an error that still includes the captured stdout/stderr (100 KB / 20 KB caps). The Deno binary is located via `$DENO_PATH` → `<ancestor>/.internal/utils/deno(.exe)` (walk-up) → `<home>/.lmstudio/.internal/utils/deno(.exe)` → PATH. |
131
160
 
161
+ ### Git tools (6)
162
+
163
+ Operate against the repo at the server process's current working directory (the directory the server was launched in, e.g. the chat working directory). Implemented by spawning the `git` CLI directly — no new dependency, and argument values (messages, branch names, file paths) never pass through a shell: they go verbatim as argv elements. Path arguments (`git_diff.file_path`, `git_add.paths`) resolve relative to the working directory. A working directory that is not inside a git repository yields a clear `{error}` string, not a crash.
164
+
165
+ | Tool | Purpose |
166
+ |---|---|
167
+ | `git_status` | Branch, upstream, ahead/behind, `isClean`, plus staged / not-staged / untracked file lists (JSON: `{current, tracking, ahead, behind, isClean, staged, notStaged, untracked, files}`). |
168
+ | `git_diff` | Working-tree or staged (`cached: true`) unified diff, optionally limited to one file (`file_path`). Returns `{diff}` (or `{diff: "No changes."}`). |
169
+ | `git_commit` | Commit staged changes with `message`; returns `{success, summary: {changed, insertions, deletions}}`. Standard git behavior: only what is staged gets committed. |
170
+ | `git_log` | Recent commits, newest first, capped by `max_count` (default 10): `{history: ["<hash> <subject> (<author>, <date relative>)", ...]}`. |
171
+ | `git_add` | Stage specific `paths` or, when omitted, all changes (`git add .`). Returns `{success, message}`. |
172
+ | `git_checkout` | Switch to an existing branch, or create + switch with `create_new: true` (`git checkout -b`). Returns `{success, message}`. |
173
+
174
+ ### GitHub tools (8)
175
+
176
+ Operate against the repo at the server process's current working directory (no working-directory parameter — `gh` itself resolves the repo from that directory's git remote, exactly as the reference Beledarian tools do). Implemented by spawning the `gh` CLI directly (no new dependency; `gh_push` spawns `git push origin [branch]`), and argument values (titles, bodies, labels, branch names, issue/PR numbers) go verbatim as argv elements — they never pass through a shell. Long issue/PR bodies are written to a temp file in the working directory and passed via `--body-file` (mirroring the reference); the temp file is always removed. `gh` is resolved from PATH first (the reference's `checkGhInstalled`), then from the known portable install `<home>\tools\gh\bin\gh.exe` (this machine's layout); if neither is found, a clear `{error}` string is returned. `gh_auth` runs `gh auth status` and, on failure, opens a **detached** terminal window (`cmd /k`, spawned with ignored stdio so it outlives the tool call) for interactive `gh auth login` — using the resolved absolute gh path so the window works even when gh is not on the inherited PATH.
177
+
178
+ | Tool | Purpose |
179
+ |---|---|
180
+ | `gh_auth` | Check GitHub authentication status. On failure, open a detached terminal window for `gh auth login` (the window persists for the user to sign in). Returns `{success: true, message}` or `{error}`. |
181
+ | `gh_create_issue` | Create a new issue with `title`, optional `body` (temp `--body-file`) and optional `labels` (each `-l`). Returns `{success: true, url}` or `{error}`. |
182
+ | `gh_list_issues` | List issues in the CWD repo: `state` (default `open`), optional `labels`, `limit` (default 10, max 50). Returns `{issues: [{number, title, state, url, labels}]}` (empty array = no matches) or `{error}`. |
183
+ | `gh_view_comments` | View comments on issue/PR `number` (`type` default `issue`). Returns `{comments: [...]}` or `{error}`. |
184
+ | `gh_create_pr` | Create a PR: `title`, `head_branch`, optional `body` (temp `--body-file`), `base_branch` (default `main`). Returns `{success: true, url}` or `{error}`. |
185
+ | `gh_list_prs` | List PRs in the CWD repo: `state` (default `open`), `limit` (default 10, max 50). Returns `{pull_requests: [{number, title, state, url, headRefName, baseRefName}]}` or `{error}`. |
186
+ | `gh_view_pr_diff` | Fetch the PR's patch/diff for `number`. Returns `{diff}` (truncated to 50,000 chars with a marker) or `{error}`. |
187
+ | `gh_push` | Push local commits to the remote (`git push origin [branch]`); `branch` optional, defaults to the current branch. Returns `{success: true, message}` or `{error}`. |
188
+
132
189
  ### Launcher tool (1, Windows-only)
133
190
 
134
191
  | Tool | Purpose |
135
192
  |---|---|
136
193
  | `launch_file` | Launch a file, folder, or http(s) link via the Windows shell opener (`explorer.exe` — the OS default association: VLC for .mkv, Explorer for folders, default browser for web links). The target is passed as a single argv element (spaces, Unicode, `&`, apostrophes, parentheses all safe) and the call returns as soon as the opener process is actually spawned — it never waits for the opened app, creates no console window, and inherits no stdio pipes. Filesystem paths are subject to the allowed-root policy and checked for existence; only `http://`/`https://` links are launched, every other URL scheme (`file:`, `javascript:`, `ms-*`, `steam:`, …) is rejected, so no `file:` URI can bypass the filesystem policy. A failed opener spawn errors instead of reporting success. The response reports `dispatched: true` (the opener process was created) — not that the app handled the target. A host-settable `LAUNCH_FILE_OPENER` env var overrides the opener executable (test seam). |
137
194
 
195
+ ### System tools (6)
196
+
197
+ Beledarian-parity system utilities, ported from the reference Beledarian `toolsProvider` (same names, parameter shapes, and return shapes). No working-directory parameter; the clipboard and notification tools work on any platform Node runs on (PowerShell / pbpaste / xclip branches, as in the reference), while `open_file` and `preview_html` are Windows-only (they use the same `explorer.exe` detached opener as `launch_file`).
198
+
199
+ | Tool | Purpose |
200
+ |---|---|
201
+ | `get_system_info` | OS/CPU/memory info via the Node `os` module. No parameters. Returns `{platform, arch, release, hostname, total_memory, free_memory, cpus, node_version}` (memory in bytes). |
202
+ | `read_clipboard` | Read the system clipboard (`powershell -command Get-Clipboard` on Windows; `pbpaste` on macOS; `xclip` elsewhere) with a 5-second race timeout. An empty clipboard returns `content ""` (not an error). Returns `{content}` or a clear error. |
203
+ | `write_clipboard` | Write text to the clipboard. On Windows the content is base64-encoded into the PowerShell command line and `Set-Clipboard` stores the decoded text (no escaping issues for quotes / newlines / Unicode); `pbcopy` on macOS, `xclip` via stdin elsewhere. 5-second race timeout. Returns `{success: true}` or a clear error. |
204
+ | `send_notification` | Send a desktop notification (title, message) via `node-notifier` (Windows toast through the bundled snoretoast; balloon fallback) — fire-and-forget (`sound: true, wait: false`), as in the reference. Returns `{success: true, message}`. |
205
+ | `open_file` | Open a file path or http(s) URL in the OS default app. The Beledarian-named equivalent of `launch_file`: same `target` parameter, same policy (allowed-roots for filesystem paths + existence check; only http/https URLs), same internal launch helper. Returns `{success: true, message}` or a clear error. |
206
+ | `preview_html` | Write `html_content` to a temp `.html` file in the system temp dir (user-supplied `file_name` reduced to its last path segment; default `preview_<timestamp>.html`), launch it in the default browser with the same detached opener, and leave the file on disk. Returns `{success: true, path, message}`. |
207
+
208
+ ### Document & database tools (3)
209
+
210
+ | Tool | Purpose |
211
+ |---|---|
212
+ | `read_document` | Read text + metadata from a PDF (pdf-parse) or DOCX (mammoth) file; other formats return a clear error. |
213
+ | `query_database` | Run a read-only SQL query against a SQLite database via Node's built-in `node:sqlite` (no native dependency); write statements (INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/REPLACE) are rejected up-front. |
214
+ | `analyze_project` | Run project-wide linting: auto-detects the project's linter (npm `lint` script → eslint → pylint) and runs it with a 60 s timeout, returning the report (capped at 10,000 chars). |
215
+
216
+ ### Web tools (3)
217
+
218
+ Ported from the reference Beledarian plugin (the DuckDuckGo chain and the Wikipedia API are near-verbatim; the browser legs share the single puppeteer launcher above).
219
+
220
+ | Tool | Purpose |
221
+ |---|---|
222
+ | `web_search` | Multi-provider web search with a fallback chain (DuckDuckGo fetch → api → html, then Google, then Bing — the html/google/bing legs run in the headless browser); per-provider errors are collected, not fatal; optional `providers` list to force an order; results deduped by link. |
223
+ | `fetch_web_content` | Fetch a URL and return clean, text-based page content (title + body via html-to-text, 40,000-char cap). |
224
+ | `wikipedia_search` | Search Wikipedia via the MediaWiki API: top-3 pages + intro extracts (2,000-char cap each), optional `lang`. |
225
+
226
+ ### RAG tools (2)
227
+
228
+ Retrieval-augmented generation over local text or a fetched URL. Embeddings go through a local OpenAI-compatible `/v1/embeddings` endpoint (default: LM Studio on `127.0.0.1:1234`, model `text-embedding-nomic-embed-text-v1.5`; override with `MCP_RAG_EMBED_MODEL` / `MCP_RAG_EMBED_URL`); Bearer token from `MCP_API_TOKEN_FILE` when present (keyless otherwise).
229
+
230
+ | Tool | Purpose |
231
+ |---|---|
232
+ | `rag_local_files` | Recursively scan up to 50 text files under a directory (binary extensions skipped), chunk by paragraph, and return the top 10 chunks scoring above cosine similarity 0.4 against the query (`{file, score, content}`). |
233
+ | `rag_web_content` | Fetch a URL, convert it to plain text, and return the top 5 chunks most relevant to the query. |
234
+
235
+ ### Browser tools (4)
236
+
237
+ Headless Chrome-for-Testing via puppeteer-core (executable resolution: `MCP_CHROME_PATH` → standard puppeteer cache → legacy path). One **persistent session per server process**: `browser_session_open` replaces the existing session (closing its browser first), and the session holds its own instance so `web_search`'s legs can never close it mid-session.
238
+
239
+ | Tool | Purpose |
240
+ |---|---|
241
+ | `browser_session_open` | Open the persistent session, navigate (networkidle0, 30 s timeout), optional `wait_for_selector`, full page-text capture. |
242
+ | `browser_session_control` | Scripted actions on the active page (wait / click / type / press / select / hover / scroll / evaluate), page metadata / full-text reads, screenshots (optionally full page), fuzzy in-page find. |
243
+ | `browser_session_close` | Close the active session (idempotent). |
244
+ | `browser_open_page` | Stateless one-shot render: navigate, optional scripted actions + screenshot, return page text (5,000-char cap). Never touches the persistent session. |
245
+
246
+ ### Beledarian-compat aliases (9)
247
+
248
+ Drop-in names, parameter shapes, and return shapes for the reference Beledarian plugin's tools, so existing prompts and tool documentation keep working. Each alias delegates to this server's native handler (identical behavior and job-registry lifetime); the synchronous ones keep Beledarian's 60 s cap (the native `shell_*` tools cap at 28 s).
249
+
250
+ | Tool | Purpose |
251
+ |---|---|
252
+ | `execute_command` | Bounded synchronous execution — alias of `shell_run` with the 60 s cap and the same result shape (`{command, exitCode, stdout, stderr, timedOut, duration_ms, cwd}`). |
253
+ | `run_in_terminal` | Open a separate, visible, interactive `cmd /k` console window — alias of `shell_terminal`. |
254
+ | `run_test_command` | Test wrapper (`CI=true`), never errors on a failing test — alias of `shell_test`. |
255
+ | `run_background_command` | Start a background job with a descriptive name — alias of `shell_start`. |
256
+ | `check_background_command` | Job status, stdout/stderr tails, log-file path — alias of `shell_check`. |
257
+ | `cancel_background_command` | Kill the whole job process tree — alias of `shell_cancel`. |
258
+ | `run_python` | Run a Python snippet (temp `.py` + system Python) — alias of `shell_python` with the 60 s cap. |
259
+ | `run_javascript` | Run a JS/TS snippet in the **confined** Deno sandbox (`--allow-read=. --allow-write=.`, net/env/sys/run/ffi denied) — the reference plugin's `run_javascript` semantics; NOT an alias of `run_javascript_free`. |
260
+ | `save_memory` | Append a timestamped fact to the memory file (path: `MCP_MEMORY_FILE` → reference workspace `memory.md` → `<cwd>/memory.md`), creating it with a `# Long-Term Memory` header when missing. No enable gate. |
261
+
262
+ ### Sub-agent tool (1)
263
+
264
+ | Tool | Purpose |
265
+ |---|---|
266
+ | `consult_secondary_agent` | Delegate a task to a secondary agent loop running against a local model endpoint (default `http://localhost:1234/v1`, model `local-model` — the LM Studio Developer API). Tool calls are parsed out of prose, validated, and executed **in-process** against this server's own handlers (file ops confined to the sub-agent's working directory); code blocks from the final response are auto-saved. Returns `{response, generated_files, filesModified, handoff_message?}` or `{error}`. |
267
+
138
268
  ## Docker
139
269
 
140
270
  ```bat
@@ -188,14 +318,35 @@ The filesystem tools work in the image; the shell tools require a Windows host w
188
318
 
189
319
  **v0.2.20 — `run_javascript_free`:** unrestricted JS/TS code execution on the Deno runtime — full filesystem, network, env, child-process, and import permissions (no working-directory sandbox). Reuses the shell layer's spawn/capture machinery (temp `.ts` in the shell-jobs dir, whole-tree kill on timeout, 100 KB / 20 KB output caps). The Deno binary is auto-located (`$DENO_PATH` → LM Studio's bundled `.internal/utils/deno(.exe)` via CWD walk-up or user home → PATH); missing binary yields a clear error. Default 5 s timeout, max 60 s.
190
320
 
321
+ **v0.2.21 — git tools:** six new tools (`git_status`, `git_diff`, `git_commit`, `git_log`, `git_add`, `git_checkout`) that spawn the git CLI against the repo at the server process's current working directory (no working-directory parameter; path arguments resolve against it). No new dependency. Failure (e.g. working directory not inside a git repository) returns a clear `{error}` string, mirroring the reference Beledarian tool shapes ({diff}, {history}, {success, ...}).
322
+
323
+ **v0.2.22 — GitHub tools:** eight new tools (`gh_auth`, `gh_create_issue`, `gh_list_issues`, `gh_view_comments`, `gh_create_pr`, `gh_list_prs`, `gh_view_pr_diff`, `gh_push`) that spawn the `gh` CLI (and `git push` for `gh_push`) against the repo at the server process's current working directory — no new dependency, no working-directory parameter (gh resolves the repo from the CWD's git remote, as the reference Beledarian tools do). `gh` is found on PATH (the reference's `checkGhInstalled`) with a documented fallback to the known portable install `<home>\tools\gh\bin\gh.exe`. Long issue/PR bodies go via a temp `--body-file` in the working directory (always cleaned up); a failed `gh auth status` opens a detached `cmd /k` terminal for interactive `gh auth login`. Failure (missing gh, bad remote/branch, unauthenticated) returns a clear `{error}` string, mirroring the reference tool shapes ({success, url}, {issues}, {pull_requests}, {comments}, {diff}).
324
+
325
+ **v0.2.23 — system tools:** six new tools (`get_system_info`, `read_clipboard`, `write_clipboard`, `send_notification`, `open_file`, `preview_html`) ported from the reference Beledarian `toolsProvider` (same names, parameter shapes, and return shapes): OS/CPU/memory info via the Node `os` module; clipboard read/write with the reference's platform branches (PowerShell Get-Clipboard / Set-Clipboard on Windows, pbpaste / pbcopy / xclip elsewhere), its 5-second race timeout, its base64 `Set-Clipboard` approach (no escaping issues), and its empty-clipboard-is-empty-content behavior; desktop notifications via `node-notifier` (the only new runtime dependency — Windows toasts through the bundled snoretoast, fire-and-forget `sound: true, wait: false`); `open_file` (the Beledarian-named `launch_file` equivalent: same `target` parameter, policy, and internal launch helper, reference success shape); and `preview_html` (temp `.html` in the system temp dir, launched in the default browser, temp path returned). `openerPath()` in `launch_file/handler.ts` gained `export` (no behavior change) so `preview_html` reuses the exact same opener resolution.
326
+
327
+ **v0.2.24 — document, database, and project tools:** `read_document` (PDF via pdf-parse 2.x + DOMMatrix polyfill; DOCX via mammoth), `query_database` (read-only SQLite via Node's built-in `node:sqlite` — no native dependency; write statements rejected up-front), `analyze_project` (lint detection: npm `lint` script → eslint → pylint; 60 s timeout; 10,000-char report cap).
328
+
329
+ **v0.2.25 — web tools:** `web_search` (DuckDuckGo fetch/api/html → Google → Bing fallback chain, ported near-verbatim from the reference; browser legs share the single puppeteer launcher), `fetch_web_content` (html-to-text clean page text, 40,000-char cap), `wikipedia_search` (MediaWiki API, top 3 + intro extracts). New deps: `duck-duck-scrape`, `puppeteer-core`.
330
+
331
+ **v0.2.26 — RAG tools:** `rag_local_files` / `rag_web_content` — paragraph chunking, cosine similarity > 0.4, top 10 / top 5; embeddings via the local `/v1/embeddings` endpoint (default model `text-embedding-nomic-embed-text-v1.5`; `MCP_RAG_EMBED_MODEL` / `MCP_RAG_EMBED_URL` overrides). No new deps (node fetch).
332
+
333
+ **v0.2.27 — browser tools:** `browser_session_open` / `browser_session_control` / `browser_session_close` (one persistent session per server process) + stateless `browser_open_page`; scripted actions, screenshots, fuzzy find; ported `browserActions.ts` / `fuzzySearch.ts` with unit tests. No new deps beyond v0.2.25's `puppeteer-core`.
334
+
335
+ **v0.2.28 — Beledarian-compat aliases:** `save_memory` + 8 aliases (`execute_command`, `run_in_terminal`, `run_test_command`, `run_background_command`, `check_background_command`, `cancel_background_command`, `run_python`, `run_javascript`) — drop-in names/shapes for the reference plugin's tools, delegating to native handlers with the reference's 60 s synchronous cap. No new deps.
336
+
337
+ **v0.2.29 — sub-agent tool:** `consult_secondary_agent` — delegated agent loop against the local model endpoint (default `http://localhost:1234/v1`, model `local-model`); tool calls parsed from prose, validated, and executed in-process against the fork's own handlers (file ops confined to the sub-agent's CWD); handoff-message extraction + auto-save of final code blocks. No new deps.
338
+
339
+ **Publish portability (v0.2.29):** the Chrome-for-Testing executable is resolved as `MCP_CHROME_PATH` → standard puppeteer cache (`<home>/.cache/puppeteer/chrome/`, highest version) → legacy path; `MCP_API_TOKEN_FILE` selects the API-token file for RAG + sub-agent Bearer auth; `MCP_RAG_EMBED_URL` selects the embeddings endpoint; the `save_memory` path is `MCP_MEMORY_FILE` → reference workspace → `<cwd>/memory.md`. With every env var unset, behavior on the reference machine is unchanged.
340
+
191
341
  ## Security
192
342
 
193
343
  - The filesystem tools are confined to the allowed directories passed at startup (and their subtrees).
194
344
  - The shell tools are **not** confined: anything the server process's user can execute will run, including against reachable network shares. Grant this server the same trust you would grant an unsandboxed shell.
195
345
  - `launch_file` targets are confined to the allowed directories (for paths), but the opened application runs with the full rights of the server user — like anything you double-click.
196
346
  - `run_javascript_free` is **not** confined: the snippet runs as an unsandboxed Deno process with the full rights of the server user (filesystem, network, processes). Grant this server the same trust you would grant an unsandboxed shell.
197
- - The server communicates over stdio and opens no network endpoints of its own.
347
+ - `consult_secondary_agent` delegates to a local model endpoint (default `http://localhost:1234/v1`); the sub-agent's file tools are confined to its working directory (default: the server's CWD, override with `MCP_SUBAGENT_CWD`), and its tool calls execute in-process against this server's own handlers.
348
+ - The server communicates over stdio and opens no network listeners of its own; the web, RAG, browser, and sub-agent tools make outbound calls to the endpoints documented above.
198
349
 
199
350
  ## License
200
351
 
201
- MIT — see [LICENSE](./LICENSE). This project derives from MIT-licensed code (Anthropic, PBC; fabienvauchelles); all copyright notices are in the LICENSE file.
352
+ MIT — see [LICENSE](./LICENSE). This project derives from MIT-licensed code (Anthropic, PBC; fabienvauchelles; Laurin Feulner / Beledarian); all copyright notices are in the LICENSE file.
@@ -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
+ }