@baitong-dev/bash-mcp 0.0.3 → 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.
- package/descriptions/glob.txt +6 -0
- package/descriptions/grep.txt +8 -0
- package/descriptions/ls.txt +1 -0
- package/dist/{src/index.d.ts → bash.d.ts} +2 -1
- package/dist/{src/index.js → bash.js} +19 -50
- package/dist/glob.d.ts +2 -0
- package/dist/glob.js +109 -0
- package/dist/grep.d.ts +10 -0
- package/dist/grep.js +182 -0
- package/dist/index.d.ts +1 -8
- package/dist/index.js +9 -346
- package/dist/ls.d.ts +2 -0
- package/dist/ls.js +111 -0
- package/package.json +5 -4
- /package/{bash.txt → descriptions/bash.txt} +0 -0
|
@@ -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.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
/**
|
|
3
3
|
* Environment variables to inherit by default, if an environment is not explicitly given.
|
|
4
4
|
*/
|
|
@@ -7,3 +7,4 @@ export declare const DEFAULT_INHERITED_ENV_VARS: string[];
|
|
|
7
7
|
* Returns a default environment object including only environment variables deemed safe to inherit.
|
|
8
8
|
*/
|
|
9
9
|
export declare function getDefaultEnvs(): {};
|
|
10
|
+
export declare function registerBashTool(server: McpServer): void;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
1
|
"use strict";
|
|
3
2
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -6,11 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
6
|
exports.DEFAULT_INHERITED_ENV_VARS = void 0;
|
|
8
7
|
exports.getDefaultEnvs = getDefaultEnvs;
|
|
9
|
-
|
|
10
|
-
* Bash MCP Server v1.0
|
|
11
|
-
*/
|
|
12
|
-
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
|
-
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
8
|
+
exports.registerBashTool = registerBashTool;
|
|
14
9
|
const path_1 = __importDefault(require("path"));
|
|
15
10
|
const zod_1 = __importDefault(require("zod"));
|
|
16
11
|
const web_tree_sitter_1 = require("web-tree-sitter");
|
|
@@ -19,11 +14,8 @@ const web_tree_sitter_wasm_1 = __importDefault(require("web-tree-sitter/web-tree
|
|
|
19
14
|
// @ts-ignore wasm
|
|
20
15
|
const tree_sitter_bash_wasm_1 = __importDefault(require("tree-sitter-bash/tree-sitter-bash.wasm"));
|
|
21
16
|
const child_process_1 = require("child_process");
|
|
22
|
-
const bash_txt_1 = __importDefault(require("../bash.txt"));
|
|
17
|
+
const bash_txt_1 = __importDefault(require("../descriptions/bash.txt"));
|
|
23
18
|
const mcp_helpers_1 = require("@baitong-dev/mcp-helpers");
|
|
24
|
-
const package_json_1 = __importDefault(require("../package.json"));
|
|
25
|
-
const MCP_NAME = 'Bash MCP';
|
|
26
|
-
const MCP_VERSION = package_json_1.default.version;
|
|
27
19
|
const parser = (0, mcp_helpers_1.lazy)(async () => {
|
|
28
20
|
const treePath = (0, mcp_helpers_1.resolveWasm)(web_tree_sitter_wasm_1.default);
|
|
29
21
|
await web_tree_sitter_1.Parser.init({
|
|
@@ -60,14 +52,6 @@ const getBashPath = (0, mcp_helpers_1.lazy)(() => {
|
|
|
60
52
|
return bash;
|
|
61
53
|
return '/bin/sh';
|
|
62
54
|
});
|
|
63
|
-
const server = new mcp_js_1.McpServer({
|
|
64
|
-
name: MCP_NAME,
|
|
65
|
-
version: MCP_VERSION
|
|
66
|
-
}, {
|
|
67
|
-
capabilities: {
|
|
68
|
-
logging: {}
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
55
|
// =============================================================================
|
|
72
56
|
// Constants
|
|
73
57
|
// =============================================================================
|
|
@@ -143,10 +127,7 @@ function middleTruncate(text, maxLength) {
|
|
|
143
127
|
const truncatedBytes = text.length - maxLength;
|
|
144
128
|
return `${start}\n\n... [truncated ${truncatedBytes} characters] ...\n\n${end}`;
|
|
145
129
|
}
|
|
146
|
-
|
|
147
|
-
// Tool Definitions
|
|
148
|
-
// =============================================================================
|
|
149
|
-
const bashInputSchema = zod_1.default.object({
|
|
130
|
+
const inputSchema = zod_1.default.object({
|
|
150
131
|
command: zod_1.default.string().min(1).describe('The command to execute'),
|
|
151
132
|
cwd: zod_1.default
|
|
152
133
|
.string()
|
|
@@ -160,7 +141,6 @@ const bashInputSchema = zod_1.default.object({
|
|
|
160
141
|
.describe(`Timeout in milliseconds (optional, default ${DEFAULT_TIMEOUT_MS}, max 600000)`),
|
|
161
142
|
description: zod_1.default
|
|
162
143
|
.string()
|
|
163
|
-
.optional()
|
|
164
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"'),
|
|
165
145
|
env: zod_1.default
|
|
166
146
|
.record(zod_1.default.string(), zod_1.default.string())
|
|
@@ -232,7 +212,7 @@ async function executeBash(options) {
|
|
|
232
212
|
if (!node)
|
|
233
213
|
continue;
|
|
234
214
|
// Get full command text including redirects if present
|
|
235
|
-
|
|
215
|
+
const commandText = node.parent?.type === 'redirected_statement' ? node.parent.text : node.text;
|
|
236
216
|
const commands = [];
|
|
237
217
|
for (let i = 0; i < node.childCount; i++) {
|
|
238
218
|
const child = node.child(i);
|
|
@@ -272,6 +252,7 @@ async function executeBash(options) {
|
|
|
272
252
|
}
|
|
273
253
|
}
|
|
274
254
|
else {
|
|
255
|
+
//
|
|
275
256
|
}
|
|
276
257
|
// cd covered by above check
|
|
277
258
|
if (commands.length && commands[0] !== 'cd') {
|
|
@@ -327,38 +308,26 @@ async function executeBash(options) {
|
|
|
327
308
|
output += '\n\n<bash_metadata>\n' + resultMetadata.join('\n') + '\n</bash_metadata>';
|
|
328
309
|
}
|
|
329
310
|
return {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
311
|
+
title: options.description,
|
|
312
|
+
metadata: {
|
|
313
|
+
output: middleTruncate(output, maxOutput),
|
|
314
|
+
exit: proc.exitCode,
|
|
315
|
+
description: options.description
|
|
316
|
+
},
|
|
317
|
+
output
|
|
333
318
|
};
|
|
334
319
|
}
|
|
335
|
-
server
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
...(0, mcp_helpers_1.getBundledBinaryEnvs)()
|
|
339
|
-
}, null, 2) +
|
|
340
|
-
bash_txt_1.default.replaceAll('${directory}', DEFAULT_CWD)
|
|
320
|
+
function registerBashTool(server) {
|
|
321
|
+
server.registerTool('bash', {
|
|
322
|
+
description: bash_txt_1.default.replaceAll('${directory}', DEFAULT_CWD)
|
|
341
323
|
.replaceAll('${maxLines}', String(2000))
|
|
342
324
|
.replaceAll('${maxBytes}', String(50 * 1024)),
|
|
343
|
-
|
|
344
|
-
}, async (args) => {
|
|
345
|
-
try {
|
|
325
|
+
inputSchema
|
|
326
|
+
}, async (args) => {
|
|
346
327
|
const result = await executeBash({ ...args });
|
|
347
328
|
return {
|
|
348
329
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
349
330
|
structuredContent: result
|
|
350
331
|
};
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return {
|
|
354
|
-
isError: true,
|
|
355
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }]
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
});
|
|
359
|
-
// =============================================================================
|
|
360
|
-
// Start Server
|
|
361
|
-
// =============================================================================
|
|
362
|
-
const transport = new stdio_js_1.StdioServerTransport();
|
|
363
|
-
server.connect(transport);
|
|
364
|
-
console.error(`${MCP_NAME} Server v${MCP_VERSION} running`);
|
|
332
|
+
});
|
|
333
|
+
}
|
package/dist/glob.d.ts
ADDED
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.d.ts
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
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(): {};
|
|
2
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -1,64 +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
|
-
exports.DEFAULT_INHERITED_ENV_VARS = void 0;
|
|
8
|
-
exports.getDefaultEnvs = getDefaultEnvs;
|
|
9
|
-
/**
|
|
10
|
-
* Bash MCP Server
|
|
11
|
-
*/
|
|
12
4
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
13
5
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
const web_tree_sitter_wasm_1 = __importDefault(require("web-tree-sitter/web-tree-sitter.wasm"));
|
|
19
|
-
// @ts-ignore wasm
|
|
20
|
-
const tree_sitter_bash_wasm_1 = __importDefault(require("tree-sitter-bash/tree-sitter-bash.wasm"));
|
|
21
|
-
const child_process_1 = require("child_process");
|
|
22
|
-
const bash_txt_1 = __importDefault(require("../bash.txt"));
|
|
23
|
-
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");
|
|
24
10
|
const MCP_NAME = 'Bash MCP';
|
|
25
|
-
const MCP_VERSION = '0.0.
|
|
26
|
-
const parser = (0, mcp_helpers_1.lazy)(async () => {
|
|
27
|
-
const treePath = (0, mcp_helpers_1.resolveWasm)(web_tree_sitter_wasm_1.default);
|
|
28
|
-
await web_tree_sitter_1.Parser.init({
|
|
29
|
-
locateFile() {
|
|
30
|
-
return treePath;
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
const bashPath = (0, mcp_helpers_1.resolveWasm)(tree_sitter_bash_wasm_1.default);
|
|
34
|
-
const bashLanguage = await web_tree_sitter_1.Language.load(bashPath);
|
|
35
|
-
const p = new web_tree_sitter_1.Parser();
|
|
36
|
-
p.setLanguage(bashLanguage);
|
|
37
|
-
return p;
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Get the path to the bash executable
|
|
41
|
-
* @returns The path to the bash executable
|
|
42
|
-
*/
|
|
43
|
-
const getBashPath = (0, mcp_helpers_1.lazy)(() => {
|
|
44
|
-
const shell = process.env.SHELL;
|
|
45
|
-
if (shell)
|
|
46
|
-
return shell;
|
|
47
|
-
// Fallback: check common Git Bash paths directly
|
|
48
|
-
if (process.platform === 'win32') {
|
|
49
|
-
const bash = (0, mcp_helpers_1.findGitBash)((0, mcp_helpers_1.getBundledBinaryPath)('bash'));
|
|
50
|
-
if (bash)
|
|
51
|
-
return bash;
|
|
52
|
-
throw new Error('Git Bash not found');
|
|
53
|
-
// return process.env.COMSPEC || "cmd.exe"
|
|
54
|
-
}
|
|
55
|
-
if (process.platform === 'darwin')
|
|
56
|
-
return '/bin/zsh';
|
|
57
|
-
const bash = Bun.which('bash');
|
|
58
|
-
if (bash)
|
|
59
|
-
return bash;
|
|
60
|
-
return '/bin/sh';
|
|
61
|
-
});
|
|
11
|
+
const MCP_VERSION = '0.0.4';
|
|
62
12
|
const server = new mcp_js_1.McpServer({
|
|
63
13
|
name: MCP_NAME,
|
|
64
14
|
version: MCP_VERSION
|
|
@@ -67,297 +17,10 @@ const server = new mcp_js_1.McpServer({
|
|
|
67
17
|
logging: {}
|
|
68
18
|
}
|
|
69
19
|
});
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const DEFAULT_CWD = path_1.default.join(mcp_helpers_1.MCP_HOME_DIR, 'workspace');
|
|
75
|
-
// Grace period before SIGKILL (ms)
|
|
76
|
-
const SIGKILL_TIMEOUT_MS = 5000;
|
|
77
|
-
// Reserved characters for truncation ellipsis
|
|
78
|
-
const TRUNCATION_ELLIPSIS_RESERVE = 50;
|
|
79
|
-
// Exit code for timeout (shell convention)
|
|
80
|
-
const EXIT_CODE_TIMEOUT = 124;
|
|
81
|
-
// Sudo rejection message
|
|
82
|
-
const SUDO_REJECTION_MESSAGE = '[REJECTED] sudo commands cannot be executed. Please STOP and ask human user to run it for you!\n';
|
|
83
|
-
// Default max_output
|
|
84
|
-
const DEFAULT_MAX_OUTPUT = 30000;
|
|
85
|
-
const DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
86
|
-
/**
|
|
87
|
-
* Check if a command contains sudo
|
|
88
|
-
* Matches: sudo at start, after semicolon, after &&, after ||, after |, after $(, after backtick
|
|
89
|
-
*/
|
|
90
|
-
function containsSudo(command) {
|
|
91
|
-
// Match sudo as a standalone command (not part of another word like "pseudocode")
|
|
92
|
-
return /(?:^|[;&|`$()]\s*)sudo(?:\s|$)/m.test(command);
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Environment variables to inherit by default, if an environment is not explicitly given.
|
|
96
|
-
*/
|
|
97
|
-
exports.DEFAULT_INHERITED_ENV_VARS = process.platform === 'win32'
|
|
98
|
-
? [
|
|
99
|
-
'APPDATA',
|
|
100
|
-
'HOMEDRIVE',
|
|
101
|
-
'HOMEPATH',
|
|
102
|
-
'LOCALAPPDATA',
|
|
103
|
-
'PATH',
|
|
104
|
-
'PROCESSOR_ARCHITECTURE',
|
|
105
|
-
'SYSTEMDRIVE',
|
|
106
|
-
'SYSTEMROOT',
|
|
107
|
-
'TEMP',
|
|
108
|
-
'TMPDIR',
|
|
109
|
-
'USERNAME',
|
|
110
|
-
'USERPROFILE',
|
|
111
|
-
'PROGRAMFILES'
|
|
112
|
-
]
|
|
113
|
-
: /* list inspired by the default env inheritance of sudo */
|
|
114
|
-
['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
|
|
115
|
-
/**
|
|
116
|
-
* Returns a default environment object including only environment variables deemed safe to inherit.
|
|
117
|
-
*/
|
|
118
|
-
function getDefaultEnvs() {
|
|
119
|
-
const env = {};
|
|
120
|
-
for (const key of exports.DEFAULT_INHERITED_ENV_VARS) {
|
|
121
|
-
const value = process.env[key];
|
|
122
|
-
if (value === undefined) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
if (value.startsWith('()')) {
|
|
126
|
-
// Skip functions, which are a security risk.
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
env[key] = value;
|
|
130
|
-
}
|
|
131
|
-
return env;
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Middle-truncate output to preserve beginning and end
|
|
135
|
-
*/
|
|
136
|
-
function middleTruncate(text, maxLength) {
|
|
137
|
-
if (text.length <= maxLength)
|
|
138
|
-
return text;
|
|
139
|
-
const halfLength = Math.floor((maxLength - TRUNCATION_ELLIPSIS_RESERVE) / 2);
|
|
140
|
-
const start = text.slice(0, halfLength);
|
|
141
|
-
const end = text.slice(-halfLength);
|
|
142
|
-
const truncatedBytes = text.length - maxLength;
|
|
143
|
-
return `${start}\n\n... [truncated ${truncatedBytes} characters] ...\n\n${end}`;
|
|
144
|
-
}
|
|
145
|
-
// =============================================================================
|
|
146
|
-
// Tool Definitions
|
|
147
|
-
// =============================================================================
|
|
148
|
-
const bashInputSchema = zod_1.default.object({
|
|
149
|
-
command: zod_1.default.string().min(1).describe('The command to execute'),
|
|
150
|
-
cwd: zod_1.default
|
|
151
|
-
.string()
|
|
152
|
-
.optional()
|
|
153
|
-
.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下
|
|
154
|
-
timeout: zod_1.default
|
|
155
|
-
.number()
|
|
156
|
-
.min(1)
|
|
157
|
-
.max(600000)
|
|
158
|
-
.optional()
|
|
159
|
-
.describe(`Timeout in milliseconds (optional, default ${DEFAULT_TIMEOUT_MS}, max 600000)`),
|
|
160
|
-
description: zod_1.default
|
|
161
|
-
.string()
|
|
162
|
-
.optional()
|
|
163
|
-
.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"'),
|
|
164
|
-
env: zod_1.default
|
|
165
|
-
.record(zod_1.default.string(), zod_1.default.string())
|
|
166
|
-
.optional()
|
|
167
|
-
.describe('Additional environment variables to set'),
|
|
168
|
-
maxOutput: zod_1.default
|
|
169
|
-
.number()
|
|
170
|
-
.min(1)
|
|
171
|
-
.max(1000000)
|
|
172
|
-
.optional()
|
|
173
|
-
.describe(`Maximum output length before middle-truncation (default ${DEFAULT_MAX_OUTPUT})`)
|
|
174
|
-
});
|
|
175
|
-
async function killTree(proc, opts) {
|
|
176
|
-
const pid = proc.pid;
|
|
177
|
-
if (!pid || opts?.exited?.())
|
|
178
|
-
return;
|
|
179
|
-
if (process.platform === 'win32') {
|
|
180
|
-
await new Promise(resolve => {
|
|
181
|
-
const killer = (0, child_process_1.spawn)('taskkill', ['/pid', String(pid), '/f', '/t'], { stdio: 'ignore' });
|
|
182
|
-
killer.once('exit', () => resolve());
|
|
183
|
-
killer.once('error', () => resolve());
|
|
184
|
-
});
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
try {
|
|
188
|
-
process.kill(-pid, 'SIGTERM');
|
|
189
|
-
await Bun.sleep(SIGKILL_TIMEOUT_MS);
|
|
190
|
-
if (!opts?.exited?.()) {
|
|
191
|
-
process.kill(-pid, 'SIGKILL');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
catch (_e) {
|
|
195
|
-
proc.kill('SIGTERM');
|
|
196
|
-
await Bun.sleep(SIGKILL_TIMEOUT_MS);
|
|
197
|
-
if (!opts?.exited?.()) {
|
|
198
|
-
proc.kill('SIGKILL');
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* Execute a bash command and return result
|
|
204
|
-
*/
|
|
205
|
-
async function executeBash(options) {
|
|
206
|
-
const command = options.command.trim();
|
|
207
|
-
const cwd = options.cwd || DEFAULT_CWD;
|
|
208
|
-
const timeout = options.timeout || DEFAULT_TIMEOUT_MS;
|
|
209
|
-
const maxOutput = options.maxOutput || DEFAULT_MAX_OUTPUT;
|
|
210
|
-
if (containsSudo(command)) {
|
|
211
|
-
throw Error(`${SUDO_REJECTION_MESSAGE}$ ${command}`);
|
|
212
|
-
}
|
|
213
|
-
if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, cwd, {
|
|
214
|
-
includeSelf: true
|
|
215
|
-
})) {
|
|
216
|
-
throw Error(`"${cwd}" must be in ${DEFAULT_CWD}`);
|
|
217
|
-
}
|
|
218
|
-
if (!(await (0, mcp_helpers_1.isDir)(cwd))) {
|
|
219
|
-
throw Error(`"${cwd}" must be a directory`);
|
|
220
|
-
}
|
|
221
|
-
const tree = await parser().then(p => p.parse(command));
|
|
222
|
-
if (!tree) {
|
|
223
|
-
throw new Error('Failed to parse command');
|
|
224
|
-
}
|
|
225
|
-
const directories = new Set();
|
|
226
|
-
// if (!Instance.containsPath(cwd)) directories.add(cwd)
|
|
227
|
-
const patterns = new Set();
|
|
228
|
-
// const always = new Set<string>()
|
|
229
|
-
let output = '';
|
|
230
|
-
for (const node of tree.rootNode.descendantsOfType('command')) {
|
|
231
|
-
if (!node)
|
|
232
|
-
continue;
|
|
233
|
-
// Get full command text including redirects if present
|
|
234
|
-
let commandText = node.parent?.type === 'redirected_statement' ? node.parent.text : node.text;
|
|
235
|
-
const commands = [];
|
|
236
|
-
for (let i = 0; i < node.childCount; i++) {
|
|
237
|
-
const child = node.child(i);
|
|
238
|
-
if (!child)
|
|
239
|
-
continue;
|
|
240
|
-
if (child.type !== 'command_name' &&
|
|
241
|
-
child.type !== 'word' &&
|
|
242
|
-
child.type !== 'string' &&
|
|
243
|
-
child.type !== 'raw_string' &&
|
|
244
|
-
child.type !== 'concatenation') {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
commands.push(child.text);
|
|
248
|
-
}
|
|
249
|
-
// not an exhaustive list, but covers most common cases
|
|
250
|
-
if (['cd', 'rm', 'cp', 'mv', 'mkdir', 'touch', 'chmod', 'chown', 'cat'].includes(commands[0])) {
|
|
251
|
-
for (const arg of commands.slice(1)) {
|
|
252
|
-
if (arg.startsWith('-') || (commands[0] === 'chmod' && arg.startsWith('+'))) {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
const resolved = await Bun.$ `realpath ${arg}`
|
|
256
|
-
.cwd(cwd)
|
|
257
|
-
.quiet()
|
|
258
|
-
.nothrow()
|
|
259
|
-
.text()
|
|
260
|
-
.then(x => x.trim());
|
|
261
|
-
if (resolved) {
|
|
262
|
-
// Git Bash on Windows returns Unix-style paths like /c/Users/...
|
|
263
|
-
const normalized = process.platform === 'win32' && resolved.match(/^\/[a-z]\//)
|
|
264
|
-
? (0, mcp_helpers_1.gitBashToWindowsPath)(resolved)
|
|
265
|
-
: resolved;
|
|
266
|
-
if (!(0, mcp_helpers_1.isSubdirectory)(DEFAULT_CWD, normalized, { includeSelf: true })) {
|
|
267
|
-
const dir = (await (0, mcp_helpers_1.isDir)(normalized)) ? normalized : path_1.default.dirname(normalized);
|
|
268
|
-
directories.add(dir);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
else {
|
|
274
|
-
}
|
|
275
|
-
// cd covered by above check
|
|
276
|
-
if (commands.length && commands[0] !== 'cd') {
|
|
277
|
-
patterns.add(commandText);
|
|
278
|
-
// always.add(BashArity.prefix(command).join(' ') + ' *')
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
const mergedEnv = {
|
|
282
|
-
...getDefaultEnvs(),
|
|
283
|
-
...(0, mcp_helpers_1.getBundledBinaryEnvs)()
|
|
284
|
-
};
|
|
285
|
-
const proc = (0, child_process_1.spawn)(command, {
|
|
286
|
-
shell: getBashPath(),
|
|
287
|
-
cwd,
|
|
288
|
-
env: {
|
|
289
|
-
...mergedEnv
|
|
290
|
-
},
|
|
291
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
292
|
-
detached: process.platform !== 'win32'
|
|
293
|
-
});
|
|
294
|
-
const append = (chunk) => {
|
|
295
|
-
output += chunk.toString();
|
|
296
|
-
};
|
|
297
|
-
proc.stdout?.on('data', append);
|
|
298
|
-
proc.stderr?.on('data', append);
|
|
299
|
-
let timedOut = false;
|
|
300
|
-
let exited = false;
|
|
301
|
-
const kill = () => killTree(proc, { exited: () => exited });
|
|
302
|
-
const timeoutTimer = setTimeout(() => {
|
|
303
|
-
timedOut = true;
|
|
304
|
-
void kill();
|
|
305
|
-
}, timeout + 100);
|
|
306
|
-
await new Promise((resolve, reject) => {
|
|
307
|
-
const cleanup = () => {
|
|
308
|
-
clearTimeout(timeoutTimer);
|
|
309
|
-
};
|
|
310
|
-
proc.once('exit', () => {
|
|
311
|
-
exited = true;
|
|
312
|
-
cleanup();
|
|
313
|
-
resolve();
|
|
314
|
-
});
|
|
315
|
-
proc.once('error', error => {
|
|
316
|
-
exited = true;
|
|
317
|
-
cleanup();
|
|
318
|
-
reject(error);
|
|
319
|
-
});
|
|
320
|
-
});
|
|
321
|
-
const resultMetadata = [];
|
|
322
|
-
if (timedOut) {
|
|
323
|
-
resultMetadata.push(`bash tool terminated command after exceeding timeout ${timeout} ms`);
|
|
324
|
-
}
|
|
325
|
-
if (resultMetadata.length > 0) {
|
|
326
|
-
output += '\n\n<bash_metadata>\n' + resultMetadata.join('\n') + '\n</bash_metadata>';
|
|
327
|
-
}
|
|
328
|
-
return {
|
|
329
|
-
output: middleTruncate(output, maxOutput),
|
|
330
|
-
exitCode: proc.exitCode,
|
|
331
|
-
description: options.description
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
server.registerTool('bash', {
|
|
335
|
-
description: JSON.stringify({
|
|
336
|
-
...getDefaultEnvs(),
|
|
337
|
-
...(0, mcp_helpers_1.getBundledBinaryEnvs)()
|
|
338
|
-
}, null, 2) +
|
|
339
|
-
bash_txt_1.default.replaceAll('${directory}', DEFAULT_CWD)
|
|
340
|
-
.replaceAll('${maxLines}', String(2000))
|
|
341
|
-
.replaceAll('${maxBytes}', String(50 * 1024)),
|
|
342
|
-
inputSchema: bashInputSchema
|
|
343
|
-
}, async (args) => {
|
|
344
|
-
try {
|
|
345
|
-
const result = await executeBash({ ...args });
|
|
346
|
-
return {
|
|
347
|
-
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
348
|
-
structuredContent: result
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
return {
|
|
353
|
-
isError: true,
|
|
354
|
-
content: [{ type: 'text', text: error instanceof Error ? error.message : String(error) }]
|
|
355
|
-
};
|
|
356
|
-
}
|
|
357
|
-
});
|
|
358
|
-
// =============================================================================
|
|
359
|
-
// Start Server
|
|
360
|
-
// =============================================================================
|
|
20
|
+
(0, bash_1.registerBashTool)(server);
|
|
21
|
+
(0, ls_1.registerLsTool)(server);
|
|
22
|
+
(0, glob_1.registerGlobTool)(server);
|
|
23
|
+
(0, grep_1.registerGrepTool)(server);
|
|
361
24
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
362
25
|
server.connect(transport);
|
|
363
26
|
console.error(`${MCP_NAME} Server v${MCP_VERSION} running`);
|
package/dist/ls.d.ts
ADDED
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.
|
|
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
|
-
"
|
|
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,7 @@
|
|
|
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.
|
|
24
|
+
"@baitong-dev/mcp-helpers": "0.0.3"
|
|
24
25
|
},
|
|
25
26
|
"devDependencies": {
|
|
26
27
|
"typescript": "^5.9.2"
|
|
File without changes
|