mcp-server-commands 0.2.1 → 0.3.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 +15 -4
- package/build/exec-utils.js +53 -0
- package/build/index.js +140 -57
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
# mcp-server-commands
|
|
2
2
|
|
|
3
|
-
An MCP server to run commands
|
|
3
|
+
An MCP server to run commands.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Be careful what you ask this server to run!
|
|
7
|
+
> In Claude Desktop app, use `Approve Once` (not `Allow for This Chat`) so you can review each command, use `Deny` if you don't trust the command.
|
|
8
|
+
> Permissions are dictated by the user that runs the server.
|
|
9
|
+
> DO NOT run with `sudo`.
|
|
4
10
|
|
|
5
11
|
## Tools
|
|
6
12
|
|
|
13
|
+
Tools are for LLMs to request, i.e. Claude Desktop app
|
|
14
|
+
|
|
7
15
|
- `run_command` - run a command, i.e. `hostname` or `ls -al` or `echo "hello world"` etc
|
|
8
16
|
- Returns STDOUT and STDERR as text
|
|
9
|
-
-
|
|
17
|
+
- `run_script` - run a script! (i.e. `fish`, `bash`, `zsh`, `python`)
|
|
18
|
+
- Let your LLM run the code it writes!
|
|
19
|
+
- script is passed over STDIN
|
|
10
20
|
|
|
11
21
|
## Prompts
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
Prompts are for users to include in chat history, i.e. via `Zed`'s slash commands (in its AI Chat panel)
|
|
24
|
+
|
|
25
|
+
- `run_command` - generate a prompt message with the command output
|
|
15
26
|
|
|
16
27
|
## Development
|
|
17
28
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Executes a file with the given arguments, piping input to stdin.
|
|
4
|
+
* @param {string} interpreter - The file to execute.
|
|
5
|
+
* @param {string} stdin_text - The string to pipe to stdin.
|
|
6
|
+
* @returns {Promise<ExecResult>} A promise that resolves with the stdout and stderr of the command. `message` is provided on a failure to explain the error.
|
|
7
|
+
*/
|
|
8
|
+
function execFileWithInput(interpreter, stdin_text, options) {
|
|
9
|
+
// FYI for now, using `exec()` so the interpreter can have cmd+args AIO
|
|
10
|
+
// could switch to `execFile()` to pass args array separately
|
|
11
|
+
// TODO starts with fish too? "fish -..." PRN use a library to parse the command and determine this?
|
|
12
|
+
if (interpreter.split(" ")[0] === "fish") {
|
|
13
|
+
// PRN also check error from fish and add possible clarification to error message though there are legit ways to trigger that same error message! i.e. `fish .` which is not the same issue!
|
|
14
|
+
return fishWorkaround(interpreter, stdin_text, options);
|
|
15
|
+
}
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const child = exec(interpreter, options, (error, stdout, stderr) => {
|
|
18
|
+
if (error) {
|
|
19
|
+
reject({ message: error.message, stdout, stderr });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
resolve({ stdout, stderr });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
if (stdin_text) {
|
|
26
|
+
if (child.stdin === null) {
|
|
27
|
+
reject(new Error("Unexpected failure: child.stdin is null"));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
child.stdin.write(stdin_text);
|
|
31
|
+
child.stdin.end();
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
async function fishWorkaround(interpreter, script, options) {
|
|
36
|
+
// fish right now chokes on piped input (STDIN) + node's exec/spawn/etc, so lets use a workaround to echo the input
|
|
37
|
+
// base64 encode thee input, then decode in pipeline
|
|
38
|
+
const base64Script = Buffer.from(script).toString("base64");
|
|
39
|
+
const command = `${interpreter} -c "echo ${base64Script} | base64 -d | fish"`;
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
// const child = ... // careful with refactoring not to return that unused child
|
|
42
|
+
exec(command, options, (error, stdout, stderr) => {
|
|
43
|
+
// I like this style of error vs success handling! it's beautiful-est (prommises are underrated)
|
|
44
|
+
if (error) {
|
|
45
|
+
reject({ message: error.message, stdout, stderr });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
resolve({ stdout, stderr });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export { execFileWithInput };
|
package/build/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { exec } from "node:child_process";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { execFileWithInput } from "./exec-utils.js";
|
|
8
|
+
// TODO use .promises?
|
|
7
9
|
const execAsync = promisify(exec);
|
|
8
10
|
const server = new Server({
|
|
9
11
|
name: "mcp-server-commands",
|
|
10
|
-
version: "0.
|
|
12
|
+
version: "0.3.0",
|
|
11
13
|
}, {
|
|
12
14
|
capabilities: {
|
|
13
15
|
//resources: {},
|
|
@@ -25,74 +27,152 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
25
27
|
properties: {
|
|
26
28
|
command: {
|
|
27
29
|
type: "string",
|
|
28
|
-
description: "Command
|
|
30
|
+
description: "Command with args",
|
|
29
31
|
},
|
|
32
|
+
cwd: {
|
|
33
|
+
// previous run_command calls can probe the filesystem and find paths to change to
|
|
34
|
+
type: "string",
|
|
35
|
+
description: "Current working directory, leave empty in most cases",
|
|
36
|
+
},
|
|
37
|
+
// FYI using child_process.exec runs command in a shell, so you can pass a script here too but I still think separate tools would be helpful?
|
|
38
|
+
// FYI gonna use execFile for run_script
|
|
39
|
+
// - env - obscure cases where command takes a param only via an env var?
|
|
40
|
+
// args to consider:
|
|
41
|
+
// - timeout - lets just hard code this for now
|
|
42
|
+
// - shell - (cmd/args) - for now use run_script for this case, also can just pass "fish -c 'command'" or "sh ..."
|
|
43
|
+
// - stdin? though this borders on the run_script below
|
|
44
|
+
// - capture_output (default true) - for now can just redirect to /dev/null - perhaps capture_stdout/capture_stderr
|
|
30
45
|
},
|
|
31
46
|
required: ["command"],
|
|
32
47
|
},
|
|
33
48
|
},
|
|
49
|
+
// PRN tool to introspect the environment (i.e. windows vs linux vs mac, maybe default shell, etc?) - for now LLM can run commands and when they fail it can make adjustments accordingly - some cases where knowing this would help avoid dispatching erroneous commands (i.e. using free on linux, vm_stat on mac)
|
|
50
|
+
{
|
|
51
|
+
// TODO is run_script even needed if I were to add STDIN support to run_command above?
|
|
52
|
+
name: "run_script",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
interpreter: {
|
|
57
|
+
// TODO use shebang on *nix?
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Command with arguments. Script will be piped to stdin. Examples: bash, fish, zsh, python, or: bash --norc",
|
|
60
|
+
},
|
|
61
|
+
script: {
|
|
62
|
+
type: "string",
|
|
63
|
+
description: "Script to run",
|
|
64
|
+
},
|
|
65
|
+
cwd: {
|
|
66
|
+
type: "string",
|
|
67
|
+
description: "Current working directory",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
required: ["script"],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
34
73
|
],
|
|
35
74
|
};
|
|
36
75
|
});
|
|
37
76
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
38
77
|
switch (request.params.name) {
|
|
39
78
|
case "run_command": {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
isError: false,
|
|
49
|
-
content: [
|
|
50
|
-
{
|
|
51
|
-
type: "text",
|
|
52
|
-
text: stdout,
|
|
53
|
-
name: "STDOUT",
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
type: "text",
|
|
57
|
-
text: stderr,
|
|
58
|
-
name: "STDERR",
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
const { message, stdout, stderr } = error;
|
|
66
|
-
return {
|
|
67
|
-
toolResult: {
|
|
68
|
-
isError: true,
|
|
69
|
-
content: [
|
|
70
|
-
{
|
|
71
|
-
// most of the time this is gonna match stderr, TODO do I want/need both error and stderr?
|
|
72
|
-
type: "text",
|
|
73
|
-
text: message,
|
|
74
|
-
name: "ERROR",
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
type: "text",
|
|
78
|
-
text: stderr || "",
|
|
79
|
-
name: "STDERR",
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
// keep STDOUT b/c there might be some useful output before the failure
|
|
83
|
-
type: "text",
|
|
84
|
-
text: stdout || "",
|
|
85
|
-
name: "STDOUT",
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
}
|
|
79
|
+
return {
|
|
80
|
+
toolResult: await runCommand(request.params.arguments),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
case "run_script": {
|
|
84
|
+
return {
|
|
85
|
+
toolResult: await runScript(request.params.arguments),
|
|
86
|
+
};
|
|
91
87
|
}
|
|
92
88
|
default:
|
|
93
89
|
throw new Error("Unknown tool");
|
|
94
90
|
}
|
|
95
91
|
});
|
|
92
|
+
async function runCommand(args) {
|
|
93
|
+
const command = String(args?.command);
|
|
94
|
+
if (!command) {
|
|
95
|
+
throw new Error("Command is required");
|
|
96
|
+
}
|
|
97
|
+
const options = {};
|
|
98
|
+
if (args?.cwd) {
|
|
99
|
+
options.cwd = String(args.cwd);
|
|
100
|
+
// ENOENT is thrown if the cwd doesn't exist, and I think LLMs can understand that?
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const result = await execAsync(command, options);
|
|
104
|
+
return {
|
|
105
|
+
isError: false,
|
|
106
|
+
content: messagesFor(result),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
// TODO catch for other errors, not just ExecException
|
|
111
|
+
return {
|
|
112
|
+
isError: true,
|
|
113
|
+
content: messagesFor(error),
|
|
114
|
+
//content: [{ type: "text", text: JSON.stringify(error) }],
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function runScript(args) {
|
|
119
|
+
const interpreter = String(args?.interpreter);
|
|
120
|
+
if (!interpreter) {
|
|
121
|
+
throw new Error("Interpreter is required");
|
|
122
|
+
}
|
|
123
|
+
const options = {
|
|
124
|
+
//const options = {
|
|
125
|
+
// constrains typescript too, to string based overload
|
|
126
|
+
encoding: "utf8",
|
|
127
|
+
};
|
|
128
|
+
if (args?.cwd) {
|
|
129
|
+
options.cwd = String(args.cwd);
|
|
130
|
+
// ENOENT is thrown if the cwd doesn't exist, and I think LLMs can understand that?
|
|
131
|
+
}
|
|
132
|
+
const script = String(args?.script);
|
|
133
|
+
if (!script) {
|
|
134
|
+
throw new Error("Script is required");
|
|
135
|
+
}
|
|
136
|
+
try {
|
|
137
|
+
const result = await execFileWithInput(interpreter, script, options);
|
|
138
|
+
return {
|
|
139
|
+
isError: false,
|
|
140
|
+
content: messagesFor(result),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
return {
|
|
145
|
+
isError: true,
|
|
146
|
+
content: messagesFor(error),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function messagesFor(result) {
|
|
151
|
+
const messages = [];
|
|
152
|
+
if (result.message) {
|
|
153
|
+
messages.push({
|
|
154
|
+
// most of the time this is gonna match stderr, TODO do I want/need both error and stderr?
|
|
155
|
+
type: "text",
|
|
156
|
+
text: result.message,
|
|
157
|
+
name: "ERROR",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
if (result.stdout) {
|
|
161
|
+
messages.push({
|
|
162
|
+
type: "text",
|
|
163
|
+
text: result.stdout,
|
|
164
|
+
name: "STDOUT",
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
if (result.stderr) {
|
|
168
|
+
messages.push({
|
|
169
|
+
type: "text",
|
|
170
|
+
text: result.stderr,
|
|
171
|
+
name: "STDERR",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return messages;
|
|
175
|
+
}
|
|
96
176
|
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
97
177
|
return {
|
|
98
178
|
prompts: [
|
|
@@ -117,8 +197,12 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
117
197
|
if (!command) {
|
|
118
198
|
throw new Error("Command is required");
|
|
119
199
|
}
|
|
200
|
+
// Is it possible/feasible to pass a path for the CWD when running the command?
|
|
201
|
+
// - currently it uses / (yikez)
|
|
202
|
+
// - IMO makes more sense to have it be based on the Zed CWD of each project
|
|
203
|
+
// - Fallback could be to configure on server level (i.e. home dir of current user) - perhaps CLI arg? (thinking of zed's context_servers config section)
|
|
120
204
|
const { stdout, stderr } = await execAsync(command);
|
|
121
|
-
//
|
|
205
|
+
// TODO gracefully handle errors and turn them into a prompt message that can be used by LLM to troubleshoot the issue, currently errors result in nothing inserted into the prompt and instead it shows the Zed's chat panel as a failure
|
|
122
206
|
const messages = [
|
|
123
207
|
{
|
|
124
208
|
role: "user",
|
|
@@ -129,7 +213,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
129
213
|
},
|
|
130
214
|
},
|
|
131
215
|
];
|
|
132
|
-
if (stdout
|
|
216
|
+
if (stdout) {
|
|
133
217
|
messages.push({
|
|
134
218
|
role: "user",
|
|
135
219
|
content: {
|
|
@@ -138,7 +222,7 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
138
222
|
},
|
|
139
223
|
});
|
|
140
224
|
}
|
|
141
|
-
if (stderr
|
|
225
|
+
if (stderr) {
|
|
142
226
|
messages.push({
|
|
143
227
|
role: "user",
|
|
144
228
|
content: {
|
|
@@ -150,7 +234,6 @@ server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
|
150
234
|
return { messages };
|
|
151
235
|
});
|
|
152
236
|
async function main() {
|
|
153
|
-
console.log("Starting server...");
|
|
154
237
|
const transport = new StdioServerTransport();
|
|
155
238
|
await server.connect(transport);
|
|
156
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-commands",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "An MCP server to run arbitrary commands",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"clean": "rm -rf build",
|
|
15
15
|
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
16
16
|
"prepare": "npm run build",
|
|
17
|
-
"watch": "tsc --watch",
|
|
17
|
+
"watch": "npm run build && tsc --watch",
|
|
18
18
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|