@iflow-mcp/shell-command-mcp 1.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/entrypoint.sh ADDED
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+
3
+ export USER=${1}
4
+ shift
5
+ export HOME=/home/$USER
6
+
7
+ # WORKDIR の uid と gid を調べる
8
+ uid=$(stat -c "%u" $WORKDIR)
9
+ gid=$(stat -c "%g" $WORKDIR)
10
+
11
+ if [ "$uid" -ne 0 ]; then
12
+ if [ "$(id -g $USER)" -ne $gid ]; then
13
+ # ユーザーの gid とカレントディレクトリの gid が異なる場合、
14
+ # ユーザーの gid をカレントディレクトリの gid に変更し、ホームディレクトリの gid も正常化する。
15
+ getent group $gid >/dev/null 2>&1 || groupmod -g $gid $USER
16
+ chgrp -R $gid $HOME
17
+ fi
18
+ if [ "$(id -u $USER)" -ne $uid ]; then
19
+ # ユーザーの uid とカレントディレクトリの uid が異なる場合、
20
+ # ユーザーの uid をカレントディレクトリの uid に変更する。
21
+ # ホームディレクトリは usermod によって正常化される。
22
+ usermod -u $uid $USER
23
+ fi
24
+ fi
25
+
26
+ if [ -z "$(find "$HOME" -mindepth 1 -print -quit)" ]; then
27
+ cp -rp /home/mcp-home-backup/. $HOME
28
+ fi
29
+
30
+ # このスクリプト自体は root で実行されているので、uid/gid 調整済みのユーザーとして指定されたコマンドを実行する。
31
+ exec setpriv --reuid=$USER --regid=$USER --init-groups "$@"
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@iflow-mcp/shell-command-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for executing shell commands in a Docker container",
5
+ "main": "build/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "shell-command-mcp": "build/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "lint": "eslint src/**/*.ts",
13
+ "lint:fix": "eslint src/**/*.ts --fix",
14
+ "format": "prettier -uw .",
15
+ "start": "node build/index.js",
16
+ "dev": "tsx src/index.ts",
17
+ "clean": "rm -rf build"
18
+ },
19
+ "keywords": [
20
+ "mcp",
21
+ "shell",
22
+ "docker",
23
+ "kubernetes"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "dependencies": {
28
+ "@modelcontextprotocol/sdk": "^1.8.0",
29
+ "execa": "^8.0.1",
30
+ "zod": "^3.22.4"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.10.3",
34
+ "@typescript-eslint/eslint-plugin": "^6.13.2",
35
+ "@typescript-eslint/parser": "^6.13.2",
36
+ "eslint": "^8.55.0",
37
+ "eslint-config-prettier": "^9.1.0",
38
+ "eslint-plugin-prettier": "^5.0.1",
39
+ "prettier": "^3.1.0",
40
+ "tsx": "^4.6.2",
41
+ "typescript": "~5.3.3"
42
+ }
43
+ }
@@ -0,0 +1,300 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { z } from 'zod';
4
+ import { spawn } from 'child_process';
5
+
6
+ export const shellProgram = '/usr/bin/bash';
7
+
8
+ export const toolName = 'execute-bash-script-async';
9
+
10
+ export const toolDescription = `This tool executes shell scripts asynchronously in bash.
11
+ Executing each command creates a new bash process.
12
+ Synchronous execution requires to wait the scripts completed.
13
+ Asynchronous execution makes it possible to execute multiple scripts in parallel.
14
+ You can reduce waiting time by planning in advance which shell scripts need to be executed and executing them in parallel.
15
+ Avoid using execute-bash-script-sync tool unless you really need to, and use this execute-bash-script-async tool whenever possible.
16
+ `;
17
+
18
+ export interface CommandOptions {
19
+ cwd?: string;
20
+ env?: Record<string, string>;
21
+ timeout?: number;
22
+ outputMode?: 'complete' | 'line' | 'character' | 'chunk';
23
+ onOutput?: (data: string, isStderr: boolean) => void;
24
+ }
25
+
26
+ export const toolOptionsDefaults = {
27
+ outPutMode: 'complete' as NonNullable<CommandOptions['outputMode']>,
28
+ };
29
+
30
+ export const toolOptionsSchema = {
31
+ command: z.string().describe('The bash script to execute'),
32
+ options: z.object({
33
+ cwd: z.string().optional().describe(`The working directory to execute the script.
34
+ use this option argument to avoid cd command in the first line of the script.
35
+ \`~\` and environment variable is not supported. use absolute path instead.
36
+ `),
37
+ env: z.record(z.string(), z.string()).optional()
38
+ .describe(`The environment variables for the script.
39
+ Set environment variables using this option instead of using export command in the script.
40
+ `),
41
+ timeout: z.number().int().positive().optional().describe(`The timeout in milliseconds.
42
+ Set enough long timeout even if you don't need to set timeout to avoid unexpected blocking.
43
+ `),
44
+ outputMode: z
45
+ .enum(['complete', 'line', 'character', 'chunk'])
46
+ .optional()
47
+ .default(toolOptionsDefaults.outPutMode).describe(`The output mode for the script.
48
+ - complete: Notify when the command is completed
49
+ - line: Notify on each line of output
50
+ - chunk: Notify on each chunk of output
51
+ - character: Notify on each character of output
52
+ `),
53
+ }),
54
+ };
55
+
56
+ export interface CommandResult {
57
+ exitCode: number;
58
+ }
59
+
60
+ /**
61
+ * シェルスクリプト出力ハンドリング関数
62
+ */
63
+ function handleOutput(
64
+ chunk: string,
65
+ isStderr: boolean,
66
+ outputMode: CommandOptions['outputMode'],
67
+ onOutput: CommandOptions['onOutput'] | undefined,
68
+ buffer: { current: string }, // バッファのコピーを回避
69
+ ) {
70
+ if (onOutput) {
71
+ if (outputMode === 'character') {
72
+ // 文字ごとに通知
73
+ for (const char of chunk) {
74
+ onOutput(char, isStderr);
75
+ }
76
+ } else if (outputMode === 'line') {
77
+ // 行ごとに通知
78
+ buffer.current += chunk;
79
+ const lines = buffer.current.split('\n'); // 改行で分割
80
+ const lastLine = lines.pop(); // 最後の行を取得
81
+ for (const line of lines) {
82
+ onOutput(line + '\n', isStderr); // 改行を追加して通知
83
+ }
84
+ if (lastLine !== undefined && chunk.endsWith('\n')) {
85
+ // 最後のデータが改行で終わっている場合は通知
86
+ onOutput(lastLine + '\n', isStderr);
87
+ buffer.current = ''; // バッファをクリア
88
+ } else {
89
+ // 最後のデータが改行で終わっていない場合は、バッファに保持
90
+ buffer.current = lastLine || ''; // 未完成の行を保持
91
+ }
92
+ } else if (outputMode === 'chunk') {
93
+ // チャンク(データ受信単位)ごとに通知
94
+ onOutput(chunk, isStderr);
95
+ } else if (outputMode === 'complete') {
96
+ // complete モードでは通知なし
97
+ buffer.current += chunk;
98
+ } else {
99
+ // 想定外の outputMode の場合に例外をスロー
100
+ throw new Error(`Unsupported outputMode: ${outputMode}`);
101
+ }
102
+ }
103
+ }
104
+
105
+ /**
106
+ * 柔軟な出力モードをサポートするコマンド実行関数
107
+ */
108
+ export async function executeCommand(
109
+ command: string,
110
+ options: CommandOptions = {},
111
+ ): Promise<CommandResult> {
112
+ const outputMode = (options as CommandOptions).outputMode || toolOptionsDefaults.outPutMode;
113
+ const onOutput = options.onOutput;
114
+
115
+ return new Promise((resolve, reject) => {
116
+ // 環境変数を設定
117
+ const env = {
118
+ ...process.env,
119
+ ...options.env,
120
+ };
121
+
122
+ // bashプロセスを起動
123
+ const bash = spawn(shellProgram, [], {
124
+ cwd: options.cwd,
125
+ env,
126
+ stdio: ['pipe', 'pipe', 'pipe'],
127
+ });
128
+
129
+ const stdoutBuffer = { current: '' }; // バッファのコピーを回避
130
+ const stderrBuffer = { current: '' }; // バッファのコピーを回避
131
+ let timeoutId: NodeJS.Timeout | null = null;
132
+
133
+ const flushBuffer = () => {
134
+ if (onOutput) {
135
+ if (stdoutBuffer.current) {
136
+ onOutput(stdoutBuffer.current, false);
137
+ stdoutBuffer.current = ''; // バッファをクリア
138
+ }
139
+ if (stderrBuffer.current) {
140
+ onOutput(stderrBuffer.current, true);
141
+ stderrBuffer.current = ''; // バッファをクリア
142
+ }
143
+ }
144
+ };
145
+
146
+ // 標準出力の処理
147
+ bash.stdout.on('data', (data) => {
148
+ handleOutput(data.toString(), false, outputMode, onOutput, stdoutBuffer);
149
+ });
150
+
151
+ // 標準エラー出力の処理
152
+ bash.stderr.on('data', (data) => {
153
+ handleOutput(data.toString(), true, outputMode, onOutput, stderrBuffer);
154
+ });
155
+
156
+ // タイムアウト処理
157
+ if (options.timeout) {
158
+ timeoutId = setTimeout(() => {
159
+ bash.kill();
160
+ reject(new Error(`Command timed out after ${options.timeout}ms`));
161
+ }, options.timeout);
162
+ }
163
+
164
+ // プロセス終了時の処理
165
+ bash.on('close', (code) => {
166
+ // タイマーをクリア
167
+ if (timeoutId) clearTimeout(timeoutId);
168
+
169
+ // バッファをフラッシュ
170
+ flushBuffer();
171
+
172
+ // NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
173
+ console.error('bash process exited with code', code);
174
+ resolve({
175
+ exitCode: code !== null ? code : 1,
176
+ });
177
+ });
178
+
179
+ bash.on('error', (error) => {
180
+ // タイマーをクリア
181
+ if (timeoutId) clearTimeout(timeoutId);
182
+
183
+ // バッファをフラッシュ
184
+ flushBuffer();
185
+
186
+ // NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
187
+ console.error('Failed to start bash process:', error);
188
+ reject(error);
189
+ });
190
+
191
+ // コマンドを標準入力に書き込み、EOF を送信
192
+ bash.stdin.write(command + '\n');
193
+ bash.stdin.end();
194
+ });
195
+ }
196
+
197
+ // MCPサーバーにコマンド実行ツールを追加する関数
198
+ export function setTool(mcpServer: McpServer) {
199
+ // McpServerインスタンスから低レベルのServerインスタンスにアクセスする
200
+ const server: Server = mcpServer.server;
201
+
202
+ // 単一のツールとして登録
203
+ mcpServer.tool(
204
+ toolName,
205
+ toolDescription,
206
+ toolOptionsSchema,
207
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
208
+ async ({ command, options = {} }, extra) => {
209
+ try {
210
+ // outputModeを取得、デフォルト値は'complete'
211
+ const outputMode = options?.outputMode || 'complete';
212
+ // 進捗トークンを生成
213
+ const progressToken = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
214
+
215
+ const onOutput = (data: string, isStderr: boolean) => {
216
+ const fsMark = isStderr ? 'stderr' : 'stdout';
217
+ server.notification({
218
+ method: 'notifications/tools/progress',
219
+ params: {
220
+ progressToken,
221
+ result: {
222
+ content: [
223
+ {
224
+ type: 'text' as const,
225
+ text: `${fsMark}: ${data}`,
226
+ },
227
+ ],
228
+ isComplete: false,
229
+ },
230
+ },
231
+ });
232
+ };
233
+
234
+ // バックグラウンドでコマンドを実行
235
+ executeCommand(command, {
236
+ ...options,
237
+ onOutput,
238
+ })
239
+ .then(({ exitCode }) => {
240
+ // 完了通知を送信
241
+ server.notification({
242
+ method: 'notifications/tools/progress',
243
+ params: {
244
+ progressToken,
245
+ result: {
246
+ content: [
247
+ {
248
+ type: 'text' as const,
249
+ text: `exitCode: ${exitCode}`,
250
+ },
251
+ ],
252
+ isComplete: true,
253
+ },
254
+ },
255
+ });
256
+ })
257
+ .catch((error) => {
258
+ // エラー通知を送信
259
+ server.notification({
260
+ method: 'notifications/tools/progress',
261
+ params: {
262
+ progressToken,
263
+ result: {
264
+ content: [
265
+ {
266
+ type: 'text' as const,
267
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
268
+ },
269
+ ],
270
+ isComplete: true,
271
+ isError: true,
272
+ },
273
+ },
274
+ });
275
+ });
276
+
277
+ // 初期レスポンスを返す
278
+ return {
279
+ content: [
280
+ {
281
+ type: 'text' as const,
282
+ text: `# Command execution started with output mode, ${outputMode}`,
283
+ },
284
+ ],
285
+ progressToken,
286
+ };
287
+ } catch (error) {
288
+ return {
289
+ content: [
290
+ {
291
+ type: 'text' as const,
292
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`,
293
+ },
294
+ ],
295
+ isError: true,
296
+ };
297
+ }
298
+ },
299
+ );
300
+ }
@@ -0,0 +1,141 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { spawn } from 'child_process';
4
+
5
+ export const shellProgram = '/usr/bin/bash';
6
+
7
+ export const toolName = 'execute-bash-script-sync';
8
+
9
+ export const toolDescription = `This tool executes shell scripts synchronously in bash.
10
+ Executing each command creates a new bash process.
11
+ Synchronous execution requires to wait the scripts completed.
12
+ Asynchronous execution makes it possible to execute multiple scripts in parallel.
13
+ You can reduce waiting time by planning in advance which shell scripts need to be executed and executing them in parallel.
14
+ Avoid using this execute-bash-script-sync tool unless you really need to, and use the execute-bash-script-async tool whenever possible.
15
+ `;
16
+
17
+ export interface CommandOptions {
18
+ cwd?: string;
19
+ env?: Record<string, string>;
20
+ timeout?: number;
21
+ }
22
+
23
+ export const toolOptionsSchema = {
24
+ command: z.string().describe('The bash script to execute'),
25
+ options: z.object({
26
+ cwd: z.string().optional().describe(`The working directory to execute the script.
27
+ use this option argument to avoid cd command in the first line of the script.
28
+ \`~\` and environment variable is not supported. use absolute path instead.
29
+ `),
30
+ env: z.record(z.string(), z.string()).optional()
31
+ .describe(`The environment variables for the script.
32
+ Set environment variables using this option instead of using export command in the script.
33
+ `),
34
+ timeout: z.number().int().positive().optional().describe(`The timeout in milliseconds.
35
+ Set enough long timeout even if you don't need to set timeout to avoid unexpected blocking.
36
+ `),
37
+ }),
38
+ };
39
+
40
+ export interface CommandResult {
41
+ stdout: string;
42
+ stderr: string;
43
+ exitCode: number;
44
+ }
45
+
46
+ /**
47
+ * Execute a command using bash and return the result
48
+ *
49
+ * Each command execution spawn a new bash process.
50
+ * This implementation causes overhead but is simple and isolated.
51
+ */
52
+ export async function executeCommand(
53
+ command: string,
54
+ options: CommandOptions = {},
55
+ ): Promise<CommandResult> {
56
+ return new Promise((resolve, reject) => {
57
+ // 環境変数を設定
58
+ const env = {
59
+ ...process.env,
60
+ ...options.env,
61
+ };
62
+
63
+ // bashプロセスを起動
64
+ const bash = spawn(shellProgram, [], {
65
+ cwd: options.cwd,
66
+ env,
67
+ stdio: ['pipe', 'pipe', 'pipe'],
68
+ });
69
+
70
+ let stdout = '';
71
+ let stderr = '';
72
+ let timeoutId: NodeJS.Timeout | null = null;
73
+
74
+ // 標準出力の収集
75
+ bash.stdout.on('data', (data) => {
76
+ stdout += data.toString();
77
+ });
78
+
79
+ // 標準エラー出力の収集
80
+ bash.stderr.on('data', (data) => {
81
+ stderr += data.toString();
82
+ });
83
+
84
+ // タイムアウト処理
85
+ if (options.timeout) {
86
+ timeoutId = setTimeout(() => {
87
+ bash.kill();
88
+ reject(new Error(`Command timed out after ${options.timeout}ms`));
89
+ }, options.timeout);
90
+ }
91
+
92
+ // プロセス終了時の処理
93
+ bash.on('close', (code) => {
94
+ if (timeoutId) clearTimeout(timeoutId);
95
+
96
+ console.error('bash process exited with code', code);
97
+ resolve({
98
+ stdout,
99
+ stderr,
100
+ exitCode: code !== null ? code : 1,
101
+ });
102
+ });
103
+
104
+ bash.on('error', (error) => {
105
+ if (timeoutId) clearTimeout(timeoutId);
106
+
107
+ console.error('Failed to start bash process:', error);
108
+ reject(error);
109
+ });
110
+
111
+ // コマンドを標準入力に書き込み、EOF を送信
112
+ bash.stdin.write(command + '\n');
113
+ bash.stdin.end();
114
+ });
115
+ }
116
+
117
+ // Execute a shell command
118
+ export function setTool(server: McpServer) {
119
+ server.tool(toolName, toolDescription, toolOptionsSchema, async ({ command, options }) => {
120
+ const { stdout, stderr, exitCode } = await executeCommand(command, options);
121
+ return {
122
+ content: [
123
+ {
124
+ type: 'text',
125
+ text: `stdout: ${stdout}`,
126
+ resource: undefined,
127
+ },
128
+ {
129
+ type: 'text',
130
+ text: `stderr: ${stderr}`,
131
+ resource: undefined,
132
+ },
133
+ {
134
+ type: 'text',
135
+ text: `exitCode: ${exitCode}`,
136
+ resource: undefined,
137
+ },
138
+ ],
139
+ };
140
+ });
141
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { setTool as setSyncTool } from './execute-bash-script-sync.js';
4
+ import { setTool as setAsyncTool } from './execute-bash-script-async.js';
5
+
6
+ // Create an MCP server
7
+ export const server = new McpServer({
8
+ name: 'shell-command-mcp',
9
+ // TODO change to llm-workspace or something
10
+ version: '1.0.0',
11
+ });
12
+
13
+ setSyncTool(server);
14
+ setAsyncTool(server);
15
+
16
+ async function main() {
17
+ try {
18
+ // Start receiving messages on stdin and sending messages on stdout
19
+ const transport = new StdioServerTransport();
20
+ await server.connect(transport);
21
+ console.error('Shell Command MCP Server started');
22
+ } catch (error) {
23
+ console.error('Fatal error starting server:', error);
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ main();
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "./build",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules"]
17
+ }