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,209 @@
1
+ // git/handler.ts — git CLI wrappers for the 6 git_* tools (MCP filesystem fork).
2
+ //
3
+ // Design: spawn the `git` binary directly (git is on PATH; no new dependency).
4
+ // Every tool operates against the repo at the server process's current working
5
+ // directory (process.cwd()) — the same "current working directory context" the
6
+ // shell tools use — and takes NO working-directory parameter (matching the
7
+ // reference Beledarian tools). Path arguments are resolved relative to that
8
+ // directory. All argument values (messages, branch names, file paths) are passed
9
+ // as individual argv elements (no shell), so quotes / Unicode / special
10
+ // characters pass through verbatim.
11
+ //
12
+ // Return shape mirrors the reference implementation: each handler returns a JSON
13
+ // text string carrying the reference's fields ({error} on failure, {diff},
14
+ // {history}, {success, ...}) instead of throwing, so a working directory that is
15
+ // not inside a git repository yields a clear error string rather than a crash.
16
+ import { spawn } from "child_process";
17
+ import nodePath from "path";
18
+ /** Run `git <args>` with the given cwd. Never rejects; spawn failures resolve with code -1. */
19
+ function runGit(args, cwd) {
20
+ return new Promise((resolve) => {
21
+ let child;
22
+ try {
23
+ child = spawn("git", args, { cwd });
24
+ }
25
+ catch (e) {
26
+ resolve({ code: -1, stdout: "", stderr: e instanceof Error ? e.message : String(e) });
27
+ return;
28
+ }
29
+ let stdout = "";
30
+ let stderr = "";
31
+ let settled = false;
32
+ const finish = (code) => {
33
+ if (settled)
34
+ return;
35
+ settled = true;
36
+ resolve({ code, stdout, stderr });
37
+ };
38
+ if (child.stdout) {
39
+ child.stdout.on("data", (d) => {
40
+ stdout += d.toString("utf8");
41
+ });
42
+ }
43
+ if (child.stderr) {
44
+ child.stderr.on("data", (d) => {
45
+ stderr += d.toString("utf8");
46
+ });
47
+ }
48
+ child.on("error", (e) => {
49
+ // e.g. ENOENT when the git binary cannot be found on PATH
50
+ stderr += (stderr ? "\n" : "") + (e.message || String(e));
51
+ finish(-1);
52
+ });
53
+ child.on("close", (code) => finish(code === null ? -1 : code));
54
+ });
55
+ }
56
+ /** JSON error text matching the reference shape: {error: "Git <what> failed: <detail>"}. */
57
+ function failText(out, what) {
58
+ const detail = out.stderr.trim() || out.stdout.trim() || `git exited with code ${out.code}`;
59
+ return JSON.stringify({ error: `Git ${what} failed: ${detail}` });
60
+ }
61
+ /** The repo context for all git_* tools: the server process's current working directory. */
62
+ function gitCwd() {
63
+ return process.cwd();
64
+ }
65
+ export async function handleGitStatus() {
66
+ const dir = gitCwd();
67
+ const st = await runGit(["status", "--porcelain=v1", "--branch"], dir);
68
+ if (st.code !== 0) {
69
+ return failText(st, "status");
70
+ }
71
+ const lines = st.stdout.replace(/\r/g, "").split("\n");
72
+ let current = null;
73
+ let tracking = null;
74
+ let ahead = 0;
75
+ let behind = 0;
76
+ const staged = [];
77
+ const notStaged = [];
78
+ const untracked = [];
79
+ const files = [];
80
+ for (let i = 0; i < lines.length; i++) {
81
+ const line = lines[i];
82
+ if (i === 0 && line.startsWith("## ")) {
83
+ // Header: "## branch", "## branch...upstream", "## branch...upstream [ahead 1, behind 2]",
84
+ // or "## HEAD (no branch)" (detached HEAD).
85
+ let rest = line.slice(3);
86
+ const bracket = / \[([^\]]*)\]$/.exec(rest);
87
+ if (bracket) {
88
+ const am = /ahead (\d+)/.exec(bracket[1]);
89
+ const bm = /behind (\d+)/.exec(bracket[1]);
90
+ if (am)
91
+ ahead = parseInt(am[1], 10);
92
+ if (bm)
93
+ behind = parseInt(bm[1], 10);
94
+ rest = rest.slice(0, rest.length - bracket[0].length);
95
+ }
96
+ const sep = rest.indexOf("...");
97
+ if (sep >= 0) {
98
+ const left = rest.slice(0, sep);
99
+ current = left === "HEAD" ? null : left;
100
+ tracking = rest.slice(sep + 3) || null;
101
+ }
102
+ else if (rest.startsWith("HEAD (")) {
103
+ current = null;
104
+ tracking = null;
105
+ }
106
+ else {
107
+ current = rest || null;
108
+ tracking = null;
109
+ }
110
+ continue;
111
+ }
112
+ if (!line)
113
+ continue;
114
+ const index = line[0];
115
+ const working = line[1];
116
+ let p = line.slice(3);
117
+ // Rename/copy entries: "R old -> new" / "C old -> new" — report the destination.
118
+ if (index === "R" || index === "C") {
119
+ const arrow = p.indexOf(" -> ");
120
+ if (arrow >= 0)
121
+ p = p.slice(arrow + 4);
122
+ }
123
+ files.push({ path: p, index, working_dir: working });
124
+ if (index === "?" && working === "?") {
125
+ untracked.push(p);
126
+ }
127
+ else {
128
+ if (index !== " " && index !== "?")
129
+ staged.push(p);
130
+ if (working !== " " && working !== "?")
131
+ notStaged.push(p);
132
+ }
133
+ }
134
+ const isClean = staged.length === 0 && notStaged.length === 0 && untracked.length === 0;
135
+ return JSON.stringify({ current, tracking, ahead, behind, isClean, staged, notStaged, untracked, files });
136
+ }
137
+ export async function handleGitDiff(file_path, cached) {
138
+ const dir = gitCwd();
139
+ const args = ["diff"];
140
+ if (cached)
141
+ args.push("--cached");
142
+ if (file_path) {
143
+ args.push("--", nodePath.resolve(dir, file_path));
144
+ }
145
+ const d = await runGit(args, dir);
146
+ if (d.code !== 0) {
147
+ return failText(d, "diff");
148
+ }
149
+ const text = d.stdout.replace(/\n$/, "");
150
+ return JSON.stringify({ diff: text.length > 0 ? text : "No changes." });
151
+ }
152
+ export async function handleGitCommit(message) {
153
+ const dir = gitCwd();
154
+ // Pre-image stat of what is staged (mirrors the reference's {changed, insertions, deletions} summary).
155
+ const summary = { changed: 0, insertions: 0, deletions: 0 };
156
+ const pre = await runGit(["diff", "--cached", "--shortstat"], dir);
157
+ if (pre.code === 0 && pre.stdout.trim()) {
158
+ const mf = /(\d+) files? changed/.exec(pre.stdout);
159
+ if (mf)
160
+ summary.changed = parseInt(mf[1], 10);
161
+ const mi = /(\d+) insertions?\(\+\)/.exec(pre.stdout);
162
+ if (mi)
163
+ summary.insertions = parseInt(mi[1], 10);
164
+ const md = /(\d+) deletions?\(-\)/.exec(pre.stdout);
165
+ if (md)
166
+ summary.deletions = parseInt(md[1], 10);
167
+ }
168
+ // Standard git behavior: commit only what is staged.
169
+ const c = await runGit(["commit", "-m", message], dir);
170
+ if (c.code !== 0) {
171
+ return failText(c, "commit");
172
+ }
173
+ return JSON.stringify({ success: true, summary });
174
+ }
175
+ export async function handleGitLog(max_count) {
176
+ const dir = gitCwd();
177
+ const n = typeof max_count === "number" && Number.isFinite(max_count) && max_count > 0
178
+ ? Math.floor(max_count)
179
+ : 10;
180
+ const l = await runGit(["log", "-n", String(n), "--pretty=format:%h %s (%an, %ar)"], dir);
181
+ if (l.code !== 0) {
182
+ return failText(l, "log");
183
+ }
184
+ const history = l.stdout
185
+ .split("\n")
186
+ .map((s) => s.trimEnd())
187
+ .filter((s) => s.length > 0);
188
+ return JSON.stringify({ history });
189
+ }
190
+ export async function handleGitAdd(paths) {
191
+ const dir = gitCwd();
192
+ const args = paths && paths.length > 0
193
+ ? ["add", "--", ...paths.map((p) => nodePath.resolve(dir, p))]
194
+ : ["add", "."];
195
+ const a = await runGit(args, dir);
196
+ if (a.code !== 0) {
197
+ return failText(a, "add");
198
+ }
199
+ return JSON.stringify({ success: true, message: "Files staged successfully." });
200
+ }
201
+ export async function handleGitCheckout(branch_name, create_new) {
202
+ const dir = gitCwd();
203
+ const args = create_new ? ["checkout", "-b", branch_name] : ["checkout", branch_name];
204
+ const c = await runGit(args, dir);
205
+ if (c.code !== 0) {
206
+ return failText(c, "checkout");
207
+ }
208
+ return JSON.stringify({ success: true, message: `Switched to branch '${branch_name}'.` });
209
+ }
@@ -0,0 +1,23 @@
1
+ // git/schema.ts — argument schemas for the 6 git_* tools.
2
+ // (MCP filesystem fork; thin git CLI wrappers that operate against the repo at the
3
+ // server process's current working directory — no working-directory parameter, same
4
+ // "current working directory context" as the shell tools.)
5
+ import { z } from "zod";
6
+ export const GitStatusArgsSchema = z.object({});
7
+ export const GitDiffArgsSchema = z.object({
8
+ file_path: z.string().optional().describe("Optional: Path to specific file to diff."),
9
+ cached: z.boolean().optional().describe("Optional: Show staged changes only (git diff --cached)."),
10
+ });
11
+ export const GitCommitArgsSchema = z.object({
12
+ message: z.string(),
13
+ });
14
+ export const GitLogArgsSchema = z.object({
15
+ max_count: z.number().optional().describe("Max number of commits to return (default: 10)"),
16
+ });
17
+ export const GitAddArgsSchema = z.object({
18
+ paths: z.array(z.string()).optional().describe("Optional: Specific file paths to stage. If omitted, stages all changes."),
19
+ });
20
+ export const GitCheckoutArgsSchema = z.object({
21
+ branch_name: z.string().describe("Name of the branch to checkout."),
22
+ create_new: z.boolean().optional().default(false).describe("If true, creates the branch if it doesn't exist (like git checkout -b)."),
23
+ });
@@ -51,7 +51,9 @@ const URI_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]+:/;
51
51
  * Only the opener executable changes; the target still passes the full
