Skip to main content
Glama
kaznak

Shell Command MCP Server

by kaznak

execute-bash-script-async

Execute bash scripts asynchronously to run multiple commands in parallel, reducing waiting time by not requiring completion before starting new processes.

Instructions

This tool executes shell scripts asynchronously in bash. Executing each command creates a new bash process. Synchronous execution requires to wait the scripts completed. Asynchronous execution makes it possible to execute multiple scripts in parallel. You can reduce waiting time by planning in advance which shell scripts need to be executed and executing them in parallel. Avoid using execute-bash-script-sync tool unless you really need to, and use this execute-bash-script-async tool whenever possible.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
commandYesThe bash script to execute
optionsYes

Implementation Reference

  • The MCP tool handler function that orchestrates asynchronous bash script execution, progress notifications via notifications/tools/progress, and error handling.
    async ({ command, options = {} }, extra) => {
      try {
        // outputModeを取得、デフォルト値は'complete'
        const outputMode = options?.outputMode || 'complete';
        // 進捗トークンを生成
        const progressToken = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
    
        const onOutput = (data: string, isStderr: boolean) => {
          const fsMark = isStderr ? 'stderr' : 'stdout';
          server.notification({
            method: 'notifications/tools/progress',
            params: {
              progressToken,
              result: {
                content: [
                  {
                    type: 'text' as const,
                    text: `${fsMark}: ${data}`,
                  },
                ],
                isComplete: false,
              },
            },
          });
        };
    
        // バックグラウンドでコマンドを実行
        executeCommand(command, {
          ...options,
          onOutput,
        })
          .then(({ exitCode }) => {
            // 完了通知を送信
            server.notification({
              method: 'notifications/tools/progress',
              params: {
                progressToken,
                result: {
                  content: [
                    {
                      type: 'text' as const,
                      text: `exitCode: ${exitCode}`,
                    },
                  ],
                  isComplete: true,
                },
              },
            });
          })
          .catch((error) => {
            // エラー通知を送信
            server.notification({
              method: 'notifications/tools/progress',
              params: {
                progressToken,
                result: {
                  content: [
                    {
                      type: 'text' as const,
                      text: `Error: ${error instanceof Error ? error.message : String(error)}`,
                    },
                  ],
                  isComplete: true,
                  isError: true,
                },
              },
            });
          });
    
        // 初期レスポンスを返す
        return {
          content: [
            {
              type: 'text' as const,
              text: `# Command execution started with output mode, ${outputMode}`,
            },
          ],
          progressToken,
        };
      } catch (error) {
        return {
          content: [
            {
              type: 'text' as const,
              text: `Error: ${error instanceof Error ? error.message : String(error)}`,
            },
          ],
          isError: true,
        };
      }
  • Zod schema defining the input parameters for the tool: command (string), and options (cwd, env, timeout, outputMode).
    export const toolOptionsSchema = {
      command: z.string().describe('The bash script to execute'),
      options: z.object({
        cwd: z.string().optional().describe(`The working directory to execute the script.
    use this option argument to avoid cd command in the first line of the script.
    \`~\` and environment variable is not supported. use absolute path instead.
    `),
        env: z.record(z.string(), z.string()).optional()
          .describe(`The environment variables for the script.
          Set environment variables using this option instead of using export command in the script.
          `),
        timeout: z.number().int().positive().optional().describe(`The timeout in milliseconds.
    Set enough long timeout even if you don't need to set timeout to avoid unexpected blocking.
    `),
        outputMode: z
          .enum(['complete', 'line', 'character', 'chunk'])
          .optional()
          .default(toolOptionsDefaults.outPutMode).describe(`The output mode for the script.
    - complete: Notify when the command is completed
    - line: Notify on each line of output
    - chunk: Notify on each chunk of output
    - character: Notify on each character of output
    `),
      }),
    };
  • The setTool function registers the 'execute-bash-script-async' tool on the MCP server using mcpServer.tool, including name, description, schema, and handler.
    export function setTool(mcpServer: McpServer) {
      // McpServerインスタンスから低レベルのServerインスタンスにアクセスする
      const server: Server = mcpServer.server;
    
      // 単一のツールとして登録
      mcpServer.tool(
        toolName,
        toolDescription,
        toolOptionsSchema,
        // eslint-disable-next-line @typescript-eslint/no-unused-vars
        async ({ command, options = {} }, extra) => {
          try {
            // outputModeを取得、デフォルト値は'complete'
            const outputMode = options?.outputMode || 'complete';
            // 進捗トークンを生成
            const progressToken = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
    
            const onOutput = (data: string, isStderr: boolean) => {
              const fsMark = isStderr ? 'stderr' : 'stdout';
              server.notification({
                method: 'notifications/tools/progress',
                params: {
                  progressToken,
                  result: {
                    content: [
                      {
                        type: 'text' as const,
                        text: `${fsMark}: ${data}`,
                      },
                    ],
                    isComplete: false,
                  },
                },
              });
            };
    
            // バックグラウンドでコマンドを実行
            executeCommand(command, {
              ...options,
              onOutput,
            })
              .then(({ exitCode }) => {
                // 完了通知を送信
                server.notification({
                  method: 'notifications/tools/progress',
                  params: {
                    progressToken,
                    result: {
                      content: [
                        {
                          type: 'text' as const,
                          text: `exitCode: ${exitCode}`,
                        },
                      ],
                      isComplete: true,
                    },
                  },
                });
              })
              .catch((error) => {
                // エラー通知を送信
                server.notification({
                  method: 'notifications/tools/progress',
                  params: {
                    progressToken,
                    result: {
                      content: [
                        {
                          type: 'text' as const,
                          text: `Error: ${error instanceof Error ? error.message : String(error)}`,
                        },
                      ],
                      isComplete: true,
                      isError: true,
                    },
                  },
                });
              });
    
            // 初期レスポンスを返す
            return {
              content: [
                {
                  type: 'text' as const,
                  text: `# Command execution started with output mode, ${outputMode}`,
                },
              ],
              progressToken,
            };
          } catch (error) {
            return {
              content: [
                {
                  type: 'text' as const,
                  text: `Error: ${error instanceof Error ? error.message : String(error)}`,
                },
              ],
              isError: true,
            };
          }
        },
      );
    }
  • Helper function to execute bash command asynchronously with child_process.spawn, supporting output modes (complete, line, chunk, character), timeout, cwd, env.
    export async function executeCommand(
      command: string,
      options: CommandOptions = {},
    ): Promise<CommandResult> {
      const outputMode = (options as CommandOptions).outputMode || toolOptionsDefaults.outPutMode;
      const onOutput = options.onOutput;
    
      return new Promise((resolve, reject) => {
        // 環境変数を設定
        const env = {
          ...process.env,
          ...options.env,
        };
    
        // bashプロセスを起動
        const bash = spawn(shellProgram, [], {
          cwd: options.cwd,
          env,
          stdio: ['pipe', 'pipe', 'pipe'],
        });
    
        const stdoutBuffer = { current: '' }; // バッファのコピーを回避
        const stderrBuffer = { current: '' }; // バッファのコピーを回避
        let timeoutId: NodeJS.Timeout | null = null;
    
        const flushBuffer = () => {
          if (onOutput) {
            if (stdoutBuffer.current) {
              onOutput(stdoutBuffer.current, false);
              stdoutBuffer.current = ''; // バッファをクリア
            }
            if (stderrBuffer.current) {
              onOutput(stderrBuffer.current, true);
              stderrBuffer.current = ''; // バッファをクリア
            }
          }
        };
    
        // 標準出力の処理
        bash.stdout.on('data', (data) => {
          handleOutput(data.toString(), false, outputMode, onOutput, stdoutBuffer);
        });
    
        // 標準エラー出力の処理
        bash.stderr.on('data', (data) => {
          handleOutput(data.toString(), true, outputMode, onOutput, stderrBuffer);
        });
    
        // タイムアウト処理
        if (options.timeout) {
          timeoutId = setTimeout(() => {
            bash.kill();
            reject(new Error(`Command timed out after ${options.timeout}ms`));
          }, options.timeout);
        }
    
        // プロセス終了時の処理
        bash.on('close', (code) => {
          // タイマーをクリア
          if (timeoutId) clearTimeout(timeoutId);
    
          // バッファをフラッシュ
          flushBuffer();
    
          // NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
          console.error('bash process exited with code', code);
          resolve({
            exitCode: code !== null ? code : 1,
          });
        });
    
        bash.on('error', (error) => {
          // タイマーをクリア
          if (timeoutId) clearTimeout(timeoutId);
    
          // バッファをフラッシュ
          flushBuffer();
    
          // NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
          console.error('Failed to start bash process:', error);
          reject(error);
        });
    
        // コマンドを標準入力に書き込み、EOF を送信
        bash.stdin.write(command + '\n');
        bash.stdin.end();
      });
    }
  • src/index.ts:13-14 (registration)
    Top-level registration calls to set both sync and async bash tools on the MCP server instance.
    setSyncTool(server);
    setAsyncTool(server);
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It explains that 'Executing each command creates a new bash process' and describes the asynchronous nature, but lacks details about error handling, security implications, resource consumption, or what the tool returns. It mentions 'Synchronous execution requires to wait the scripts completed' which helps contrast behaviors, but more operational transparency would be beneficial for a tool executing arbitrary shell scripts.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is appropriately sized at 6 sentences. It's front-loaded with the core purpose, then explains the asynchronous nature and benefits, and ends with usage guidance. Some sentences could be more concise (e.g., 'Synchronous execution requires to wait the scripts completed' could be simplified), but overall it's well-structured with minimal redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity of executing arbitrary shell scripts with no annotations and no output schema, the description is incomplete. It explains the asynchronous nature and provides usage guidance, but lacks critical information about security considerations, error handling, return values, or execution environment. For a tool that could have significant side effects, more comprehensive context is needed despite the good usage guidelines.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 50% (only 'command' has a description in the schema, while 'options' and its nested properties lack schema descriptions). The tool description provides no parameter information beyond what's implied by the tool's purpose. The description doesn't mention any parameters, their meanings, or how they affect execution, so it doesn't compensate for the schema coverage gap. With 2 parameters and low schema coverage, this represents a significant documentation gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool 'executes shell scripts asynchronously in bash', providing a specific verb ('executes') and resource ('shell scripts'). It distinguishes from the sibling tool 'execute-bash-script-sync' by mentioning it in the last sentence, though the differentiation is more about usage preference than functional distinction. The purpose is clear but could be more specific about what 'asynchronously' means operationally.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit guidance on when to use this tool versus alternatives. It states 'Avoid using execute-bash-script-sync tool unless you really need to, and use this execute-bash-script-async tool whenever possible.' It also explains the benefit of asynchronous execution for parallel processing and reducing waiting time, giving clear context for when this approach is advantageous.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/kaznak/shell-command-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server