@baitong-dev/bash-mcp 0.0.2 → 0.0.4

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,6 @@
1
+ - Fast file pattern matching tool that works with any codebase size
2
+ - Supports glob patterns like "**/*.js" or "src/**/*.ts"
3
+ - Returns matching file paths sorted by modification time
4
+ - Use this tool when you need to find files by name patterns
5
+ - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
6
+ - You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
@@ -0,0 +1,8 @@
1
+ - Fast content search tool that works with any codebase size
2
+ - Searches file contents using regular expressions
3
+ - Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
4
+ - Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
5
+ - Returns file paths and line numbers with at least one match sorted by modification time
6
+ - Use this tool when you need to find files containing specific patterns
7
+ - If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
8
+ - When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
@@ -0,0 +1 @@
1
+ Lists files and directories in a given path. The path parameter must be absolute; omit it to use the current workspace directory. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.
package/dist/bash.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ /**
3
+ * Environment variables to inherit by default, if an environment is not explicitly given.
4
+ */
5
+ export declare const DEFAULT_INHERITED_ENV_VARS: string[];
6
+ /**
7
+ * Returns a default environment object including only environment variables deemed safe to inherit.
8
+ */
9
+ export declare function getDefaultEnvs(): {};
10
+ export declare function registerBashTool(server: McpServer): void;
package/dist/bash.js ADDED
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DEFAULT_INHERITED_ENV_VARS = void 0;
7
+ exports.getDefaultEnvs = getDefaultEnvs;
8
+ exports.registerBashTool = registerBashTool;
9
+ const path_1 = __importDefault(require("path"));
10
+ const zod_1 = __importDefault(require("zod"));
11
+ const web_tree_sitter_1 = require("web-tree-sitter");
12
+ // @ts-ignore wasm
13
+ const web_tree_sitter_wasm_1 = __importDefault(require("web-tree-sitter/web-tree-sitter.wasm"));
14
+ // @ts-ignore wasm
15
+ const tree_sitter_bash_wasm_1 = __importDefault(require("tree-sitter-bash/tree-sitter-bash.wasm"));
16
+ const child_process_1 = require("child_process");
17
+ const bash_txt_1 = __importDefault(require("../descriptions/bash.txt"));
18
+ const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
19
+ const parser = (0, mcp_helpers_1.lazy)(async () => {
20
+ const treePath = (0, mcp_helpers_1.resolveWasm)(web_tree_sitter_wasm_1.default);
21
+ await web_tree_sitter_1.Parser.init({
22
+ locateFile() {
23
+ return treePath;
24
+ }
25
+ });
26
+ const bashPath = (0, mcp_helpers_1.resolveWasm)(tree_sitter_bash_wasm_1.default);
27
+ const bashLanguage = await web_tree_sitter_1.Language.load(bashPath);
28
+ const p = new web_tree_sitter_1.Parser();
29
+ p.setLanguage(bashLanguage);
30
+ return p;
31
+ });
32
+ /**
33
+ * Get the path to the bash executable
34
+ * @returns The path to the bash executable
35
+ */
36
+ const getBashPath = (0, mcp_helpers_1.lazy)(() => {
37
+ const shell = process.env.SHELL;
38
+ if (shell)
39
+ return shell;
40
+ // Fallback: check common Git Bash paths directly
41
+ if (process.platform === 'win32') {
42
+ const bash = (0, mcp_helpers_1.findGitBash)((0, mcp_helpers_1.getBundledBinaryPath)('bash'));
43
+ if (bash)
44
+ return bash;
45
+ throw new Error('Git Bash not found');
46
+ // return process.env.COMSPEC || "cmd.exe"
47
+ }
48
+ if (process.platform === 'darwin')
49
+ return '/bin/zsh';
50
+ const bash = Bun.which('bash');
51
+ if (bash)
52
+ return bash;
53
+ return '/bin/sh';
54
+ });
55
+ // =============================================================================
56
+ // Constants
57
+ // =============================================================================
58
+ // Default working directory
59
+ const DEFAULT_CWD = path_1.default.join(mcp_helpers_1.MCP_HOME_DIR, 'workspace');
60
+ // Grace period before SIGKILL (ms)
61
+ const SIGKILL_TIMEOUT_MS = 5000;
62
+ // Reserved characters for truncation ellipsis
63
+ const TRUNCATION_ELLIPSIS_RESERVE = 50;
64
+ // Exit code for timeout (shell convention)
65
+ const EXIT_CODE_TIMEOUT = 124;
66
+ // Sudo rejection message
67
+ const SUDO_REJECTION_MESSAGE = '[REJECTED] sudo commands cannot be executed. Please STOP and ask human user to run it for you!\n';
68
+ // Default max_output
69
+ const DEFAULT_MAX_OUTPUT = 30000;
70
+ const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
71
+ /**
72
+ * Check if a command contains sudo
73
+ * Matches: sudo at start, after semicolon, after &&, after ||, after |, after $(, after backtick
74
+ */
75
+ function containsSudo(command) {
76
+ // Match sudo as a standalone command (not part of another word like "pseudocode")
77
+ return /(?:^|[;&|`$()]\s*)sudo(?:\s|$)/m.test(command);
78
+ }
79
+ /**
80
+ * Environment variables to inherit by default, if an environment is not explicitly given.
81
+ */
82
+ exports.DEFAULT_INHERITED_ENV_VARS = process.platform === 'win32'
83
+ ? [
84
+ 'APPDATA',
85
+ 'HOMEDRIVE',
86
+ 'HOMEPATH',
87
+ 'LOCALAPPDATA',
88
+ 'PATH',
89
+ 'PROCESSOR_ARCHITECTURE',
90
+ 'SYSTEMDRIVE',
91
+ 'SYSTEMROOT',
92
+ 'TEMP',
93
+ 'TMPDIR',
94
+ 'USERNAME',
95
+ 'USERPROFILE',
96
+ 'PROGRAMFILES'
97
+ ]
98
+ : /* list inspired by the default env inheritance of sudo */
99
+ ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
100
+ /**
101
+ * Returns a default environment object including only environment variables deemed safe to inherit.
102
+ */
103
+ function getDefaultEnvs() {
104
+ const env = {};
105
+ for (const key of exports.DEFAULT_INHERITED_ENV_VARS) {
106
+ const value = process.env[key];
107
+ if (value === undefined) {
108
+ continue;
109
+ }
110
+ if (value.startsWith('()')) {
111
+ // Skip functions, which are a security risk.
112
+ continue;
113
+ }
114
+ env[key] = value;
115
+ }
116
+ return env;
117
+ }
118
+ /**
119
+ * Middle-truncate output to preserve beginning and end
120
+ */
121
+ function middleTruncate(text, maxLength) {
122
+ if (text.length <= maxLength)
123
+ return text;
124
+ const halfLength = Math.floor((maxLength - TRUNCATION_ELLIPSIS_RESERVE) / 2);
125
+ const start = text.slice(0, halfLength);
126
+ const end = text.slice(-halfLength);
127
+ const truncatedBytes = text.length - maxLength;
128
+ return `${start}\n\n... [truncated ${truncatedBytes} characters] ...\n\n${end}`;
129
+ }
130
+ const inputSchema = zod_1.default.object({
131
+ command: zod_1.default.string().min(1).describe('The command to execute'),
132
+ cwd: zod_1.default
133
+ .string()
134
+ .optional()
135
+ .describe(`The working directory to run the command in. Must be in ${DEFAULT_CWD}. Defaults to ${DEFAULT_CWD}. Use this instead of 'cd' commands.`), // 需要在DEFAULT_CWD下
136
+ timeout: zod_1.default
137
+ .number()
138
+ .min(1)
139
+ .max(600000)
140
+ .optional()
141
+ .describe(`Timeout in milliseconds (optional, default ${DEFAULT_TIMEOUT_MS}, max 600000)`),
142
+ description: zod_1.default
143
+ .string()
144
+ .describe('Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory "foo"'),
145
+ env: zod_1.default
146
+ .record(zod_1.default.string(), zod_1.default.string())
147
+ .optional()
148
+ .describe('Additional environment variables to set'),
149
+ maxOutput: zod_1.default
150
+ .number()
151
+ .min(1)
152
+ .max(1000000)
153
+ .optional()
154
+ .describe(`Maximum output length before middle-truncation (default ${DEFAULT_MAX_OUTPUT})`)
155
+ });
156
+ async function killTree(proc, opts) {
157
+ const pid = proc.pid;
158
+ if (!pid || opts?.exited?.())
159
+ return;
160
+ if (process.platform === 'win32') {
161
+ await new Promise(resolve => {
162
+ const killer = (0, child_process_1.spawn)('taskkill', ['/pid', String(pid), '/f', '/t'], { stdio: 'ignore' });
163
+ killer.once('exit', () => resolve());
164
+ killer.once('error', () => resolve());
165
+ });
166
+ return;
167
+ }
168
+ try {
169
+ process.kill(-pid, 'SIGTERM');
170
+ await Bun.sleep(SIGKILL_TIMEOUT_MS);
171
+ if (!opts?.exited?.()) {
172
+ process.kill(-pid, 'SIGKILL');
173
+ }
174
+ }
175
+ catch (_e) {
176
+ proc.kill('SIGTERM');
177
+ await Bun.sleep(SIGKILL_TIMEOUT_MS);
178
+ if (!opts?.exited?.()) {
179
+ proc.kill('SIGKILL');
180
+ }
181
+ }
182
+ }
183
+ /**
184
+ * Execute a bash command and return result
185
+ */
186
+ async function executeBash(options) {
187
+ const command = options.command.trim();
188
+ const cwd = options.cwd || DEFAULT_CWD;
189
+ const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
190
+ const maxOutput = options.maxOutput || DEFAULT_MAX_OUTPUT;
191
+ if (containsSudo(command)) {
192
+ throw Error(`${SUDO_REJECTION_MESSAGE}$ ${command}`);
193
+ }
194
+ if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, cwd, {
195
+ includeSelf: true
196
+ })) {
197
+ throw Error(`"${cwd}" must be in ${DEFAULT_CWD}`);
198
+ }
199
+ if (!(await (0, mcp_helpers_1.isDir)(cwd))) {
200
+ throw Error(`"${cwd}" must be a directory`);
201
+ }
202
+ const tree = await parser().then(p => p.parse(command));
203
+ if (!tree) {
204
+ throw new Error('Failed to parse command');
205
+ }
206
+ const directories = new Set();
207
+ // if (!Instance.containsPath(cwd)) directories.add(cwd)
208
+ const patterns = new Set();
209
+ // const always = new Set<string>()
210
+ let output = '';
211
+ for (const node of tree.rootNode.descendantsOfType('command')) {
212
+ if (!node)
213
+ continue;
214
+ // Get full command text including redirects if present
215
+ const commandText = node.parent?.type === 'redirected_statement' ? node.parent.text : node.text;
216
+ const commands = [];
217
+ for (let i = 0; i < node.childCount; i++) {
218
+ const child = node.child(i);
219
+ if (!child)
220
+ continue;
221
+ if (child.type !== 'command_name' &&
222
+ child.type !== 'word' &&
223
+ child.type !== 'string' &&
224
+ child.type !== 'raw_string' &&
225
+ child.type !== 'concatenation') {
226
+ continue;
227
+ }
228
+ commands.push(child.text);
229
+ }
230
+ // not an exhaustive list, but covers most common cases
231
+ if (['cd', 'rm', 'cp', 'mv', 'mkdir', 'touch', 'chmod', 'chown', 'cat'].includes(commands[0])) {
232
+ for (const arg of commands.slice(1)) {
233
+ if (arg.startsWith('-') || (commands[0] === 'chmod' && arg.startsWith('+'))) {
234
+ continue;
235
+ }
236
+ const resolved = await Bun.$ `realpath ${arg}`
237
+ .cwd(cwd)
238
+ .quiet()
239
+ .nothrow()
240
+ .text()
241
+ .then(x => x.trim());
242
+ if (resolved) {
243
+ // Git Bash on Windows returns Unix-style paths like /c/Users/...
244
+ const normalized = process.platform === 'win32' && resolved.match(/^\/[a-z]\//)
245
+ ? (0, mcp_helpers_1.gitBashToWindowsPath)(resolved)
246
+ : resolved;
247
+ if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, normalized, { includeSelf: true })) {
248
+ const dir = (await (0, mcp_helpers_1.isDir)(normalized)) ? normalized : path_1.default.dirname(normalized);
249
+ directories.add(dir);
250
+ }
251
+ }
252
+ }
253
+ }
254
+ else {
255
+ //
256
+ }
257
+ // cd covered by above check
258
+ if (commands.length && commands[0] !== 'cd') {
259
+ patterns.add(commandText);
260
+ // always.add(BashArity.prefix(command).join(' ') + ' *')
261
+ }
262
+ }
263
+ const mergedEnv = {
264
+ ...getDefaultEnvs(),
265
+ ...(0, mcp_helpers_1.getBundledBinaryEnvs)()
266
+ };
267
+ const proc = (0, child_process_1.spawn)(command, {
268
+ shell: getBashPath(),
269
+ cwd,
270
+ env: {
271
+ ...mergedEnv
272
+ },
273
+ stdio: ['ignore', 'pipe', 'pipe'],
274
+ detached: process.platform !== 'win32'
275
+ });
276
+ const append = (chunk) => {
277
+ output += chunk.toString();
278
+ };
279
+ proc.stdout?.on('data', append);
280
+ proc.stderr?.on('data', append);
281
+ let timedOut = false;
282
+ let exited = false;
283
+ const kill = () => killTree(proc, { exited: () => exited });
284
+ const timeoutTimer = setTimeout(() => {
285
+ timedOut = true;
286
+ void kill();
287
+ }, timeout + 100);
288
+ await new Promise((resolve, reject) => {
289
+ const cleanup = () => {
290
+ clearTimeout(timeoutTimer);
291
+ };
292
+ proc.once('exit', () => {
293
+ exited = true;
294
+ cleanup();
295
+ resolve();
296
+ });
297
+ proc.once('error', error => {
298
+ exited = true;
299
+ cleanup();
300
+ reject(error);
301
+ });
302
+ });
303
+ const resultMetadata = [];
304
+ if (timedOut) {
305
+ resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`);
306
+ }
307
+ if (resultMetadata.length > 0) {
308
+ output += '\n\n<bash_metadata>\n' + resultMetadata.join('\n') + '\n</bash_metadata>';
309
+ }
310
+ return {
311
+ title: options.description,
312
+ metadata: {
313
+ output: middleTruncate(output, maxOutput),
314
+ exit: proc.exitCode,
315
+ description: options.description
316
+ },
317
+ output
318
+ };
319
+ }
320
+ function registerBashTool(server) {
321
+ server.registerTool('bash', {
322
+ description: bash_txt_1.default.replaceAll('${directory}', DEFAULT_CWD)
323
+ .replaceAll('${maxLines}', String(2000))
324
+ .replaceAll('${maxBytes}', String(50 * 1024)),
325
+ inputSchema
326
+ }, async (args) => {
327
+ const result = await executeBash({ ...args });
328
+ return {
329
+ content: [{ type: 'text', text: JSON.stringify(result) }],
330
+ structuredContent: result
331
+ };
332
+ });
333
+ }
package/dist/glob.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerGlobTool(server: McpServer): void;
package/dist/glob.js ADDED
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerGlobTool = registerGlobTool;
40
+ const glob_txt_1 = __importDefault(require("../descriptions/glob.txt"));
41
+ const zod_1 = __importDefault(require("zod"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
44
+ const fs = __importStar(require("fs"));
45
+ const FILE_LIMIT = 100;
46
+ function registerGlobTool(server) {
47
+ server.registerTool('glob', {
48
+ description: glob_txt_1.default,
49
+ inputSchema: zod_1.default.object({
50
+ pattern: zod_1.default.string().describe('The glob pattern to match files against'),
51
+ path: zod_1.default
52
+ .string()
53
+ .optional()
54
+ .describe(`The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.`)
55
+ })
56
+ }, async (args) => {
57
+ if (!args.pattern) {
58
+ throw new Error('pattern is required');
59
+ }
60
+ let searchPath = args.path ?? mcp_helpers_1.MCP_WORKSPACE_DIR;
61
+ searchPath = path_1.default.isAbsolute(searchPath)
62
+ ? searchPath
63
+ : path_1.default.resolve(mcp_helpers_1.MCP_WORKSPACE_DIR, searchPath);
64
+ // await assertExternalDirectory(ctx, searchPath, { kind: 'directory' })
65
+ const limit = FILE_LIMIT;
66
+ const files = [];
67
+ let truncated = false;
68
+ const rgFiles = await mcp_helpers_1.Ripgrep.files({
69
+ cwd: searchPath,
70
+ glob: [args.pattern]
71
+ // signal: ctx.abort
72
+ });
73
+ for (const file of rgFiles) {
74
+ if (files.length >= limit) {
75
+ truncated = true;
76
+ break;
77
+ }
78
+ const full = path_1.default.resolve(searchPath, file);
79
+ const stats = fs.statSync(full, { throwIfNoEntry: false })?.mtime.getTime() ?? 0;
80
+ files.push({
81
+ path: full,
82
+ mtime: stats
83
+ });
84
+ }
85
+ files.sort((a, b) => b.mtime - a.mtime);
86
+ const output = [];
87
+ if (files.length === 0)
88
+ output.push('No files found');
89
+ if (files.length > 0) {
90
+ output.push(...files.map(f => f.path));
91
+ if (truncated) {
92
+ output.push('');
93
+ output.push(`(Results are truncated: showing first ${limit} results. Consider using a more specific path or pattern.)`);
94
+ }
95
+ }
96
+ const result = {
97
+ title: path_1.default.relative(mcp_helpers_1.MCP_WORKSPACE_DIR, searchPath),
98
+ metadata: {
99
+ count: files.length,
100
+ truncated
101
+ },
102
+ output: output.join('\n')
103
+ };
104
+ return {
105
+ content: [{ type: 'text', text: JSON.stringify(result) }],
106
+ structuredContent: result
107
+ };
108
+ });
109
+ }
package/dist/grep.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export interface GrepToolResult {
3
+ title: string;
4
+ metadata: {
5
+ matches: number;
6
+ truncated: boolean;
7
+ };
8
+ output: string;
9
+ }
10
+ export declare function registerGrepTool(server: McpServer): void;
package/dist/grep.js ADDED
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.registerGrepTool = registerGrepTool;
40
+ const grep_txt_1 = __importDefault(require("../descriptions/grep.txt"));
41
+ const zod_1 = __importDefault(require("zod"));
42
+ const path_1 = __importDefault(require("path"));
43
+ const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
44
+ const consumers_1 = require("stream/consumers");
45
+ const fs = __importStar(require("fs"));
46
+ const FILE_LIMIT = 100;
47
+ const MAX_LINE_LENGTH = 2000;
48
+ function handleResult(result) {
49
+ return {
50
+ content: [{ type: 'text', text: JSON.stringify(result) }],
51
+ structuredContent: result
52
+ };
53
+ }
54
+ function registerGrepTool(server) {
55
+ server.registerTool('grep', {
56
+ description: grep_txt_1.default,
57
+ inputSchema: zod_1.default.object({
58
+ pattern: zod_1.default.string().describe('The regex pattern to search for in file contents'),
59
+ path: zod_1.default
60
+ .string()
61
+ .optional()
62
+ .describe('The directory to search in. Defaults to the current working directory.'),
63
+ include: zod_1.default
64
+ .string()
65
+ .optional()
66
+ .describe('File pattern to include in the search (e.g. "*.js", "*.{ts,tsx}")')
67
+ })
68
+ }, async (args) => {
69
+ if (!args.pattern) {
70
+ throw new Error('pattern is required');
71
+ }
72
+ let searchPath = args.path ?? mcp_helpers_1.MCP_WORKSPACE_DIR;
73
+ searchPath = path_1.default.isAbsolute(searchPath)
74
+ ? searchPath
75
+ : path_1.default.resolve(mcp_helpers_1.MCP_WORKSPACE_DIR, searchPath);
76
+ // await assertExternalDirectory(ctx, searchPath, { kind: 'directory' })
77
+ const rgPath = await mcp_helpers_1.Ripgrep.filepath();
78
+ const rgArgs = [
79
+ '-nH',
80
+ '--hidden',
81
+ '--no-messages',
82
+ '--field-match-separator=|',
83
+ '--regexp',
84
+ args.pattern
85
+ ];
86
+ if (args.include) {
87
+ rgArgs.push('--glob', args.include);
88
+ }
89
+ rgArgs.push(searchPath);
90
+ const proc = mcp_helpers_1.SimpleProcess.spawn(rgPath, rgArgs, {
91
+ stdout: 'pipe',
92
+ stderr: 'pipe'
93
+ // abort: ctx.abort
94
+ });
95
+ if (!proc.stdout || !proc.stderr) {
96
+ throw new Error('Process output not available');
97
+ }
98
+ const output = await (0, consumers_1.text)(proc.stdout);
99
+ const errorOutput = await (0, consumers_1.text)(proc.stderr);
100
+ const exitCode = await proc.exited;
101
+ // Exit codes: 0 = matches found, 1 = no matches, 2 = errors (but may still have matches)
102
+ // With --no-messages, we suppress error output but still get exit code 2 for broken symlinks etc.
103
+ // Only fail if exit code is 2 AND no output was produced
104
+ if (exitCode === 1 || (exitCode === 2 && !output.trim())) {
105
+ return handleResult({
106
+ title: args.pattern,
107
+ metadata: { matches: 0, truncated: false },
108
+ output: 'No files found'
109
+ });
110
+ }
111
+ if (exitCode !== 0 && exitCode !== 2) {
112
+ throw new Error(`ripgrep failed: ${errorOutput}`);
113
+ }
114
+ const hasErrors = exitCode === 2;
115
+ // Handle both Unix (\n) and Windows (\r\n) line endings
116
+ const lines = output.trim().split(/\r?\n/);
117
+ const matches = [];
118
+ for (const line of lines) {
119
+ if (!line)
120
+ continue;
121
+ const [filePath, lineNumStr, ...lineTextParts] = line.split('|');
122
+ if (!filePath || !lineNumStr || lineTextParts.length === 0)
123
+ continue;
124
+ const lineNum = parseInt(lineNumStr, 10);
125
+ const lineText = lineTextParts.join('|');
126
+ const stats = fs.statSync(filePath, { throwIfNoEntry: false });
127
+ if (!stats)
128
+ continue;
129
+ matches.push({
130
+ path: filePath,
131
+ modTime: stats.mtime.getTime(),
132
+ lineNum,
133
+ lineText
134
+ });
135
+ }
136
+ matches.sort((a, b) => b.modTime - a.modTime);
137
+ const limit = FILE_LIMIT;
138
+ const truncated = matches.length > limit;
139
+ const finalMatches = truncated ? matches.slice(0, limit) : matches;
140
+ if (finalMatches.length === 0) {
141
+ return handleResult({
142
+ title: args.pattern,
143
+ metadata: { matches: 0, truncated: false },
144
+ output: 'No files found'
145
+ });
146
+ }
147
+ const totalMatches = matches.length;
148
+ const outputLines = [
149
+ `Found ${totalMatches} matches${truncated ? ` (showing first ${limit})` : ''}`
150
+ ];
151
+ let currentFile = '';
152
+ for (const match of finalMatches) {
153
+ if (currentFile !== match.path) {
154
+ if (currentFile !== '') {
155
+ outputLines.push('');
156
+ }
157
+ currentFile = match.path;
158
+ outputLines.push(`${match.path}:`);
159
+ }
160
+ const truncatedLineText = match.lineText.length > MAX_LINE_LENGTH
161
+ ? match.lineText.substring(0, MAX_LINE_LENGTH) + '...'
162
+ : match.lineText;
163
+ outputLines.push(` Line ${match.lineNum}: ${truncatedLineText}`);
164
+ }
165
+ if (truncated) {
166
+ outputLines.push('');
167
+ outputLines.push(`(Results truncated: showing ${limit} of ${totalMatches} matches (${totalMatches - limit} hidden). Consider using a more specific path or pattern.)`);
168
+ }
169
+ if (hasErrors) {
170
+ outputLines.push('');
171
+ outputLines.push('(Some paths were inaccessible and skipped)');
172
+ }
173
+ return handleResult({
174
+ title: args.pattern,
175
+ metadata: {
176
+ matches: totalMatches,
177
+ truncated
178
+ },
179
+ output
180
+ });
181
+ });
182
+ }
package/dist/index.js CHANGED
@@ -1,62 +1,14 @@
1
1
  #!/usr/bin/env bun
2
2
  "use strict";
3
- var __importDefault = (this && this.__importDefault) || function (mod) {
4
- return (mod && mod.__esModule) ? mod : { "default": mod };
5
- };
6
3
  Object.defineProperty(exports, "__esModule", { value: true });
7
- /**
8
- * Bash MCP Server v1.0
9
- */
10
4
  const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
11
5
  const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
12
- const path_1 = __importDefault(require("path"));
13
- const zod_1 = __importDefault(require("zod"));
14
- const web_tree_sitter_1 = require("web-tree-sitter");
15
- // @ts-ignore wasm
16
- const web_tree_sitter_wasm_1 = __importDefault(require("web-tree-sitter/web-tree-sitter.wasm"));
17
- // @ts-ignore wasm
18
- const tree_sitter_bash_wasm_1 = __importDefault(require("tree-sitter-bash/tree-sitter-bash.wasm"));
19
- const child_process_1 = require("child_process");
20
- const bash_txt_1 = __importDefault(require("../bash.txt"));
21
- const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
6
+ const ls_1 = require("./ls");
7
+ const bash_1 = require("./bash");
8
+ const grep_1 = require("./grep");
9
+ const glob_1 = require("./glob");
22
10
  const MCP_NAME = 'Bash MCP';
23
- const MCP_VERSION = '0.0.1';
24
- const parser = (0, mcp_helpers_1.lazy)(async () => {
25
- const treePath = (0, mcp_helpers_1.resolveWasm)(web_tree_sitter_wasm_1.default);
26
- await web_tree_sitter_1.Parser.init({
27
- locateFile() {
28
- return treePath;
29
- }
30
- });
31
- const bashPath = (0, mcp_helpers_1.resolveWasm)(tree_sitter_bash_wasm_1.default);
32
- const bashLanguage = await web_tree_sitter_1.Language.load(bashPath);
33
- const p = new web_tree_sitter_1.Parser();
34
- p.setLanguage(bashLanguage);
35
- return p;
36
- });
37
- /**
38
- * Get the path to the bash executable
39
- * @returns The path to the bash executable
40
- */
41
- const getBashPath = (0, mcp_helpers_1.lazy)(() => {
42
- const shell = process.env.SHELL;
43
- if (shell)
44
- return shell;
45
- // Fallback: check common Git Bash paths directly
46
- if (process.platform === 'win32') {
47
- const bash = (0, mcp_helpers_1.findGitBash)((0, mcp_helpers_1.getBundledBinaryPath)('bash'));
48
- if (bash)
49
- return bash;
50
- throw new Error('Git Bash not found');
51
- // return process.env.COMSPEC || "cmd.exe"
52
- }
53
- if (process.platform === 'darwin')
54
- return '/bin/zsh';
55
- const bash = Bun.which('bash');
56
- if (bash)
57
- return bash;
58
- return '/bin/sh';
59
- });
11
+ const MCP_VERSION = '0.0.4';
60
12
  const server = new mcp_js_1.McpServer({
61
13
  name: MCP_NAME,
62
14
  version: MCP_VERSION
@@ -65,285 +17,10 @@ const server = new mcp_js_1.McpServer({
65
17
  logging: {}
66
18
  }
67
19
  });
68
- // =============================================================================
69
- // Constants
70
- // =============================================================================
71
- // Default working directory
72
- const DEFAULT_CWD = path_1.default.join(mcp_helpers_1.MCP_HOME_DIR, 'workspace');
73
- // Grace period before SIGKILL (ms)
74
- const SIGKILL_TIMEOUT_MS = 5000;
75
- // Reserved characters for truncation ellipsis
76
- const TRUNCATION_ELLIPSIS_RESERVE = 50;
77
- // Exit code for timeout (shell convention)
78
- const EXIT_CODE_TIMEOUT = 124;
79
- // Sudo rejection message
80
- const SUDO_REJECTION_MESSAGE = '[REJECTED] sudo commands cannot be executed. Please STOP and ask human user to run it for you!\n';
81
- // Default max_output
82
- const DEFAULT_MAX_OUTPUT = 30000;
83
- const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
84
- // Environment variables that cannot be overridden (hardened mode only)
85
- const PROTECTED_ENV_VARS = new Set([
86
- 'LD_PRELOAD',
87
- 'LD_LIBRARY_PATH',
88
- 'DYLD_INSERT_LIBRARIES',
89
- 'DYLD_LIBRARY_PATH'
90
- ]);
91
- /**
92
- * Check if a command contains sudo
93
- * Matches: sudo at start, after semicolon, after &&, after ||, after |, after $(, after backtick
94
- */
95
- function containsSudo(command) {
96
- // Match sudo as a standalone command (not part of another word like "pseudocode")
97
- return /(?:^|[;&|`$()]\s*)sudo(?:\s|$)/m.test(command);
98
- }
99
- /**
100
- * Filter out protected environment variables that could be used for injection.
101
- * Only active in hardened mode. Passes everything through when hardened mode is off.
102
- */
103
- function filterEnvVars(env) {
104
- if (!env)
105
- return { filtered: undefined, rejected: [] };
106
- const rejected = [];
107
- const filtered = {};
108
- for (const [key, value] of Object.entries(env)) {
109
- if (PROTECTED_ENV_VARS.has(key.toUpperCase())) {
110
- rejected.push(key);
111
- }
112
- else {
113
- filtered[key] = value;
114
- }
115
- }
116
- return { filtered: Object.keys(filtered).length > 0 ? filtered : undefined, rejected };
117
- }
118
- /**
119
- * Middle-truncate output to preserve beginning and end
120
- */
121
- function middleTruncate(text, maxLength) {
122
- if (text.length <= maxLength)
123
- return text;
124
- const halfLength = Math.floor((maxLength - TRUNCATION_ELLIPSIS_RESERVE) / 2);
125
- const start = text.slice(0, halfLength);
126
- const end = text.slice(-halfLength);
127
- const truncatedBytes = text.length - maxLength;
128
- return `${start}\n\n... [truncated ${truncatedBytes} characters] ...\n\n${end}`;
129
- }
130
- // =============================================================================
131
- // Tool Definitions
132
- // =============================================================================
133
- const bashInputSchema = zod_1.default.object({
134
- command: zod_1.default.string().min(1).describe('The command to execute'),
135
- cwd: zod_1.default
136
- .string()
137
- .optional()
138
- .describe(`The working directory to run the command in. Must be in ${DEFAULT_CWD}. Defaults to ${DEFAULT_CWD}. Use this instead of 'cd' commands.`), // 需要在DEFAULT_CWD下
139
- timeout: zod_1.default
140
- .number()
141
- .min(1)
142
- .max(600000)
143
- .optional()
144
- .describe(`Timeout in milliseconds (optional, default ${DEFAULT_TIMEOUT_MS}, max 600000)`),
145
- description: zod_1.default
146
- .string()
147
- .optional()
148
- .describe('Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory "foo"'),
149
- env: zod_1.default
150
- .record(zod_1.default.string(), zod_1.default.string())
151
- .optional()
152
- .describe('Additional environment variables to set'),
153
- maxOutput: zod_1.default
154
- .number()
155
- .min(1)
156
- .max(1000000)
157
- .optional()
158
- .describe(`Maximum output length before middle-truncation (default ${DEFAULT_MAX_OUTPUT})`)
159
- });
160
- async function killTree(proc, opts) {
161
- const pid = proc.pid;
162
- if (!pid || opts?.exited?.())
163
- return;
164
- if (process.platform === 'win32') {
165
- await new Promise(resolve => {
166
- const killer = (0, child_process_1.spawn)('taskkill', ['/pid', String(pid), '/f', '/t'], { stdio: 'ignore' });
167
- killer.once('exit', () => resolve());
168
- killer.once('error', () => resolve());
169
- });
170
- return;
171
- }
172
- try {
173
- process.kill(-pid, 'SIGTERM');
174
- await Bun.sleep(SIGKILL_TIMEOUT_MS);
175
- if (!opts?.exited?.()) {
176
- process.kill(-pid, 'SIGKILL');
177
- }
178
- }
179
- catch (_e) {
180
- proc.kill('SIGTERM');
181
- await Bun.sleep(SIGKILL_TIMEOUT_MS);
182
- if (!opts?.exited?.()) {
183
- proc.kill('SIGKILL');
184
- }
185
- }
186
- }
187
- /**
188
- * Execute a bash command and return result
189
- */
190
- async function executeBash(options) {
191
- const command = options.command.trim();
192
- const cwd = options.cwd || DEFAULT_CWD;
193
- const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
194
- const maxOutput = options.maxOutput || DEFAULT_MAX_OUTPUT;
195
- const { filtered: safeEnv, rejected: rejectedVars } = filterEnvVars(options.env);
196
- if (containsSudo(command)) {
197
- throw Error(`${SUDO_REJECTION_MESSAGE}$ ${command}`);
198
- }
199
- if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, cwd, {
200
- includeSelf: true
201
- })) {
202
- throw Error(`"${cwd}" must be in ${DEFAULT_CWD}`);
203
- }
204
- if (!(await (0, mcp_helpers_1.isDir)(cwd))) {
205
- throw Error(`"${cwd}" must be a directory`);
206
- }
207
- const tree = await parser().then(p => p.parse(command));
208
- if (!tree) {
209
- throw new Error('Failed to parse command');
210
- }
211
- const directories = new Set();
212
- // if (!Instance.containsPath(cwd)) directories.add(cwd)
213
- const patterns = new Set();
214
- // const always = new Set<string>()
215
- let output = '';
216
- for (const node of tree.rootNode.descendantsOfType('command')) {
217
- if (!node)
218
- continue;
219
- // Get full command text including redirects if present
220
- let commandText = node.parent?.type === 'redirected_statement' ? node.parent.text : node.text;
221
- const commands = [];
222
- for (let i = 0; i < node.childCount; i++) {
223
- const child = node.child(i);
224
- if (!child)
225
- continue;
226
- if (child.type !== 'command_name' &&
227
- child.type !== 'word' &&
228
- child.type !== 'string' &&
229
- child.type !== 'raw_string' &&
230
- child.type !== 'concatenation') {
231
- continue;
232
- }
233
- commands.push(child.text);
234
- }
235
- // not an exhaustive list, but covers most common cases
236
- if (['cd', 'rm', 'cp', 'mv', 'mkdir', 'touch', 'chmod', 'chown', 'cat'].includes(commands[0])) {
237
- for (const arg of commands.slice(1)) {
238
- if (arg.startsWith('-') || (commands[0] === 'chmod' && arg.startsWith('+'))) {
239
- continue;
240
- }
241
- const resolved = await Bun.$ `realpath ${arg}`
242
- .cwd(cwd)
243
- .quiet()
244
- .nothrow()
245
- .text()
246
- .then(x => x.trim());
247
- if (resolved) {
248
- // Git Bash on Windows returns Unix-style paths like /c/Users/...
249
- const normalized = process.platform === 'win32' && resolved.match(/^\/[a-z]\//)
250
- ? (0, mcp_helpers_1.gitBashToWindowsPath)(resolved)
251
- : resolved;
252
- if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, normalized, { includeSelf: true })) {
253
- const dir = (await (0, mcp_helpers_1.isDir)(normalized)) ? normalized : path_1.default.dirname(normalized);
254
- directories.add(dir);
255
- }
256
- }
257
- }
258
- }
259
- else {
260
- }
261
- // cd covered by above check
262
- if (commands.length && commands[0] !== 'cd') {
263
- patterns.add(commandText);
264
- // always.add(BashArity.prefix(command).join(' ') + ' *')
265
- }
266
- }
267
- const mergedEnv = {
268
- ...process.env,
269
- ...safeEnv,
270
- ...(0, mcp_helpers_1.getBinaryEnvs)()
271
- };
272
- const proc = (0, child_process_1.spawn)(command, {
273
- shell: getBashPath(),
274
- cwd,
275
- env: {
276
- ...mergedEnv
277
- },
278
- stdio: ['ignore', 'pipe', 'pipe'],
279
- detached: process.platform !== 'win32'
280
- });
281
- const append = (chunk) => {
282
- output += chunk.toString();
283
- };
284
- proc.stdout?.on('data', append);
285
- proc.stderr?.on('data', append);
286
- let timedOut = false;
287
- let exited = false;
288
- const kill = () => killTree(proc, { exited: () => exited });
289
- const timeoutTimer = setTimeout(() => {
290
- timedOut = true;
291
- void kill();
292
- }, timeout + 100);
293
- await new Promise((resolve, reject) => {
294
- const cleanup = () => {
295
- clearTimeout(timeoutTimer);
296
- };
297
- proc.once('exit', () => {
298
- exited = true;
299
- cleanup();
300
- resolve();
301
- });
302
- proc.once('error', error => {
303
- exited = true;
304
- cleanup();
305
- reject(error);
306
- });
307
- });
308
- const resultMetadata = [];
309
- if (timedOut) {
310
- resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`);
311
- }
312
- if (rejectedVars.length > 0) {
313
- resultMetadata.push(`bash tool blocked env vars: ${rejectedVars.join(', ')}`);
314
- }
315
- if (resultMetadata.length > 0) {
316
- output += '\n\n<bash_metadata>\n' + resultMetadata.join('\n') + '\n</bash_metadata>';
317
- }
318
- return {
319
- output: middleTruncate(output, maxOutput),
320
- exitCode: proc.exitCode,
321
- description: options.description
322
- };
323
- }
324
- server.registerTool('bash', {
325
- description: bash_txt_1.default.replaceAll('${directory}', DEFAULT_CWD)
326
- .replaceAll('${maxLines}', String(2000))
327
- .replaceAll('${maxBytes}', String(50 * 1024)),
328
- inputSchema: bashInputSchema
329
- }, async (args) => {
330
- try {
331
- const result = await executeBash({ ...args });
332
- return {
333
- content: [{ type: 'text', text: JSON.stringify(result) }],
334
- structuredContent: result
335
- };
336
- }
337
- catch (error) {
338
- return {
339
- isError: true,
340
- content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }]
341
- };
342
- }
343
- });
344
- // =============================================================================
345
- // Start Server
346
- // =============================================================================
20
+ (0, bash_1.registerBashTool)(server);
21
+ (0, ls_1.registerLsTool)(server);
22
+ (0, glob_1.registerGlobTool)(server);
23
+ (0, grep_1.registerGrepTool)(server);
347
24
  const transport = new stdio_js_1.StdioServerTransport();
348
25
  server.connect(transport);
349
26
  console.error(`${MCP_NAME} Server v${MCP_VERSION} running`);
package/dist/ls.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function registerLsTool(server: McpServer): void;
package/dist/ls.js ADDED
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.registerLsTool = registerLsTool;
7
+ const ls_txt_1 = __importDefault(require("../descriptions/ls.txt"));
8
+ const zod_1 = __importDefault(require("zod"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
11
+ const IGNORE_PATTERNS = [
12
+ 'node_modules/',
13
+ '__pycache__/',
14
+ '.git/',
15
+ 'dist/',
16
+ 'build/',
17
+ 'target/',
18
+ 'vendor/',
19
+ 'bin/',
20
+ 'obj/',
21
+ '.idea/',
22
+ '.vscode/',
23
+ '.zig-cache/',
24
+ 'zig-out',
25
+ '.coverage',
26
+ 'coverage/',
27
+ 'vendor/',
28
+ 'tmp/',
29
+ 'temp/',
30
+ '.cache/',
31
+ 'cache/',
32
+ 'logs/',
33
+ '.venv/',
34
+ 'venv/',
35
+ 'env/'
36
+ ];
37
+ const FILE_LIMIT = 100;
38
+ function registerLsTool(server) {
39
+ server.registerTool('ls', {
40
+ description: ls_txt_1.default,
41
+ inputSchema: zod_1.default.object({
42
+ path: zod_1.default
43
+ .string()
44
+ .describe('The absolute path to the directory to list (must be absolute, not relative)')
45
+ .optional(),
46
+ ignore: zod_1.default.array(zod_1.default.string()).describe('List of glob patterns to ignore').optional()
47
+ })
48
+ }, async (args) => {
49
+ const searchPath = path_1.default.resolve(mcp_helpers_1.MCP_WORKSPACE_DIR, args.path || '.');
50
+ const ignoreGlobs = IGNORE_PATTERNS.map(p => `!${p}*`).concat(args.ignore?.map(p => `!${p}`) || []);
51
+ const files = [];
52
+ const rgFiles = await mcp_helpers_1.Ripgrep.files({
53
+ cwd: searchPath,
54
+ glob: ignoreGlobs
55
+ });
56
+ for (const file of rgFiles) {
57
+ files.push(file);
58
+ if (files.length >= FILE_LIMIT)
59
+ break;
60
+ }
61
+ // Build directory structure
62
+ const dirs = new Set();
63
+ const filesByDir = new Map();
64
+ for (const file of files) {
65
+ const dir = path_1.default.dirname(file);
66
+ const parts = dir === '.' ? [] : dir.split('/');
67
+ // Add all parent directories
68
+ for (let i = 0; i <= parts.length; i++) {
69
+ const dirPath = i === 0 ? '.' : parts.slice(0, i).join('/');
70
+ dirs.add(dirPath);
71
+ }
72
+ // Add file to its directory
73
+ if (!filesByDir.has(dir))
74
+ filesByDir.set(dir, []);
75
+ filesByDir.get(dir).push(path_1.default.basename(file));
76
+ }
77
+ function renderDir(dirPath, depth) {
78
+ const indent = ' '.repeat(depth);
79
+ let output = '';
80
+ if (depth > 0) {
81
+ output += `${indent}${path_1.default.basename(dirPath)}/\n`;
82
+ }
83
+ const childIndent = ' '.repeat(depth + 1);
84
+ const children = Array.from(dirs)
85
+ .filter(d => path_1.default.dirname(d) === dirPath && d !== dirPath)
86
+ .sort();
87
+ // Render subdirectories first
88
+ for (const child of children) {
89
+ output += renderDir(child, depth + 1);
90
+ }
91
+ // Render files
92
+ const files = filesByDir.get(dirPath) || [];
93
+ for (const file of files.sort()) {
94
+ output += `${childIndent}${file}\n`;
95
+ }
96
+ return output;
97
+ }
98
+ const result = {
99
+ title: path_1.default.relative(mcp_helpers_1.MCP_WORKSPACE_DIR, searchPath),
100
+ metadata: {
101
+ count: files.length,
102
+ truncated: files.length >= FILE_LIMIT
103
+ },
104
+ output: `${searchPath}/\n` + renderDir('.', 0)
105
+ };
106
+ return {
107
+ content: [{ type: 'text', text: JSON.stringify(result) }],
108
+ structuredContent: result
109
+ };
110
+ });
111
+ }
package/package.json CHANGED
@@ -1,18 +1,19 @@
1
1
  {
2
2
  "name": "@baitong-dev/bash-mcp",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "main": "./dist/index.js",
5
5
  "bin": {
6
6
  "@baitong-dev/bash-mcp": "./dist/index.js"
7
7
  },
8
8
  "files": [
9
9
  "dist",
10
- "bash.txt",
10
+ "descriptions",
11
11
  "README.md"
12
12
  ],
13
13
  "keywords": [
14
14
  "mcp",
15
- "bash"
15
+ "bash",
16
+ "ls"
16
17
  ],
17
18
  "description": "bash-mcp",
18
19
  "dependencies": {
@@ -20,7 +21,10 @@
20
21
  "tree-sitter-bash": "^0.25.1",
21
22
  "web-tree-sitter": "^0.26.5",
22
23
  "zod": "^4.3.4",
23
- "@baitong-dev/mcp-helpers": "0.0.1"
24
+ "@baitong-dev/mcp-helpers": "0.0.3"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.9.2"
24
28
  },
25
29
  "publishConfig": {
26
30
  "access": "public",
File without changes