52
52
  * URL/path policy below.
53
53
  */
54
- function openerPath() {
54
+ // Exported (v0.2.23) so the system tools (preview_html) can reuse the exact
55
+ // same opener resolution — no behavior change.
56
+ export function openerPath() {
55
57
  if (process.platform !== "win32") {
56
58
  throw new Error("launch_file is supported only on Windows");
57
59
  }
@@ -0,0 +1,27 @@
1
+ // query_database/handler.ts — read-only SQLite queries (MCP filesystem fork).
2
+ //
3
+ // Port of the Beledarian query_database tool (beledarians-lm-studio-tools
4
+ // src/toolsProvider.ts), with one substitution: the reference uses the native
5
+ // better-sqlite3 module; this fork runs on system Node which ships the built-in
6
+ // node:sqlite module (DatabaseSync), so no native dependency is added. Same
7
+ // behavior: naive write-statement block, open READONLY, prepare + all(), close,
8
+ // return { results } or { error }.
9
+ import { DatabaseSync } from "node:sqlite";
10
+ import { validatePath } from "../helpers/path.js";
11
+ export async function handleQueryDatabase(dbPath, query, allowedDirectories) {
12
+ const fpath = await validatePath(dbPath, allowedDirectories);
13
+ // Safety: Attempt to block write operations (naive check)
14
+ if (/^\s*(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE)\b/i.test(query)) {
15
+ return JSON.stringify({ error: "Only SELECT/read queries are allowed for safety." });
16
+ }
17
+ try {
18
+ const db = new DatabaseSync(fpath, { readOnly: true });
19
+ const stmt = db.prepare(query);
20
+ const results = stmt.all();
21
+ db.close();
22
+ return JSON.stringify({ results });
23
+ }
24
+ catch (e) {
25
+ return JSON.stringify({ error: `Database query failed: ${e instanceof Error ? e.message : String(e)}` });
26
+ }
27
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ export const QueryDatabaseArgsSchema = z.object({
3
+ db_path: z.string(),
4
+ query: z.string(),
5
+ });
@@ -0,0 +1,103 @@
1
+ // rag/handler.ts — RAG tools (MCP filesystem fork).
2
+ //
3
+ // Port of the Beledarian rag_local_files (beledarians-lm-studio-tools
4
+ // src/toolsProvider.ts L2389-2463) and rag_web_content (L1706-1754) tools.
5
+ // Same behavior throughout: rag_local_files recursively scans up to 50 text
6
+ // files, chunks on blank lines (>20 chars), keeps chunks scoring above
7
+ // cosine similarity 0.4, and returns the top 10 with {file, score, content}
8
+ // (score as a 3-decimal string, exactly like the reference). rag_web_content
9
+ // fetches the URL, converts to plain text with html-to-text (the same
10
+ // options the fork's fetch_web_content tool uses), and returns the top 5
11
+ // chunks from performRagOnText. Embedding goes through the LM Studio
12
+ // /v1/embeddings endpoint (see helpers.ts) instead of the SDK client.
13
+ import fs from "fs/promises";
14
+ import path from "path";
15
+ import { validatePath } from "../helpers/path.js";
16
+ import { chunkText, cosineSimilarity, embedStrings, performRagOnText } from "./helpers.js";
17
+ export async function handleRagLocalFiles(query, searchPath, filePattern, allowedDirectories) {
18
+ try {
19
+ const targetDir = await validatePath(searchPath ?? ".", allowedDirectories);
20
+ const entries = await fs.readdir(targetDir, { recursive: true, withFileTypes: true });
21
+ const textFiles = entries.filter((e) => e.isFile() && !e.name.match(/\.(png|jpg|jpeg|gif|ico|exe|dll|bin)$/i));
22
+ // Filter by pattern if provided
23
+ const filteredFiles = filePattern
24
+ ? textFiles.filter((e) => e.name.includes(filePattern) || path.join(e.parentPath, e.name).includes(filePattern))
25
+ : textFiles;
26
+ // Limit to avoid massive reads. In a real 'Gemini Flow' robust
27
+ // implementation, we'd use an index. Here we'll read top 50 files max to be safe.
28
+ const filesToScan = filteredFiles.slice(0, 50);
29
+ let allChunks = [];
30
+ const [queryEmbedding] = await embedStrings([query]);
31
+ for (const file of filesToScan) {
32
+ try {
33
+ const fullPath = path.join(file.parentPath, file.name);
34
+ const content = await fs.readFile(fullPath, "utf-8");
35
+ // reuse chunking logic
36
+ const chunks = chunkText(content);
37
+ if (chunks.length === 0)
38
+ continue;
39
+ // Batch embed chunks for this file
40
+ const chunkEmbeddings = await embedStrings(chunks);
41
+ chunks.forEach((chunk, i) => {
42
+ const score = cosineSimilarity(queryEmbedding, chunkEmbeddings[i]);
43
+ if (score > 0.4) { // Threshold
44
+ allChunks.push({ chunk, score, file: file.name });
45
+ }
46
+ });
47
+ }
48
+ catch {
49
+ // ignore read errors
50
+ }
51
+ }
52
+ // Sort all chunks
53
+ allChunks.sort((a, b) => b.score - a.score);
54
+ return JSON.stringify({
55
+ query,
56
+ results: allChunks.slice(0, 10).map((c) => ({
57
+ file: c.file,
58
+ score: c.score.toFixed(3),
59
+ content: c.chunk,
60
+ })),
61
+ });
62
+ }
63
+ catch (error) {
64
+ return JSON.stringify({
65
+ error: `Local RAG failed: ${error instanceof Error ? error.message : String(error)}`,
66
+ });
67
+ }
68
+ }
69
+ export async function handleRagWebContent(url, query) {
70
+ try {
71
+ // 1. Fetch content
72
+ const response = await fetch(url);
73
+ if (!response.ok) {
74
+ throw new Error(`HTTP error! status: ${response.status}`);
75
+ }
76
+ let text = await response.text();
77
+ // Same text extraction the fork's fetch_web_content tool uses
78
+ const { compile } = await import("html-to-text");
79
+ const compiledConvert = compile({
80
+ wordwrap: false,
81
+ selectors: [
82
+ { selector: "a", options: { ignoreHref: true } },
83
+ { selector: "img", format: "skip" },
84
+ ],
85
+ });
86
+ text = compiledConvert(text);
87
+ if (text.length === 0) {
88
+ return JSON.stringify({ error: "Could not extract any text from the URL." });
89
+ }
90
+ // 2. Perform RAG
91
+ const ragResults = await performRagOnText(text, query);
92
+ return JSON.stringify({
93
+ url: url,
94
+ query: query,
95
+ relevant_chunks: ragResults,
96
+ });
97
+ }
98
+ catch (error) {
99
+ return JSON.stringify({
100
+ error: `Failed during RAG web search: ${error instanceof Error ? error.message : String(error)}`,
101
+ });
102
+ }
103
+ }
@@ -0,0 +1,126 @@
1
+ // rag/helpers.ts — shared RAG helpers (MCP filesystem fork).
2
+ //
3
+ // Port of the Beledarian RAG core (beledarians-lm-studio-tools
4
+ // src/toolsProvider.ts: cosineSimilarity L76-84, performRagOnText L86-110),
5
+ // with one substitution: the reference embeds through the LM Studio SDK
6
+ // client; this fork runs on plain Node, so embedding is a fetch against
7
+ // LM Studio's OpenAI-compatible API: POST http://127.0.0.1:1234/v1/embeddings
8
+ // with JSON { model, input: [strings] } and (when a token is available)
9
+ // Authorization: Bearer <token>. The token is read from MCP_API_TOKEN_FILE
10
+ // (or, when unset, the reference machine's overnight pipeline token file);
11
+ // if that file is missing or empty the request is sent keyless. The endpoint
12
+ // is MCP_RAG_EMBED_URL when set. All thresholds, chunking, and top-N
13
+ // behavior are identical
14
+ // to the reference.
15
+ import fs from "fs";
16
+ const DEFAULT_EMBEDDINGS_URL = "http://127.0.0.1:1234/v1/embeddings";
17
+ // Reference machine's overnight pipeline token file (kept for back-compat;
18
+ // on other machines the file is simply absent -> keyless request).
19
+ const LEGACY_TOKEN_FILE = "C:/Users/Gerar/.beledarians-llm-toolbox/workspace/overnight/lm_api_token.txt";
20
+ // Env overrides (v0.2.29 publish portability). When unset, behavior on the
21
+ // reference machine is unchanged.
22
+ function getEmbeddingsUrl() {
23
+ const o = process.env.MCP_RAG_EMBED_URL;
24
+ return o && o.trim() ? o.trim() : DEFAULT_EMBEDDINGS_URL;
25
+ }
26
+ function getTokenFile() {
27
+ const o = process.env.MCP_API_TOKEN_FILE;
28
+ return o && o.trim() ? o.trim() : LEGACY_TOKEN_FILE;
29
+ }
30
+ const DEFAULT_EMBED_MODEL = "text-embedding-nomic-embed-text-v1.5";
31
+ // Read the API token fresh on every call (small file; rotation stays live).
32
+ function readApiKey() {
33
+ try {
34
+ const token = fs.readFileSync(getTokenFile(), "utf-8").trim();
35
+ return token.length > 0 ? token : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ // Env override for the embedding model id (LM Studio resolves model ids the
42
+ // same way for the /v1/embeddings endpoint).
43
+ function getEmbedModel() {
44
+ const override = process.env.MCP_RAG_EMBED_MODEL;
45
+ return override && override.trim().length > 0 ? override.trim() : DEFAULT_EMBED_MODEL;
46
+ }
47
+ // Embed a batch of strings. Response shape: {data: [{embedding: number[], index}]}.
48
+ // Results are mapped back to input order by `index`. One retry (2 attempts)
49
+ // on transient network/API errors with a short backoff — the first embedding
50
+ // call of a session can trigger an auto-load of the embedding model on its
51
+ // own llama-server, which takes a few seconds.
52
+ export async function embedStrings(strings) {
53
+ if (strings.length === 0)
54
+ return [];
55
+ let lastError = null;
56
+ for (let attempt = 0; attempt < 2; attempt++) {
57
+ try {
58
+ const headers = {
59
+ "Content-Type": "application/json",
60
+ };
61
+ const token = readApiKey();
62
+ if (token)
63
+ headers["Authorization"] = `Bearer ${token}`;
64
+ const res = await fetch(getEmbeddingsUrl(), {
65
+ method: "POST",
66
+ headers,
67
+ body: JSON.stringify({ model: getEmbedModel(), input: strings }),
68
+ });
69
+ if (!res.ok) {
70
+ const body = await res.text().catch(() => "");
71
+ throw new Error(`embeddings HTTP ${res.status}: ${body.slice(0, 300)}`);
72
+ }
73
+ const data = await res.json();
74
+ if (!data || !Array.isArray(data.data)) {
75
+ throw new Error("embeddings response missing data array");
76
+ }
77
+ const byIndex = new Map();
78
+ for (const item of data.data) {
79
+ byIndex.set(item.index, item.embedding);
80
+ }
81
+ return strings.map((_, i) => {
82
+ const e = byIndex.get(i);
83
+ if (!Array.isArray(e) || e.length === 0) {
84
+ throw new Error(`embeddings response missing embedding for index ${i}`);
85
+ }
86
+ return e;
87
+ });
88
+ }
89
+ catch (e) {
90
+ lastError = e;
91
+ await new Promise((r) => setTimeout(r, 2000));
92
+ }
93
+ }
94
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
95
+ }
96
+ // Helper function for cosine similarity (identical to the reference).
97
+ export function cosineSimilarity(vecA, vecB) {
98
+ const dotProduct = vecA.reduce((acc, val, i) => acc + val * vecB[i], 0);
99
+ const magA = Math.sqrt(vecA.reduce((acc, val) => acc + val * val, 0));
100
+ const magB = Math.sqrt(vecB.reduce((acc, val) => acc + val * val, 0));
101
+ if (magA === 0 || magB === 0) {
102
+ return 0;
103
+ }
104
+ return dotProduct / (magA * magB);
105
+ }
106
+ // Paragraph-based chunking (identical to the reference): split on blank
107
+ // lines, keep chunks with more than 20 non-whitespace characters.
108
+ export function chunkText(text) {
109
+ return text.split(/\n\s*\n/).filter((chunk) => chunk.trim().length > 20);
110
+ }
111
+ // Main RAG-on-text helper (identical behavior to the reference:
112
+ // chunk, embed query + chunks, cosine similarity, sort, top 5).
113
+ export async function performRagOnText(text, query) {
114
+ const chunks = chunkText(text);
115
+ if (chunks.length === 0) {
116
+ return [{ chunk: text.substring(0, 4000), score: 1 }];
117
+ }
118
+ const [queryEmbedding] = await embedStrings([query]);
119
+ const chunkEmbeddings = await embedStrings(chunks);
120
+ const similarities = chunkEmbeddings.map((chunkEmb, i) => ({
121
+ chunk: chunks[i],
122
+ score: cosineSimilarity(queryEmbedding, chunkEmb),
123
+ }));
124
+ similarities.sort((a, b) => b.score - a.score);
125
+ return similarities.slice(0, 5); // Return top 5
126
+ }
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ export const RagLocalFilesArgsSchema = z.object({
3
+ query: z.string().describe("The search query to match file contents against"),
4
+ path: z
5
+ .string()
6
+ .optional()
7
+ .describe("Root directory to search (default: the server process's working directory). Must be within an allowed directory."),
8
+ file_pattern: z
9
+ .string()
10
+ .optional()
11
+ .describe("File pattern to include (e.g. '.ts', 'src/'). Default: all text files."),
12
+ });
13
+ export const RagWebContentArgsSchema = z.object({
14
+ url: z.string().describe("The URL to fetch"),
15
+ query: z.string().describe("The query to find relevant content for"),
16
+ });
@@ -0,0 +1,61 @@
1
+ // read_document/handler.ts — read content from PDF or DOCX files (MCP filesystem fork).
2
+ //
3
+ // Port of the Beledarian read_document tool (beledarians-lm-studio-tools
4
+ // src/toolsProvider.ts). PDF via pdf-parse v2 class-based API
5
+ // (new PDFParse({ data }) -> getText() / getInfo() / destroy()); DOCX via
6
+ // mammoth extractRawText. The DOMMatrix polyfill is required by pdf-parse v2
7
+ // (pdfjs) in a bare Node environment and is ported VERBATIM from the reference.
8
+ import { readFile } from "fs/promises";
9
+ import { validatePath } from "../helpers/path.js";
10
+ // Polyfill DOMMatrix for pdf-parse v2 (required for node environment)
11
+ function ensureDomMatrix() {
12
+ if (typeof global.DOMMatrix === "undefined") {
13
+ global.DOMMatrix = class DOMMatrix {
14
+ constructor(arg) {
15
+ this.a = 1;
16
+ this.b = 0;
17
+ this.c = 0;
18
+ this.d = 1;
19
+ this.e = 0;
20
+ this.f = 0;
21
+ if (Array.isArray(arg)) {
22
+ this.a = arg[0];
23
+ this.b = arg[1];
24
+ this.c = arg[2];
25
+ this.d = arg[3];
26
+ this.e = arg[4];
27
+ this.f = arg[5];
28
+ }
29
+ }
30
+ };
31
+ }
32
+ }
33
+ export async function handleReadDocument(filePath, allowedDirectories) {
34
+ const fpath = await validatePath(filePath, allowedDirectories);
35
+ const ext = fpath.split('.').pop()?.toLowerCase();
36
+ try {
37
+ if (ext === 'pdf') {
38
+ ensureDomMatrix();
39
+ // Dynamically import pdf-parse v2 (ESM equivalent of the reference's require)
40
+ const { PDFParse } = await import("pdf-parse");
41
+ const dataBuffer = await readFile(fpath);
42
+ // Use new class-based API
43
+ const parser = new PDFParse({ data: dataBuffer });
44
+ const textResult = await parser.getText();
45
+ const infoResult = await parser.getInfo(); // Optional: get metadata
46
+ await parser.destroy(); // Cleanup
47
+ return JSON.stringify({ content: textResult.text, metadata: infoResult.info });
48
+ }
49
+ else if (ext === 'docx') {
50
+ const mammoth = await import("mammoth");
51
+ const result = await mammoth.extractRawText({ path: fpath });
52
+ return JSON.stringify({ content: result.value, messages: result.messages });
53
+ }
54
+ else {
55
+ return JSON.stringify({ error: "Unsupported document format. Use read_file for text files." });
56
+ }
57
+ }
58
+ catch (e) {
59
+ return JSON.stringify({ error: `Failed to read document: ${e instanceof Error ? e.message : String(e)}` });
60
+ }
61
+ }
@@ -0,0 +1,4 @@
1
+ import { z } from "zod";
2
+ export const ReadDocumentArgsSchema = z.object({
3
+ file_path: z.string(),
4
+ });