@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/.dockleignore +15 -0
- package/.eslintrc.json +23 -0
- package/.github/workflows/docker-build.yaml +102 -0
- package/.prettierignore +2 -0
- package/.prettierrc +7 -0
- package/.prompt.yaml +542 -0
- package/Dockerfile +120 -0
- package/LICENSE +21 -0
- package/Makefile +8 -0
- package/README.md +96 -0
- package/build/execute-bash-script-async.js +255 -0
- package/build/execute-bash-script-sync.js +111 -0
- package/build/index.js +26 -0
- package/client-sequence-example.json +9 -0
- package/docker-compose.yaml +17 -0
- package/entrypoint.sh +31 -0
- package/package.json +43 -0
- package/src/execute-bash-script-async.ts +300 -0
- package/src/execute-bash-script-sync.ts +141 -0
- package/src/index.ts +28 -0
- package/tsconfig.json +17 -0
package/Dockerfile
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
FROM ubuntu:22.04
|
|
2
|
+
|
|
3
|
+
# Set non-interactive mode for apt
|
|
4
|
+
ENV DEBIAN_FRONTEND=noninteractive
|
|
5
|
+
ENV NODE_VERSION=22.14.0
|
|
6
|
+
|
|
7
|
+
# Install basic utilities and dependencies
|
|
8
|
+
RUN apt-get update && apt-get install -y \
|
|
9
|
+
tzdata \
|
|
10
|
+
curl \
|
|
11
|
+
wget \
|
|
12
|
+
git \
|
|
13
|
+
gnupg \
|
|
14
|
+
lsb-release \
|
|
15
|
+
software-properties-common \
|
|
16
|
+
apt-transport-https \
|
|
17
|
+
ca-certificates \
|
|
18
|
+
unzip \
|
|
19
|
+
zip \
|
|
20
|
+
vim \
|
|
21
|
+
nano \
|
|
22
|
+
jq \
|
|
23
|
+
netcat \
|
|
24
|
+
iputils-ping \
|
|
25
|
+
dnsutils \
|
|
26
|
+
net-tools \
|
|
27
|
+
sudo \
|
|
28
|
+
python3 \
|
|
29
|
+
python3-pip \
|
|
30
|
+
ssh \
|
|
31
|
+
openssl \
|
|
32
|
+
build-essential \
|
|
33
|
+
--no-install-recommends \
|
|
34
|
+
&& apt-get clean \
|
|
35
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
36
|
+
|
|
37
|
+
# Install Kubernetes tools
|
|
38
|
+
# kubectl (latest stable version)
|
|
39
|
+
RUN curl -fsSL https://dl.k8s.io/release/stable.txt \
|
|
40
|
+
| xargs -I {} curl -fsSL https://dl.k8s.io/release/{}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl \
|
|
41
|
+
&& chmod +x /usr/local/bin/kubectl
|
|
42
|
+
|
|
43
|
+
# helm (latest stable version)
|
|
44
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
45
|
+
&& curl -fsSL https://api.github.com/repos/helm/helm/releases/latest \
|
|
46
|
+
| jq -r '.tag_name' \
|
|
47
|
+
| xargs -I {} curl -fsSL https://get.helm.sh/helm-{}-linux-amd64.tar.gz -o helm.tar.gz \
|
|
48
|
+
&& tar -zxvf helm.tar.gz \
|
|
49
|
+
&& mv linux-amd64/helm /usr/local/bin/helm \
|
|
50
|
+
&& cd .. && rm -rf /root/tmp
|
|
51
|
+
|
|
52
|
+
# kustomize (latest stable version)
|
|
53
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
54
|
+
&& curl -fsSL https://api.github.com/repos/kubernetes-sigs/kustomize/releases/latest | jq -r '.tag_name' | sed 's/kustomize\///' | xargs -I {} curl -fsSL https://github.com/kubernetes-sigs/kustomize/releases/download/kustomize%2F{}/kustomize_{}_linux_amd64.tar.gz -o kustomize.tar.gz \
|
|
55
|
+
&& tar -zxvf kustomize.tar.gz \
|
|
56
|
+
&& mv kustomize /usr/local/bin/ \
|
|
57
|
+
&& cd .. && rm -rf /root/tmp
|
|
58
|
+
|
|
59
|
+
# Install additional tools like k9s (latest stable version)
|
|
60
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
61
|
+
&& curl -fsSL https://api.github.com/repos/derailed/k9s/releases/latest \
|
|
62
|
+
| jq -r '.tag_name' \
|
|
63
|
+
| xargs -I {} curl -fsSL https://github.com/derailed/k9s/releases/download/{}/k9s_Linux_amd64.tar.gz -o k9s.tar.gz \
|
|
64
|
+
&& tar -zxvf k9s.tar.gz \
|
|
65
|
+
&& mv k9s /usr/local/bin/ \
|
|
66
|
+
&& rm k9s.tar.gz LICENSE README.md 2>/dev/null || true \
|
|
67
|
+
&& cd .. && rm -rf /root/tmp
|
|
68
|
+
|
|
69
|
+
# Install helmfile
|
|
70
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
71
|
+
&& curl -fsSL https://github.com/helmfile/helmfile/releases/download/v0.171.0/helmfile_0.171.0_linux_amd64.tar.gz -o helmfile.tar.gz \
|
|
72
|
+
&& tar -zxvf helmfile.tar.gz \
|
|
73
|
+
&& mv helmfile /usr/local/bin/ \
|
|
74
|
+
&& cd .. && rm -rf /root/tmp
|
|
75
|
+
|
|
76
|
+
# Install sops
|
|
77
|
+
RUN curl -fsSL https://github.com/getsops/sops/releases/download/v3.9.4/sops-v3.9.4.linux.amd64 -o /usr/local/bin/sops \
|
|
78
|
+
&& chmod +x /usr/local/bin/sops
|
|
79
|
+
|
|
80
|
+
# Install age
|
|
81
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
82
|
+
&& curl -fsSL https://github.com/FiloSottile/age/releases/download/v1.2.1/age-v1.2.1-linux-amd64.tar.gz -o age.tar.gz \
|
|
83
|
+
&& tar -zxvf age.tar.gz \
|
|
84
|
+
&& mv age/age /usr/local/bin/ \
|
|
85
|
+
&& mv age/age-keygen /usr/local/bin/ \
|
|
86
|
+
&& cd .. && rm -rf /root/tmp
|
|
87
|
+
|
|
88
|
+
# Node.jsのバイナリをダウンロードしてインストール
|
|
89
|
+
RUN mkdir -p /root/tmp && cd /root/tmp \
|
|
90
|
+
&& ARCH=$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x64/') \
|
|
91
|
+
&& curl -fsSLO --compressed "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
|
|
92
|
+
&& tar -xJf "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
|
|
93
|
+
&& rm "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
|
|
94
|
+
&& npm install -g npm@11.2.0 \
|
|
95
|
+
&& ln -s /usr/local/bin/node /usr/local/bin/nodejs \
|
|
96
|
+
&& cd .. && rm -rf /root/tmp
|
|
97
|
+
|
|
98
|
+
WORKDIR /mcpserver
|
|
99
|
+
COPY . .
|
|
100
|
+
RUN npm install && npm run build
|
|
101
|
+
|
|
102
|
+
# Create a non-root user to run the MCP server
|
|
103
|
+
RUN useradd -m -s /bin/bash mcp
|
|
104
|
+
|
|
105
|
+
USER mcp
|
|
106
|
+
RUN helm plugin install https://github.com/databus23/helm-diff \
|
|
107
|
+
&& helm plugin install https://github.com/aslafy-z/helm-git \
|
|
108
|
+
&& helm plugin install https://github.com/hypnoglow/helm-s3.git \
|
|
109
|
+
&& helm plugin install https://github.com/jkroepke/helm-secrets
|
|
110
|
+
|
|
111
|
+
USER root
|
|
112
|
+
|
|
113
|
+
# Copy default home directory contents if the home directory is empty
|
|
114
|
+
RUN cp -rp /home/mcp/. /home/mcp-home-backup
|
|
115
|
+
|
|
116
|
+
WORKDIR /home/mcp
|
|
117
|
+
ENV WORKDIR=/home/mcp
|
|
118
|
+
|
|
119
|
+
# Command to run the MCP server
|
|
120
|
+
CMD ["/mcpserver/entrypoint.sh", "mcp", "node", "/mcpserver/build/index.js"]
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nakamura Kazutaka
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/Makefile
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# *OBSOLETE*
|
|
2
|
+
|
|
3
|
+
I recommend using Claude Code by running `claude mcp serve` instead of this MCP server.
|
|
4
|
+
I have created [ai-agent-workspace](https://github.com/kaznak/container-images/tree/main/ai-agent-workspace) as a container to run Claude Code.
|
|
5
|
+
Please use it as needed.
|
|
6
|
+
|
|
7
|
+
# Shell Command MCP Server
|
|
8
|
+
|
|
9
|
+
This is an MCP (Model Context Protocol) server that allows executing shell commands within a Docker container. It provides a secure and isolated workspace for running commands without giving access to the host Docker daemon.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Run shell scripts through a simple MCP interface
|
|
14
|
+
- synchronous execution
|
|
15
|
+
- asynchronous execution with 4 different modes
|
|
16
|
+
- complete: notify when the command is completed
|
|
17
|
+
- line: notify on each line of output
|
|
18
|
+
- chunk: notify on each chunk of output
|
|
19
|
+
- character: notify on each character of output
|
|
20
|
+
- Kubernetes tools included: kubectl, helm, kustomize, hemfile
|
|
21
|
+
- Isolated Docker container environment with non-root user
|
|
22
|
+
- host-container userid/groupid mapping implemented. this allows the container to run as the same user as the host, ensuring that files created by the container have the same ownership and permissions as those created by the host.
|
|
23
|
+
- mount a host directory to the container /home/mcp directory for persistence. it become the home directory the AI works in.
|
|
24
|
+
- if the host directory is empty, the initial files will be copied form the backup in the container.
|
|
25
|
+
|
|
26
|
+
## Design Philosophy
|
|
27
|
+
|
|
28
|
+
This MCP server provides AI with a workspace similar to that of a human.
|
|
29
|
+
Authorization is limited not by MCP functions, but by container isolation and external authorization restrictions.
|
|
30
|
+
|
|
31
|
+
It provides more general tools such as shell script execution, so that they can be used without specialized knowledge of tool use.
|
|
32
|
+
|
|
33
|
+
The server implementation is kept as simple as possible to facilitate code auditing.
|
|
34
|
+
|
|
35
|
+
## Getting Started
|
|
36
|
+
|
|
37
|
+
### Prerequisites
|
|
38
|
+
|
|
39
|
+
- Docker
|
|
40
|
+
|
|
41
|
+
### Usage with Claude for Desktop
|
|
42
|
+
|
|
43
|
+
Add the following configuration to your Claude for Desktop configuration file.
|
|
44
|
+
|
|
45
|
+
MacOS:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
"shell-command": {
|
|
49
|
+
"command": "docker",
|
|
50
|
+
"args": [
|
|
51
|
+
"run",
|
|
52
|
+
"--rm",
|
|
53
|
+
"-i",
|
|
54
|
+
"--mount",
|
|
55
|
+
"type=bind,src=/Users/user-name/MCPHome,dst=/home/mcp",
|
|
56
|
+
"ghcr.io/kaznak/shell-command-mcp:latest"
|
|
57
|
+
]
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Replace `/Users/user-name/ClaudeWorks` with the directory you want to make available to the container.
|
|
62
|
+
|
|
63
|
+
Windows:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
"shell-command": {
|
|
67
|
+
"command": "docker",
|
|
68
|
+
"args": [
|
|
69
|
+
"run",
|
|
70
|
+
"--rm",
|
|
71
|
+
"-i",
|
|
72
|
+
"--mount",
|
|
73
|
+
"type=bind,src=\\\\wsl.localhost\\Ubuntu\\home\\user-name\\MCPHome,dst=/home/mcp",
|
|
74
|
+
"ghcr.io/kaznak/shell-command-mcp:latest"
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Feed some prompts
|
|
80
|
+
|
|
81
|
+
To Operate the files in the mounted directory.
|
|
82
|
+
|
|
83
|
+
## Available MCP Tools
|
|
84
|
+
|
|
85
|
+
- [execute-bash-script-sync](./src/execute-bash-script-sync.ts)
|
|
86
|
+
- [execute-bash-script-async](./src/execute-bash-script-async.ts)
|
|
87
|
+
|
|
88
|
+
## Security Considerations
|
|
89
|
+
|
|
90
|
+
- The MCP server runs as a non-root user within the container
|
|
91
|
+
- The container does not have access to the host Docker daemon
|
|
92
|
+
- User workspace is mounted from the host for persistence
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
export const shellProgram = '/usr/bin/bash';
|
|
4
|
+
export const toolName = 'execute-bash-script-async';
|
|
5
|
+
export const toolDescription = `This tool executes shell scripts asynchronously in bash.
|
|
6
|
+
Executing each command creates a new bash process.
|
|
7
|
+
Synchronous execution requires to wait the scripts completed.
|
|
8
|
+
Asynchronous execution makes it possible to execute multiple scripts in parallel.
|
|
9
|
+
You can reduce waiting time by planning in advance which shell scripts need to be executed and executing them in parallel.
|
|
10
|
+
Avoid using execute-bash-script-sync tool unless you really need to, and use this execute-bash-script-async tool whenever possible.
|
|
11
|
+
`;
|
|
12
|
+
export const toolOptionsDefaults = {
|
|
13
|
+
outPutMode: 'complete',
|
|
14
|
+
};
|
|
15
|
+
export const toolOptionsSchema = {
|
|
16
|
+
command: z.string().describe('The bash script to execute'),
|
|
17
|
+
options: z.object({
|
|
18
|
+
cwd: z.string().optional().describe(`The working directory to execute the script.
|
|
19
|
+
use this option argument to avoid cd command in the first line of the script.
|
|
20
|
+
\`~\` and environment variable is not supported. use absolute path instead.
|
|
21
|
+
`),
|
|
22
|
+
env: z.record(z.string(), z.string()).optional()
|
|
23
|
+
.describe(`The environment variables for the script.
|
|
24
|
+
Set environment variables using this option instead of using export command in the script.
|
|
25
|
+
`),
|
|
26
|
+
timeout: z.number().int().positive().optional().describe(`The timeout in milliseconds.
|
|
27
|
+
Set enough long timeout even if you don't need to set timeout to avoid unexpected blocking.
|
|
28
|
+
`),
|
|
29
|
+
outputMode: z
|
|
30
|
+
.enum(['complete', 'line', 'character', 'chunk'])
|
|
31
|
+
.optional()
|
|
32
|
+
.default(toolOptionsDefaults.outPutMode).describe(`The output mode for the script.
|
|
33
|
+
- complete: Notify when the command is completed
|
|
34
|
+
- line: Notify on each line of output
|
|
35
|
+
- chunk: Notify on each chunk of output
|
|
36
|
+
- character: Notify on each character of output
|
|
37
|
+
`),
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* シェルスクリプト出力ハンドリング関数
|
|
42
|
+
*/
|
|
43
|
+
function handleOutput(chunk, isStderr, outputMode, onOutput, buffer) {
|
|
44
|
+
if (onOutput) {
|
|
45
|
+
if (outputMode === 'character') {
|
|
46
|
+
// 文字ごとに通知
|
|
47
|
+
for (const char of chunk) {
|
|
48
|
+
onOutput(char, isStderr);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else if (outputMode === 'line') {
|
|
52
|
+
// 行ごとに通知
|
|
53
|
+
buffer.current += chunk;
|
|
54
|
+
const lines = buffer.current.split('\n'); // 改行で分割
|
|
55
|
+
const lastLine = lines.pop(); // 最後の行を取得
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
onOutput(line + '\n', isStderr); // 改行を追加して通知
|
|
58
|
+
}
|
|
59
|
+
if (lastLine !== undefined && chunk.endsWith('\n')) {
|
|
60
|
+
// 最後のデータが改行で終わっている場合は通知
|
|
61
|
+
onOutput(lastLine + '\n', isStderr);
|
|
62
|
+
buffer.current = ''; // バッファをクリア
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// 最後のデータが改行で終わっていない場合は、バッファに保持
|
|
66
|
+
buffer.current = lastLine || ''; // 未完成の行を保持
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else if (outputMode === 'chunk') {
|
|
70
|
+
// チャンク(データ受信単位)ごとに通知
|
|
71
|
+
onOutput(chunk, isStderr);
|
|
72
|
+
}
|
|
73
|
+
else if (outputMode === 'complete') {
|
|
74
|
+
// complete モードでは通知なし
|
|
75
|
+
buffer.current += chunk;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
// 想定外の outputMode の場合に例外をスロー
|
|
79
|
+
throw new Error(`Unsupported outputMode: ${outputMode}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 柔軟な出力モードをサポートするコマンド実行関数
|
|
85
|
+
*/
|
|
86
|
+
export async function executeCommand(command, options = {}) {
|
|
87
|
+
const outputMode = options.outputMode || toolOptionsDefaults.outPutMode;
|
|
88
|
+
const onOutput = options.onOutput;
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
// 環境変数を設定
|
|
91
|
+
const env = {
|
|
92
|
+
...process.env,
|
|
93
|
+
...options.env,
|
|
94
|
+
};
|
|
95
|
+
// bashプロセスを起動
|
|
96
|
+
const bash = spawn(shellProgram, [], {
|
|
97
|
+
cwd: options.cwd,
|
|
98
|
+
env,
|
|
99
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
const stdoutBuffer = { current: '' }; // バッファのコピーを回避
|
|
102
|
+
const stderrBuffer = { current: '' }; // バッファのコピーを回避
|
|
103
|
+
let timeoutId = null;
|
|
104
|
+
const flushBuffer = () => {
|
|
105
|
+
if (onOutput) {
|
|
106
|
+
if (stdoutBuffer.current) {
|
|
107
|
+
onOutput(stdoutBuffer.current, false);
|
|
108
|
+
stdoutBuffer.current = ''; // バッファをクリア
|
|
109
|
+
}
|
|
110
|
+
if (stderrBuffer.current) {
|
|
111
|
+
onOutput(stderrBuffer.current, true);
|
|
112
|
+
stderrBuffer.current = ''; // バッファをクリア
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
// 標準出力の処理
|
|
117
|
+
bash.stdout.on('data', (data) => {
|
|
118
|
+
handleOutput(data.toString(), false, outputMode, onOutput, stdoutBuffer);
|
|
119
|
+
});
|
|
120
|
+
// 標準エラー出力の処理
|
|
121
|
+
bash.stderr.on('data', (data) => {
|
|
122
|
+
handleOutput(data.toString(), true, outputMode, onOutput, stderrBuffer);
|
|
123
|
+
});
|
|
124
|
+
// タイムアウト処理
|
|
125
|
+
if (options.timeout) {
|
|
126
|
+
timeoutId = setTimeout(() => {
|
|
127
|
+
bash.kill();
|
|
128
|
+
reject(new Error(`Command timed out after ${options.timeout}ms`));
|
|
129
|
+
}, options.timeout);
|
|
130
|
+
}
|
|
131
|
+
// プロセス終了時の処理
|
|
132
|
+
bash.on('close', (code) => {
|
|
133
|
+
// タイマーをクリア
|
|
134
|
+
if (timeoutId)
|
|
135
|
+
clearTimeout(timeoutId);
|
|
136
|
+
// バッファをフラッシュ
|
|
137
|
+
flushBuffer();
|
|
138
|
+
// NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
|
|
139
|
+
console.error('bash process exited with code', code);
|
|
140
|
+
resolve({
|
|
141
|
+
exitCode: code !== null ? code : 1,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
bash.on('error', (error) => {
|
|
145
|
+
// タイマーをクリア
|
|
146
|
+
if (timeoutId)
|
|
147
|
+
clearTimeout(timeoutId);
|
|
148
|
+
// バッファをフラッシュ
|
|
149
|
+
flushBuffer();
|
|
150
|
+
// NOTE これは MCP サーバの実装であるので、ログは標準エラー出力に出す
|
|
151
|
+
console.error('Failed to start bash process:', error);
|
|
152
|
+
reject(error);
|
|
153
|
+
});
|
|
154
|
+
// コマンドを標準入力に書き込み、EOF を送信
|
|
155
|
+
bash.stdin.write(command + '\n');
|
|
156
|
+
bash.stdin.end();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// MCPサーバーにコマンド実行ツールを追加する関数
|
|
160
|
+
export function setTool(mcpServer) {
|
|
161
|
+
// McpServerインスタンスから低レベルのServerインスタンスにアクセスする
|
|
162
|
+
const server = mcpServer.server;
|
|
163
|
+
// 単一のツールとして登録
|
|
164
|
+
mcpServer.tool(toolName, toolDescription, toolOptionsSchema,
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
166
|
+
async ({ command, options = {} }, extra) => {
|
|
167
|
+
try {
|
|
168
|
+
// outputModeを取得、デフォルト値は'complete'
|
|
169
|
+
const outputMode = options?.outputMode || 'complete';
|
|
170
|
+
// 進捗トークンを生成
|
|
171
|
+
const progressToken = `cmd-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`;
|
|
172
|
+
const onOutput = (data, isStderr) => {
|
|
173
|
+
const fsMark = isStderr ? 'stderr' : 'stdout';
|
|
174
|
+
server.notification({
|
|
175
|
+
method: 'notifications/tools/progress',
|
|
176
|
+
params: {
|
|
177
|
+
progressToken,
|
|
178
|
+
result: {
|
|
179
|
+
content: [
|
|
180
|
+
{
|
|
181
|
+
type: 'text',
|
|
182
|
+
text: `${fsMark}: ${data}`,
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
isComplete: false,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
};
|
|
190
|
+
// バックグラウンドでコマンドを実行
|
|
191
|
+
executeCommand(command, {
|
|
192
|
+
...options,
|
|
193
|
+
onOutput,
|
|
194
|
+
})
|
|
195
|
+
.then(({ exitCode }) => {
|
|
196
|
+
// 完了通知を送信
|
|
197
|
+
server.notification({
|
|
198
|
+
method: 'notifications/tools/progress',
|
|
199
|
+
params: {
|
|
200
|
+
progressToken,
|
|
201
|
+
result: {
|
|
202
|
+
content: [
|
|
203
|
+
{
|
|
204
|
+
type: 'text',
|
|
205
|
+
text: `exitCode: ${exitCode}`,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
isComplete: true,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
})
|
|
213
|
+
.catch((error) => {
|
|
214
|
+
// エラー通知を送信
|
|
215
|
+
server.notification({
|
|
216
|
+
method: 'notifications/tools/progress',
|
|
217
|
+
params: {
|
|
218
|
+
progressToken,
|
|
219
|
+
result: {
|
|
220
|
+
content: [
|
|
221
|
+
{
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
isComplete: true,
|
|
227
|
+
isError: true,
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
// 初期レスポンスを返す
|
|
233
|
+
return {
|
|
234
|
+
content: [
|
|
235
|
+
{
|
|
236
|
+
type: 'text',
|
|
237
|
+
text: `# Command execution started with output mode, ${outputMode}`,
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
progressToken,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
return {
|
|
245
|
+
content: [
|
|
246
|
+
{
|
|
247
|
+
type: 'text',
|
|
248
|
+
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
isError: true,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
export const shellProgram = '/usr/bin/bash';
|
|
4
|
+
export const toolName = 'execute-bash-script-sync';
|
|
5
|
+
export const toolDescription = `This tool executes shell scripts synchronously in bash.
|
|
6
|
+
Executing each command creates a new bash process.
|
|
7
|
+
Synchronous execution requires to wait the scripts completed.
|
|
8
|
+
Asynchronous execution makes it possible to execute multiple scripts in parallel.
|
|
9
|
+
You can reduce waiting time by planning in advance which shell scripts need to be executed and executing them in parallel.
|
|
10
|
+
Avoid using this execute-bash-script-sync tool unless you really need to, and use the execute-bash-script-async tool whenever possible.
|
|
11
|
+
`;
|
|
12
|
+
export const toolOptionsSchema = {
|
|
13
|
+
command: z.string().describe('The bash script to execute'),
|
|
14
|
+
options: z.object({
|
|
15
|
+
cwd: z.string().optional().describe(`The working directory to execute the script.
|
|
16
|
+
use this option argument to avoid cd command in the first line of the script.
|
|
17
|
+
\`~\` and environment variable is not supported. use absolute path instead.
|
|
18
|
+
`),
|
|
19
|
+
env: z.record(z.string(), z.string()).optional()
|
|
20
|
+
.describe(`The environment variables for the script.
|
|
21
|
+
Set environment variables using this option instead of using export command in the script.
|
|
22
|
+
`),
|
|
23
|
+
timeout: z.number().int().positive().optional().describe(`The timeout in milliseconds.
|
|
24
|
+
Set enough long timeout even if you don't need to set timeout to avoid unexpected blocking.
|
|
25
|
+
`),
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Execute a command using bash and return the result
|
|
30
|
+
*
|
|
31
|
+
* Each command execution spawn a new bash process.
|
|
32
|
+
* This implementation causes overhead but is simple and isolated.
|
|
33
|
+
*/
|
|
34
|
+
export async function executeCommand(command, options = {}) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
// 環境変数を設定
|
|
37
|
+
const env = {
|
|
38
|
+
...process.env,
|
|
39
|
+
...options.env,
|
|
40
|
+
};
|
|
41
|
+
// bashプロセスを起動
|
|
42
|
+
const bash = spawn(shellProgram, [], {
|
|
43
|
+
cwd: options.cwd,
|
|
44
|
+
env,
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
});
|
|
47
|
+
let stdout = '';
|
|
48
|
+
let stderr = '';
|
|
49
|
+
let timeoutId = null;
|
|
50
|
+
// 標準出力の収集
|
|
51
|
+
bash.stdout.on('data', (data) => {
|
|
52
|
+
stdout += data.toString();
|
|
53
|
+
});
|
|
54
|
+
// 標準エラー出力の収集
|
|
55
|
+
bash.stderr.on('data', (data) => {
|
|
56
|
+
stderr += data.toString();
|
|
57
|
+
});
|
|
58
|
+
// タイムアウト処理
|
|
59
|
+
if (options.timeout) {
|
|
60
|
+
timeoutId = setTimeout(() => {
|
|
61
|
+
bash.kill();
|
|
62
|
+
reject(new Error(`Command timed out after ${options.timeout}ms`));
|
|
63
|
+
}, options.timeout);
|
|
64
|
+
}
|
|
65
|
+
// プロセス終了時の処理
|
|
66
|
+
bash.on('close', (code) => {
|
|
67
|
+
if (timeoutId)
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
console.error('bash process exited with code', code);
|
|
70
|
+
resolve({
|
|
71
|
+
stdout,
|
|
72
|
+
stderr,
|
|
73
|
+
exitCode: code !== null ? code : 1,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
bash.on('error', (error) => {
|
|
77
|
+
if (timeoutId)
|
|
78
|
+
clearTimeout(timeoutId);
|
|
79
|
+
console.error('Failed to start bash process:', error);
|
|
80
|
+
reject(error);
|
|
81
|
+
});
|
|
82
|
+
// コマンドを標準入力に書き込み、EOF を送信
|
|
83
|
+
bash.stdin.write(command + '\n');
|
|
84
|
+
bash.stdin.end();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Execute a shell command
|
|
88
|
+
export function setTool(server) {
|
|
89
|
+
server.tool(toolName, toolDescription, toolOptionsSchema, async ({ command, options }) => {
|
|
90
|
+
const { stdout, stderr, exitCode } = await executeCommand(command, options);
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: 'text',
|
|
95
|
+
text: `stdout: ${stdout}`,
|
|
96
|
+
resource: undefined,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: `stderr: ${stderr}`,
|
|
101
|
+
resource: undefined,
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
type: 'text',
|
|
105
|
+
text: `exitCode: ${exitCode}`,
|
|
106
|
+
resource: undefined,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
4
|
+
import { setTool as setSyncTool } from './execute-bash-script-sync.js';
|
|
5
|
+
import { setTool as setAsyncTool } from './execute-bash-script-async.js';
|
|
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
|
+
setSyncTool(server);
|
|
13
|
+
setAsyncTool(server);
|
|
14
|
+
async function main() {
|
|
15
|
+
try {
|
|
16
|
+
// Start receiving messages on stdin and sending messages on stdout
|
|
17
|
+
const transport = new StdioServerTransport();
|
|
18
|
+
await server.connect(transport);
|
|
19
|
+
console.error('Shell Command MCP Server started');
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error('Fatal error starting server:', error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
main();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
|
|
2
|
+
{"method":"notifications/initialized","jsonrpc":"2.0"}
|
|
3
|
+
{"method":"resources/list","params":{},"jsonrpc":"2.0","id":1}
|
|
4
|
+
{"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}
|
|
5
|
+
{"method":"prompts/list","params":{},"jsonrpc":"2.0","id":3}
|
|
6
|
+
{"method":"tools/call","params":{"name":"execute-bash-script-sync","arguments":{"command":"echo \"現在の日時: $(date)\"","options":{"cwd":"/home/mcp","timeout":5000,"outputMode":"complete"}}},"jsonrpc":"2.0","id":4}
|
|
7
|
+
{"method":"tools/call","params":{"name":"execute-bash-script-async","arguments":{"command":"echo \"現在の日時: $(date)\"","options":{"cwd":"/home/mcp","timeout":5000,"outputMode":"complete"}}},"jsonrpc":"2.0","id":5}
|
|
8
|
+
{"method":"tools/call","params":{"name":"execute-bash-script-sync","arguments":{"command":"ls","options":{"cwd":"/home/mcp","timeout":5000,"outputMode":"complete"}}},"jsonrpc":"2.0","id":6}
|
|
9
|
+
{"method":"tools/call","params":{"name":"execute-bash-script-async","arguments":{"command":"ls","options":{"cwd":"/home/mcp","timeout":5000,"outputMode":"complete"}}},"jsonrpc":"2.0","id":7}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# version属性は廃止されたので削除
|
|
2
|
+
services:
|
|
3
|
+
mcp-server:
|
|
4
|
+
build:
|
|
5
|
+
context: .
|
|
6
|
+
dockerfile: Dockerfile
|
|
7
|
+
volumes:
|
|
8
|
+
- ./src:/home/mcp/app/src
|
|
9
|
+
- home-volume:/home/mcp
|
|
10
|
+
environment:
|
|
11
|
+
- NODE_ENV=production
|
|
12
|
+
restart: unless-stopped
|
|
13
|
+
stdin_open: true
|
|
14
|
+
tty: true
|
|
15
|
+
|
|
16
|
+
volumes:
|
|
17
|
+
home-volume:
|