just-bash-mcp 2.9.4 → 3.0.0
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/README.md +47 -5
- package/package.json +3 -2
- package/src/config/index.ts +346 -326
- package/src/index.ts +8 -12
- package/src/tools/bash-instance.ts +66 -131
- package/src/tools/exec-tools.ts +92 -139
- package/src/tools/file-tools.ts +89 -182
- package/src/tools/index.ts +19 -27
- package/src/tools/info-tools.ts +103 -135
- package/src/tools/sandbox-tools.ts +114 -209
- package/src/types.ts +1 -1
- package/src/utils/index.ts +20 -55
|
@@ -9,230 +9,135 @@
|
|
|
9
9
|
* - OutputMessage type for streaming output
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type OutputMessage,
|
|
19
|
-
} from "just-bash";
|
|
20
|
-
import { z } from "zod/v4";
|
|
21
|
-
import { config } from "../config/index.ts";
|
|
22
|
-
import {
|
|
23
|
-
createErrorResponse,
|
|
24
|
-
createJsonResponse,
|
|
25
|
-
createSuccessResponse,
|
|
26
|
-
truncateOutput,
|
|
27
|
-
} from "../utils/index.ts";
|
|
28
|
-
import { getPersistentSandbox, resetPersistentSandbox } from "./bash-instance.ts";
|
|
12
|
+
import type {McpServer} from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
13
|
+
import {NetworkAccessDeniedError, RedirectNotAllowedError, SecurityViolationError, TooManyRedirectsError, type OutputMessage} from 'just-bash'
|
|
14
|
+
import {z} from 'zod/v4'
|
|
15
|
+
import {config} from '../config/index.ts'
|
|
16
|
+
import {createErrorResponse, createJsonResponse, createSuccessResponse, truncateOutput} from '../utils/index.ts'
|
|
17
|
+
import {getPersistentSandbox, resetPersistentSandbox} from './bash-instance.ts'
|
|
29
18
|
|
|
30
19
|
function classifyError(error: unknown, prefix: string) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
20
|
+
if (error instanceof NetworkAccessDeniedError) {
|
|
21
|
+
return createErrorResponse(error, `${prefix} [Network Access Denied]`)
|
|
22
|
+
}
|
|
23
|
+
if (error instanceof TooManyRedirectsError) {
|
|
24
|
+
return createErrorResponse(error, `${prefix} [Too Many Redirects]`)
|
|
25
|
+
}
|
|
26
|
+
if (error instanceof RedirectNotAllowedError) {
|
|
27
|
+
return createErrorResponse(error, `${prefix} [Redirect Not Allowed]`)
|
|
28
|
+
}
|
|
29
|
+
if (error instanceof SecurityViolationError) {
|
|
30
|
+
return createErrorResponse(error, `${prefix} [Security Violation]`)
|
|
31
|
+
}
|
|
32
|
+
return createErrorResponse(error, prefix)
|
|
44
33
|
}
|
|
45
34
|
|
|
46
35
|
export function registerSandboxTools(server: McpServer): void {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
async ({
|
|
67
|
-
command,
|
|
68
|
-
cwd,
|
|
69
|
-
env,
|
|
70
|
-
includeOutput = false,
|
|
71
|
-
includeLogs = false,
|
|
72
|
-
}: {
|
|
73
|
-
command: string;
|
|
74
|
-
cwd?: string;
|
|
75
|
-
env?: Record<string, string>;
|
|
76
|
-
includeOutput?: boolean;
|
|
77
|
-
includeLogs?: boolean;
|
|
78
|
-
}) => {
|
|
79
|
-
try {
|
|
80
|
-
const sandbox = await getPersistentSandbox();
|
|
81
|
-
const cmd = await sandbox.runCommand(command, { cwd, env });
|
|
82
|
-
const result = await cmd.wait();
|
|
83
|
-
const stdout = await cmd.stdout();
|
|
84
|
-
const stderr = await cmd.stderr();
|
|
36
|
+
server.registerTool(
|
|
37
|
+
'bash_sandbox_run',
|
|
38
|
+
{
|
|
39
|
+
description: 'Run a command in a persistent isolated environment with optional structured output and logs.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
command: z.string().describe('The command to execute'),
|
|
42
|
+
cwd: z.string().optional().describe('Working directory for the command'),
|
|
43
|
+
env: z.record(z.string(), z.string()).optional().describe('Environment variables to set'),
|
|
44
|
+
includeOutput: z.boolean().optional().describe('Include structured output messages from SandboxCommand.output()'),
|
|
45
|
+
includeLogs: z.boolean().optional().describe('Include execution logs from SandboxCommand.logs()')
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
async ({command, cwd, env, includeOutput = false, includeLogs = false}: {command: string; cwd?: string; env?: Record<string, string>; includeOutput?: boolean; includeLogs?: boolean}) => {
|
|
49
|
+
try {
|
|
50
|
+
const sandbox = await getPersistentSandbox()
|
|
51
|
+
const cmd = await sandbox.runCommand(command, {cwd, env})
|
|
52
|
+
const result = await cmd.wait()
|
|
53
|
+
const stdout = await cmd.stdout()
|
|
54
|
+
const stderr = await cmd.stderr()
|
|
85
55
|
|
|
86
|
-
|
|
87
|
-
stdout: string;
|
|
88
|
-
stderr: string;
|
|
89
|
-
exitCode: number;
|
|
90
|
-
output?: string;
|
|
91
|
-
logs?: OutputMessage[];
|
|
92
|
-
} = {
|
|
93
|
-
stdout: truncateOutput(stdout, config.MAX_OUTPUT_LENGTH, "stdout"),
|
|
94
|
-
stderr: truncateOutput(stderr, config.MAX_OUTPUT_LENGTH, "stderr"),
|
|
95
|
-
exitCode: result.exitCode,
|
|
96
|
-
};
|
|
56
|
+
const response: {stdout: string; stderr: string; exitCode: number; output?: string; logs?: OutputMessage[]} = {stdout: truncateOutput(stdout, config.MAX_OUTPUT_LENGTH, 'stdout'), stderr: truncateOutput(stderr, config.MAX_OUTPUT_LENGTH, 'stderr'), exitCode: result.exitCode}
|
|
97
57
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
58
|
+
if (includeOutput) {
|
|
59
|
+
response.output = truncateOutput(await cmd.output(), config.MAX_OUTPUT_LENGTH, 'stdout')
|
|
60
|
+
}
|
|
61
|
+
if (includeLogs) {
|
|
62
|
+
const logs: OutputMessage[] = []
|
|
63
|
+
for await (const message of cmd.logs()) {
|
|
64
|
+
logs.push(message)
|
|
65
|
+
}
|
|
66
|
+
response.logs = logs
|
|
67
|
+
}
|
|
108
68
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
return createJsonResponse(response, result.exitCode !== 0)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
return classifyError(error, 'Sandbox error')
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
)
|
|
115
75
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const sandbox = await getPersistentSandbox();
|
|
125
|
-
return createJsonResponse({ domain: sandbox.domain });
|
|
126
|
-
} catch (error) {
|
|
127
|
-
return classifyError(error, "Domain error");
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
);
|
|
76
|
+
server.registerTool('bash_sandbox_domain', {description: 'Get the current sandbox domain or identifier.', inputSchema: {}}, async () => {
|
|
77
|
+
try {
|
|
78
|
+
const sandbox = await getPersistentSandbox()
|
|
79
|
+
return createJsonResponse({domain: sandbox.domain})
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return classifyError(error, 'Domain error')
|
|
82
|
+
}
|
|
83
|
+
})
|
|
131
84
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
inputSchema: {
|
|
137
|
-
files: z.record(z.string(), z.string()).describe("Files to write (path -> content)"),
|
|
138
|
-
},
|
|
139
|
-
},
|
|
140
|
-
async ({ files }: { files: Record<string, string> }) => {
|
|
141
|
-
try {
|
|
142
|
-
const sandbox = await getPersistentSandbox();
|
|
143
|
-
await sandbox.writeFiles(files);
|
|
85
|
+
server.registerTool('bash_sandbox_write_files', {description: 'Write multiple files to the persistent isolated environment at once.', inputSchema: {files: z.record(z.string(), z.string()).describe('Files to write (path -> content)')}}, async ({files}: {files: Record<string, string>}) => {
|
|
86
|
+
try {
|
|
87
|
+
const sandbox = await getPersistentSandbox()
|
|
88
|
+
await sandbox.writeFiles(files)
|
|
144
89
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
);
|
|
90
|
+
return createSuccessResponse(`Successfully wrote ${Object.keys(files).length} file(s): ${Object.keys(files).join(', ')}`)
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return classifyError(error, 'Write error')
|
|
93
|
+
}
|
|
94
|
+
})
|
|
153
95
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
async ({ path, encoding = "utf-8" }: { path: string; encoding?: "utf-8" | "base64" }) => {
|
|
164
|
-
try {
|
|
165
|
-
const sandbox = await getPersistentSandbox();
|
|
166
|
-
const content = await sandbox.readFile(path, encoding);
|
|
96
|
+
server.registerTool(
|
|
97
|
+
'bash_sandbox_read_file',
|
|
98
|
+
{description: 'Read a file from the persistent isolated environment.', inputSchema: {path: z.string().describe('The file path to read'), encoding: z.enum(['utf-8', 'base64']).optional().describe('File encoding (default: utf-8)')}},
|
|
99
|
+
async ({path, encoding = 'utf-8'}: {path: string; encoding?: 'utf-8' | 'base64'}) => {
|
|
100
|
+
try {
|
|
101
|
+
const sandbox = await getPersistentSandbox()
|
|
102
|
+
const content = await sandbox.readFile(path, encoding)
|
|
167
103
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
],
|
|
175
|
-
};
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return classifyError(error, "Read error");
|
|
178
|
-
}
|
|
179
|
-
},
|
|
180
|
-
);
|
|
104
|
+
return {content: [{type: 'text' as const, text: truncateOutput(content, config.MAX_OUTPUT_LENGTH, 'stdout')}]}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return classifyError(error, 'Read error')
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
)
|
|
181
110
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.boolean()
|
|
190
|
-
.optional()
|
|
191
|
-
.describe("Create parent directories if needed (default: true)"),
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
async ({ path, recursive = true }: { path: string; recursive?: boolean }) => {
|
|
195
|
-
try {
|
|
196
|
-
const sandbox = await getPersistentSandbox();
|
|
197
|
-
await sandbox.mkDir(path, { recursive });
|
|
111
|
+
server.registerTool(
|
|
112
|
+
'bash_sandbox_mkdir',
|
|
113
|
+
{description: 'Create a directory in the persistent isolated environment.', inputSchema: {path: z.string().describe('The directory path to create'), recursive: z.boolean().optional().describe('Create parent directories if needed (default: true)')}},
|
|
114
|
+
async ({path, recursive = true}: {path: string; recursive?: boolean}) => {
|
|
115
|
+
try {
|
|
116
|
+
const sandbox = await getPersistentSandbox()
|
|
117
|
+
await sandbox.mkDir(path, {recursive})
|
|
198
118
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
119
|
+
return createSuccessResponse(`Successfully created directory: ${path}`)
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return classifyError(error, 'Mkdir error')
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
)
|
|
205
125
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
try {
|
|
215
|
-
await resetPersistentSandbox();
|
|
216
|
-
return createSuccessResponse("Sandbox environment has been stopped and cleaned up.");
|
|
217
|
-
} catch (error) {
|
|
218
|
-
return classifyError(error, "Stop error");
|
|
219
|
-
}
|
|
220
|
-
},
|
|
221
|
-
);
|
|
126
|
+
server.registerTool('bash_sandbox_stop', {description: 'Stop and clean up the persistent isolated environment, releasing all resources. Use bash_sandbox_reset to just clear state.', inputSchema: {}}, async () => {
|
|
127
|
+
try {
|
|
128
|
+
await resetPersistentSandbox()
|
|
129
|
+
return createSuccessResponse('Sandbox environment has been stopped and cleaned up.')
|
|
130
|
+
} catch (error) {
|
|
131
|
+
return classifyError(error, 'Stop error')
|
|
132
|
+
}
|
|
133
|
+
})
|
|
222
134
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
await resetPersistentSandbox();
|
|
232
|
-
return createSuccessResponse("Sandbox environment has been reset.");
|
|
233
|
-
} catch (error) {
|
|
234
|
-
return classifyError(error, "Reset error");
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
);
|
|
135
|
+
server.registerTool('bash_sandbox_reset', {description: 'Reset the persistent isolated environment, clearing all files and state.', inputSchema: {}}, async () => {
|
|
136
|
+
try {
|
|
137
|
+
await resetPersistentSandbox()
|
|
138
|
+
return createSuccessResponse('Sandbox environment has been reset.')
|
|
139
|
+
} catch (error) {
|
|
140
|
+
return classifyError(error, 'Reset error')
|
|
141
|
+
}
|
|
142
|
+
})
|
|
238
143
|
}
|
package/src/types.ts
CHANGED
package/src/utils/index.ts
CHANGED
|
@@ -2,83 +2,48 @@
|
|
|
2
2
|
* Utility functions for just-bash-mcp
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {config} from '../config/index.ts'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Truncate output to maximum length with notification message
|
|
9
9
|
*/
|
|
10
|
-
export function truncateOutput(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return output;
|
|
17
|
-
}
|
|
18
|
-
const truncatedLength = output.length - maxLength;
|
|
19
|
-
return `${output.slice(0, maxLength)}\n\n[${streamName} truncated: ${truncatedLength} characters removed]`;
|
|
10
|
+
export function truncateOutput(output: string, maxLength: number, streamName: 'stdout' | 'stderr'): string {
|
|
11
|
+
if (output.length <= maxLength) {
|
|
12
|
+
return output
|
|
13
|
+
}
|
|
14
|
+
const truncatedLength = output.length - maxLength
|
|
15
|
+
return `${output.slice(0, maxLength)}\n\n[${streamName} truncated: ${truncatedLength} characters removed]`
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
/**
|
|
23
19
|
* Create a successful MCP tool response
|
|
24
20
|
*/
|
|
25
|
-
export function createSuccessResponse(text: string): {
|
|
26
|
-
|
|
27
|
-
} {
|
|
28
|
-
return {
|
|
29
|
-
content: [{ type: "text" as const, text }],
|
|
30
|
-
};
|
|
21
|
+
export function createSuccessResponse(text: string): {content: Array<{type: 'text'; text: string}>} {
|
|
22
|
+
return {content: [{type: 'text' as const, text}]}
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
/**
|
|
34
26
|
* Create an error MCP tool response
|
|
35
27
|
*/
|
|
36
|
-
export function createErrorResponse(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
): { content: Array<{ type: "text"; text: string }>; isError: true } {
|
|
40
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
41
|
-
return {
|
|
42
|
-
content: [{ type: "text" as const, text: `${prefix}: ${message}` }],
|
|
43
|
-
isError: true,
|
|
44
|
-
};
|
|
28
|
+
export function createErrorResponse(error: unknown, prefix = 'Error'): {content: Array<{type: 'text'; text: string}>; isError: true} {
|
|
29
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
30
|
+
return {content: [{type: 'text' as const, text: `${prefix}: ${message}`}], isError: true}
|
|
45
31
|
}
|
|
46
32
|
|
|
47
33
|
/**
|
|
48
34
|
* Create a JSON MCP tool response
|
|
49
35
|
*/
|
|
50
|
-
export function createJsonResponse(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
isError?: boolean;
|
|
57
|
-
} = {
|
|
58
|
-
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
59
|
-
};
|
|
60
|
-
if (isError) {
|
|
61
|
-
response.isError = true;
|
|
62
|
-
}
|
|
63
|
-
return response;
|
|
36
|
+
export function createJsonResponse(data: unknown, isError = false): {content: Array<{type: 'text'; text: string}>; isError?: boolean} {
|
|
37
|
+
const response: {content: Array<{type: 'text'; text: string}>; isError?: boolean} = {content: [{type: 'text' as const, text: JSON.stringify(data, null, 2)}]}
|
|
38
|
+
if (isError) {
|
|
39
|
+
response.isError = true
|
|
40
|
+
}
|
|
41
|
+
return response
|
|
64
42
|
}
|
|
65
43
|
|
|
66
44
|
/**
|
|
67
45
|
* Format bash execution result for MCP response
|
|
68
46
|
*/
|
|
69
|
-
export function formatExecResult(result: {
|
|
70
|
-
|
|
71
|
-
stderr: string;
|
|
72
|
-
exitCode: number;
|
|
73
|
-
env?: Record<string, string>;
|
|
74
|
-
}): { content: Array<{ type: "text"; text: string }>; isError?: boolean } {
|
|
75
|
-
return createJsonResponse(
|
|
76
|
-
{
|
|
77
|
-
stdout: truncateOutput(result.stdout, config.MAX_OUTPUT_LENGTH, "stdout"),
|
|
78
|
-
stderr: truncateOutput(result.stderr, config.MAX_OUTPUT_LENGTH, "stderr"),
|
|
79
|
-
exitCode: result.exitCode,
|
|
80
|
-
...(result.env && { env: result.env }),
|
|
81
|
-
},
|
|
82
|
-
result.exitCode !== 0,
|
|
83
|
-
);
|
|
47
|
+
export function formatExecResult(result: {stdout: string; stderr: string; exitCode: number; env?: Record<string, string>}): {content: Array<{type: 'text'; text: string}>; isError?: boolean} {
|
|
48
|
+
return createJsonResponse({stdout: truncateOutput(result.stdout, config.MAX_OUTPUT_LENGTH, 'stdout'), stderr: truncateOutput(result.stderr, config.MAX_OUTPUT_LENGTH, 'stderr'), exitCode: result.exitCode, ...(result.env && {env: result.env})}, result.exitCode !== 0)
|
|
84
49
|
}
|