mcp-server-commands 0.4.2 → 0.6.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/LICENSE +5 -0
- package/README.md +54 -12
- package/build/always_log.js +48 -0
- package/build/exec-utils.js +20 -11
- package/build/index.js +7 -287
- package/build/messages.js +50 -0
- package/build/prompts.js +91 -0
- package/build/run-command.js +61 -0
- package/build/tools.js +49 -0
- package/package.json +31 -31
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
2
|
+
|
|
3
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
4
|
+
|
|
5
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
## Tools
|
|
2
|
+
|
|
3
|
+
Tools are for LLMs to request. Claude Sonnet 3.5 intelligently uses `run_command`. And, initial testing shows promising results with [Groq Desktop with MCP](https://github.com/groq/groq-desktop-beta) and `llama4` models.
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
Currently, just one command to rule them all!
|
|
6
|
+
|
|
7
|
+
- `run_command` - run a command, i.e. `hostname` or `ls -al` or `echo "hello world"` etc
|
|
8
|
+
- Returns `STDOUT` and `STDERR` as text
|
|
9
|
+
- Optional `stdin` parameter means your LLM can
|
|
10
|
+
- pass code in `stdin` to commands like `fish`, `bash`, `zsh`, `python`
|
|
11
|
+
- create files with `cat >> foo/bar.txt` from the text in `stdin`
|
|
4
12
|
|
|
5
13
|
> [!WARNING]
|
|
6
14
|
> Be careful what you ask this server to run!
|
|
@@ -8,17 +16,9 @@ An MCP server to run commands.
|
|
|
8
16
|
> Permissions are dictated by the user that runs the server.
|
|
9
17
|
> DO NOT run with `sudo`.
|
|
10
18
|
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
Tools are for LLMs to request, i.e. Claude Desktop app. Claude Sonnet 3.5 intelligently uses both tools, I was pleasantly surprised.
|
|
19
|
+
## Video walkthrough
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
- Returns STDOUT and STDERR as text
|
|
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
|
|
20
|
-
- `run_script` == `run_command` + script over STDIN
|
|
21
|
-
- Claude has been pretty creative with this, i.e. using `cat` as the interpreter to create new files!
|
|
21
|
+
<a href="https://youtu.be/0-VPu1Pc18w"><img src="https://img.youtube.com/vi/0-VPu1Pc18w/maxresdefault.jpg" width="480" alt="YouTube Thumbnail"></a>
|
|
22
22
|
|
|
23
23
|
## Prompts
|
|
24
24
|
|
|
@@ -50,8 +50,12 @@ To use with Claude Desktop, add the server config:
|
|
|
50
50
|
On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
51
51
|
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
52
52
|
|
|
53
|
+
Groq Desktop (beta, macOS) uses `~/Library/Application Support/groq-desktop-app/settings.json`
|
|
54
|
+
|
|
53
55
|
### Use the published npm package
|
|
54
56
|
|
|
57
|
+
Published to npm as [mcp-server-commands](https://www.npmjs.com/package/mcp-server-commands) using this [workflow](https://github.com/g0t4/mcp-server-commands/actions)
|
|
58
|
+
|
|
55
59
|
```json
|
|
56
60
|
{
|
|
57
61
|
"mcpServers": {
|
|
@@ -65,6 +69,8 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
|
65
69
|
|
|
66
70
|
### Use a local build (repo checkout)
|
|
67
71
|
|
|
72
|
+
Make sure to run `npm run build`
|
|
73
|
+
|
|
68
74
|
```json
|
|
69
75
|
{
|
|
70
76
|
"mcpServers": {
|
|
@@ -76,6 +82,42 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
|
|
|
76
82
|
}
|
|
77
83
|
```
|
|
78
84
|
|
|
85
|
+
## Local Models
|
|
86
|
+
|
|
87
|
+
- Most models are trained such that they don't think they can run commands for you.
|
|
88
|
+
- Sometimes, they use tools w/o hesitation... other times, I have to coax them.
|
|
89
|
+
- Use a system prompt or prompt template to instruct that they should follow user requests. Including to use `run_commands` without double checking.
|
|
90
|
+
- Ollama is a great way to run a model locally (w/ Open-WebUI)
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
# NOTE: make sure to review variants and sizes, so the model fits in your VRAM to perform well!
|
|
94
|
+
|
|
95
|
+
# Probably the best so far is [OpenHands LM](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model)
|
|
96
|
+
ollama pull https://huggingface.co/lmstudio-community/openhands-lm-32b-v0.1-GGUF
|
|
97
|
+
|
|
98
|
+
# https://ollama.com/library/devstral
|
|
99
|
+
ollama pull devstral
|
|
100
|
+
|
|
101
|
+
# Qwen2.5-Coder has tool use but you have to coax it
|
|
102
|
+
ollama pull qwen2.5-coder
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### HTTP / OpenAPI
|
|
106
|
+
|
|
107
|
+
The server is implemented with the `STDIO` transport.
|
|
108
|
+
For `HTTP`, use [`mcpo`](https://github.com/open-webui/mcpo) for an `OpenAPI` compatible web server interface.
|
|
109
|
+
This works with [`Open-WebUI`](https://github.com/open-webui/open-webui)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uvx mcpo --port 3010 --api-key "supersecret" -- npx mcp-server-commands
|
|
113
|
+
|
|
114
|
+
# uvx runs mcpo => mcpo run npx => npx runs mcp-server-commands
|
|
115
|
+
# then, mcpo bridges STDIO <=> HTTP
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
> [!WARNING]
|
|
119
|
+
> I briefly used `mcpo` with `open-webui`, make sure to vet it for security concerns.
|
|
120
|
+
|
|
79
121
|
### Logging
|
|
80
122
|
|
|
81
123
|
Claude Desktop app writes logs to `~/Library/Logs/Claude/mcp-server-mcp-server-commands.log`
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
let verbose = false;
|
|
2
|
+
// check CLI args:
|
|
3
|
+
if (process.argv.includes("--verbose")) {
|
|
4
|
+
verbose = true;
|
|
5
|
+
}
|
|
6
|
+
if (verbose) {
|
|
7
|
+
always_log("INFO: verbose logging enabled");
|
|
8
|
+
}
|
|
9
|
+
export function verbose_log(message, data) {
|
|
10
|
+
// https://modelcontextprotocol.io/docs/tools/debugging - mentions various ways to debug/troubleshoot (including dev tools)
|
|
11
|
+
//
|
|
12
|
+
// remember STDIO transport means can't log over STDOUT (client expects JSON messages per the spec)
|
|
13
|
+
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
14
|
+
// mentions STDERR is captured by the host app (i.e. Claude Desktop app)
|
|
15
|
+
// server.sendLoggingMessage is captured by MCP client (not Claude Desktop app)
|
|
16
|
+
// SO, IIUC use STDERR for logging into Claude Desktop app logs in:
|
|
17
|
+
// '~/Library/Logs/Claude/mcp.log'
|
|
18
|
+
if (verbose) {
|
|
19
|
+
always_log(message, data);
|
|
20
|
+
}
|
|
21
|
+
// inspector, catches these logs and shows them on left hand side of screen (sidebar)
|
|
22
|
+
// TODO add verbose parameter (CLI arg?)
|
|
23
|
+
// IF I wanted to log via MCP client logs (not sure what those are/do):
|
|
24
|
+
// I do not see inspector catching these logs :(, there is a server notifications section and it remains empty
|
|
25
|
+
//server.sendLoggingMessage({
|
|
26
|
+
// level: "info",
|
|
27
|
+
// data: message,
|
|
28
|
+
//});
|
|
29
|
+
// which results in something like:
|
|
30
|
+
//server.notification({
|
|
31
|
+
// method: "notifications/message",
|
|
32
|
+
// params: {
|
|
33
|
+
// level: "warning",
|
|
34
|
+
// logger: "mcp-server-commands",
|
|
35
|
+
// data: "ListToolsRequest2",
|
|
36
|
+
// },
|
|
37
|
+
//});
|
|
38
|
+
//
|
|
39
|
+
// FYI client should also requets a log level from the server, so that needs to be here at some point too
|
|
40
|
+
}
|
|
41
|
+
export function always_log(message, data) {
|
|
42
|
+
if (data) {
|
|
43
|
+
console.error(message + ": " + JSON.stringify(data));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.error(message);
|
|
47
|
+
}
|
|
48
|
+
}
|
package/build/exec-utils.js
CHANGED
|
@@ -2,47 +2,56 @@ import { exec } from "child_process";
|
|
|
2
2
|
/**
|
|
3
3
|
* Executes a file with the given arguments, piping input to stdin.
|
|
4
4
|
* @param {string} interpreter - The file to execute.
|
|
5
|
-
* @param {string}
|
|
6
|
-
* @returns {Promise<ExecResult>}
|
|
5
|
+
* @param {string} stdin - The string to pipe to stdin.
|
|
6
|
+
* @returns {Promise<ExecResult>}
|
|
7
7
|
*/
|
|
8
|
-
function execFileWithInput(interpreter,
|
|
8
|
+
function execFileWithInput(interpreter, stdin, options) {
|
|
9
9
|
// FYI for now, using `exec()` so the interpreter can have cmd+args AIO
|
|
10
10
|
// could switch to `execFile()` to pass args array separately
|
|
11
11
|
// TODO starts with fish too? "fish -..." PRN use a library to parse the command and determine this?
|
|
12
12
|
if (interpreter.split(" ")[0] === "fish") {
|
|
13
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,
|
|
14
|
+
return fishWorkaround(interpreter, stdin, options);
|
|
15
15
|
}
|
|
16
16
|
return new Promise((resolve, reject) => {
|
|
17
17
|
const child = exec(interpreter, options, (error, stdout, stderr) => {
|
|
18
18
|
if (error) {
|
|
19
|
-
|
|
19
|
+
// console.log("execFileWithInput ERROR:", error);
|
|
20
|
+
// mirror ExecException used by throws
|
|
21
|
+
error.stdout = stdout;
|
|
22
|
+
error.stderr = stderr;
|
|
23
|
+
reject(error);
|
|
20
24
|
}
|
|
21
25
|
else {
|
|
26
|
+
// I assume RC==0 else would trigger error?
|
|
22
27
|
resolve({ stdout, stderr });
|
|
23
28
|
}
|
|
24
29
|
});
|
|
25
|
-
if (
|
|
30
|
+
if (stdin) {
|
|
26
31
|
if (child.stdin === null) {
|
|
27
32
|
reject(new Error("Unexpected failure: child.stdin is null"));
|
|
28
33
|
return;
|
|
29
34
|
}
|
|
30
|
-
child.stdin.write(
|
|
35
|
+
child.stdin.write(stdin);
|
|
31
36
|
child.stdin.end();
|
|
32
37
|
}
|
|
33
38
|
});
|
|
34
39
|
}
|
|
35
|
-
async function fishWorkaround(interpreter,
|
|
40
|
+
async function fishWorkaround(interpreter, stdin, options) {
|
|
36
41
|
// fish right now chokes on piped input (STDIN) + node's exec/spawn/etc, so lets use a workaround to echo the input
|
|
37
42
|
// base64 encode thee input, then decode in pipeline
|
|
38
|
-
const
|
|
39
|
-
const command = `${interpreter} -c "echo ${
|
|
43
|
+
const base64stdin = Buffer.from(stdin).toString("base64");
|
|
44
|
+
const command = `${interpreter} -c "echo ${base64stdin} | base64 -d | fish"`;
|
|
40
45
|
return new Promise((resolve, reject) => {
|
|
41
46
|
// const child = ... // careful with refactoring not to return that unused child
|
|
42
47
|
exec(command, options, (error, stdout, stderr) => {
|
|
43
48
|
// I like this style of error vs success handling! it's beautiful-est (prommises are underrated)
|
|
44
49
|
if (error) {
|
|
45
|
-
|
|
50
|
+
// console.log("fishWorkaround ERROR:", error);
|
|
51
|
+
// mirror ExecException used by throws
|
|
52
|
+
error.stdout = stdout;
|
|
53
|
+
error.stderr = stderr;
|
|
54
|
+
reject(error);
|
|
46
55
|
}
|
|
47
56
|
else {
|
|
48
57
|
resolve({ stdout, stderr });
|
package/build/index.js
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import os from "os";
|
|
2
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import { exec } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
7
|
-
import { execFileWithInput } from "./exec-utils.js";
|
|
8
5
|
import { createRequire } from "module";
|
|
6
|
+
import { registerPrompts } from "./prompts.js";
|
|
7
|
+
import { reisterTools } from "./tools.js";
|
|
9
8
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const { name: package_name, version: package_version } = require("../package.json");
|
|
11
|
-
// TODO use .promises? in node api
|
|
12
|
-
const execAsync = promisify(exec);
|
|
13
|
-
let verbose = false;
|
|
14
|
-
// check CLI args:
|
|
15
|
-
if (process.argv.includes("--verbose")) {
|
|
16
|
-
verbose = true;
|
|
17
|
-
}
|
|
9
|
+
const { name: package_name, version: package_version, } = require("../package.json");
|
|
18
10
|
const server = new Server({
|
|
19
11
|
name: package_name,
|
|
20
12
|
version: package_version,
|
|
21
|
-
|
|
13
|
+
description: "Run commands on this " + os.platform() + " machine",
|
|
22
14
|
}, {
|
|
23
15
|
capabilities: {
|
|
24
16
|
//resources: {},
|
|
@@ -27,280 +19,8 @@ const server = new Server({
|
|
|
27
19
|
//logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
|
|
28
20
|
},
|
|
29
21
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
console.error(message + ": " + JSON.stringify(data));
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
console.error(message);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
if (verbose) {
|
|
39
|
-
always_log("INFO: verbose logging enabled");
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
always_log("INFO: verbose logging disabled, enable it with --verbose");
|
|
43
|
-
}
|
|
44
|
-
function verbose_log(message, data) {
|
|
45
|
-
// https://modelcontextprotocol.io/docs/tools/debugging - mentions various ways to debug/troubleshoot (including dev tools)
|
|
46
|
-
//
|
|
47
|
-
// remember STDIO transport means can't log over STDOUT (client expects JSON messages per the spec)
|
|
48
|
-
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
49
|
-
// mentions STDERR is captured by the host app (i.e. Claude Desktop app)
|
|
50
|
-
// server.sendLoggingMessage is captured by MCP client (not Claude Desktop app)
|
|
51
|
-
// SO, IIUC use STDERR for logging into Claude Desktop app logs in:
|
|
52
|
-
// '~/Library/Logs/Claude/mcp.log'
|
|
53
|
-
if (verbose) {
|
|
54
|
-
always_log(message, data);
|
|
55
|
-
}
|
|
56
|
-
// inspector, catches these logs and shows them on left hand side of screen (sidebar)
|
|
57
|
-
// TODO add verbose parameter (CLI arg?)
|
|
58
|
-
// IF I wanted to log via MCP client logs (not sure what those are/do):
|
|
59
|
-
// I do not see inspector catching these logs :(, there is a server notifications section and it remains empty
|
|
60
|
-
//server.sendLoggingMessage({
|
|
61
|
-
// level: "info",
|
|
62
|
-
// data: message,
|
|
63
|
-
//});
|
|
64
|
-
// which results in something like:
|
|
65
|
-
//server.notification({
|
|
66
|
-
// method: "notifications/message",
|
|
67
|
-
// params: {
|
|
68
|
-
// level: "warning",
|
|
69
|
-
// logger: "mcp-server-commands",
|
|
70
|
-
// data: "ListToolsRequest2",
|
|
71
|
-
// },
|
|
72
|
-
//});
|
|
73
|
-
//
|
|
74
|
-
// FYI client should also requets a log level from the server, so that needs to be here at some point too
|
|
75
|
-
}
|
|
76
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
77
|
-
verbose_log("INFO: ListTools");
|
|
78
|
-
return {
|
|
79
|
-
tools: [
|
|
80
|
-
{
|
|
81
|
-
name: "run_command",
|
|
82
|
-
//description: "Run a command on this " + os.platform() + " machine",
|
|
83
|
-
inputSchema: {
|
|
84
|
-
type: "object",
|
|
85
|
-
properties: {
|
|
86
|
-
command: {
|
|
87
|
-
type: "string",
|
|
88
|
-
description: "Command with args",
|
|
89
|
-
},
|
|
90
|
-
cwd: {
|
|
91
|
-
// previous run_command calls can probe the filesystem and find paths to change to
|
|
92
|
-
type: "string",
|
|
93
|
-
description: "Current working directory, leave empty in most cases",
|
|
94
|
-
},
|
|
95
|
-
// 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?
|
|
96
|
-
// FYI gonna use execFile for run_script
|
|
97
|
-
// - env - obscure cases where command takes a param only via an env var?
|
|
98
|
-
// args to consider:
|
|
99
|
-
// - timeout - lets just hard code this for now
|
|
100
|
-
// - shell - (cmd/args) - for now use run_script for this case, also can just pass "fish -c 'command'" or "sh ..."
|
|
101
|
-
// - stdin? though this borders on the run_script below
|
|
102
|
-
// - capture_output (default true) - for now can just redirect to /dev/null - perhaps capture_stdout/capture_stderr
|
|
103
|
-
},
|
|
104
|
-
required: ["command"],
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
// 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)
|
|
108
|
-
{
|
|
109
|
-
// TODO is run_script even needed if I were to add STDIN support to run_command above?
|
|
110
|
-
name: "run_script",
|
|
111
|
-
// TODO is it useful to include OS type? I need to test this on a windows machine and see how Claude does w/ and w/o this os hint:
|
|
112
|
-
//description: "Run a script on this " + os.platform() + " machine",
|
|
113
|
-
inputSchema: {
|
|
114
|
-
type: "object",
|
|
115
|
-
properties: {
|
|
116
|
-
interpreter: {
|
|
117
|
-
// TODO use shebang on *nix?
|
|
118
|
-
type: "string",
|
|
119
|
-
description: "Command with arguments. Script will be piped to stdin. Examples: bash, fish, zsh, python, or: bash --norc",
|
|
120
|
-
},
|
|
121
|
-
script: {
|
|
122
|
-
type: "string",
|
|
123
|
-
description: "Script to run",
|
|
124
|
-
},
|
|
125
|
-
cwd: {
|
|
126
|
-
type: "string",
|
|
127
|
-
description: "Current working directory",
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
required: ["script"],
|
|
131
|
-
},
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
};
|
|
135
|
-
});
|
|
136
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
137
|
-
verbose_log("INFO: ToolRequest", request);
|
|
138
|
-
switch (request.params.name) {
|
|
139
|
-
case "run_command": {
|
|
140
|
-
return {
|
|
141
|
-
toolResult: await runCommand(request.params.arguments),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
case "run_script": {
|
|
145
|
-
return {
|
|
146
|
-
toolResult: await runScript(request.params.arguments),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
default:
|
|
150
|
-
throw new Error("Unknown tool");
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
async function runCommand(args) {
|
|
154
|
-
const command = String(args?.command);
|
|
155
|
-
if (!command) {
|
|
156
|
-
throw new Error("Command is required");
|
|
157
|
-
}
|
|
158
|
-
const options = {};
|
|
159
|
-
if (args?.cwd) {
|
|
160
|
-
options.cwd = String(args.cwd);
|
|
161
|
-
// ENOENT is thrown if the cwd doesn't exist, and I think LLMs can understand that?
|
|
162
|
-
}
|
|
163
|
-
try {
|
|
164
|
-
const result = await execAsync(command, options);
|
|
165
|
-
return {
|
|
166
|
-
isError: false,
|
|
167
|
-
content: messagesFor(result),
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
// TODO catch for other errors, not just ExecException
|
|
172
|
-
// FYI failure may not always be a bad thing if for example checking for a file to exist so just keep that in mind in terms of logging?
|
|
173
|
-
const response = {
|
|
174
|
-
isError: true,
|
|
175
|
-
content: messagesFor(error),
|
|
176
|
-
};
|
|
177
|
-
always_log("WARN: run_command failed", response);
|
|
178
|
-
return response;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
async function runScript(args) {
|
|
182
|
-
const interpreter = String(args?.interpreter);
|
|
183
|
-
if (!interpreter) {
|
|
184
|
-
throw new Error("Interpreter is required");
|
|
185
|
-
}
|
|
186
|
-
const options = {
|
|
187
|
-
//const options = {
|
|
188
|
-
// constrains typescript too, to string based overload
|
|
189
|
-
encoding: "utf8",
|
|
190
|
-
};
|
|
191
|
-
if (args?.cwd) {
|
|
192
|
-
options.cwd = String(args.cwd);
|
|
193
|
-
// ENOENT is thrown if the cwd doesn't exist, and I think LLMs can understand that?
|
|
194
|
-
}
|
|
195
|
-
const script = String(args?.script);
|
|
196
|
-
if (!script) {
|
|
197
|
-
throw new Error("Script is required");
|
|
198
|
-
}
|
|
199
|
-
try {
|
|
200
|
-
const result = await execFileWithInput(interpreter, script, options);
|
|
201
|
-
return {
|
|
202
|
-
isError: false,
|
|
203
|
-
content: messagesFor(result),
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
const response = {
|
|
208
|
-
isError: true,
|
|
209
|
-
content: messagesFor(error),
|
|
210
|
-
};
|
|
211
|
-
always_log("WARN: run_script failed", response);
|
|
212
|
-
return response;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
function messagesFor(result) {
|
|
216
|
-
const messages = [];
|
|
217
|
-
if (result.message) {
|
|
218
|
-
messages.push({
|
|
219
|
-
// most of the time this is gonna match stderr, TODO do I want/need both error and stderr?
|
|
220
|
-
type: "text",
|
|
221
|
-
text: result.message,
|
|
222
|
-
name: "ERROR",
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
if (result.stdout) {
|
|
226
|
-
messages.push({
|
|
227
|
-
type: "text",
|
|
228
|
-
text: result.stdout,
|
|
229
|
-
name: "STDOUT",
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
if (result.stderr) {
|
|
233
|
-
messages.push({
|
|
234
|
-
type: "text",
|
|
235
|
-
text: result.stderr,
|
|
236
|
-
name: "STDERR",
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
return messages;
|
|
240
|
-
}
|
|
241
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
242
|
-
verbose_log("INFO: ListPrompts");
|
|
243
|
-
return {
|
|
244
|
-
prompts: [
|
|
245
|
-
{
|
|
246
|
-
name: "run_command",
|
|
247
|
-
description: "Include command output in the prompt. Instead of a tool call, the user decides what commands are relevant.",
|
|
248
|
-
arguments: [
|
|
249
|
-
{
|
|
250
|
-
name: "command",
|
|
251
|
-
required: true,
|
|
252
|
-
},
|
|
253
|
-
],
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
};
|
|
257
|
-
});
|
|
258
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
259
|
-
if (request.params.name !== "run_command") {
|
|
260
|
-
throw new Error("Unknown prompt");
|
|
261
|
-
}
|
|
262
|
-
verbose_log("INFO: PromptRequest", request);
|
|
263
|
-
const command = String(request.params.arguments?.command);
|
|
264
|
-
if (!command) {
|
|
265
|
-
throw new Error("Command is required");
|
|
266
|
-
}
|
|
267
|
-
// Is it possible/feasible to pass a path for the CWD when running the command?
|
|
268
|
-
// - currently it uses / (yikez)
|
|
269
|
-
// - IMO makes more sense to have it be based on the Zed CWD of each project
|
|
270
|
-
// - 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)
|
|
271
|
-
const { stdout, stderr } = await execAsync(command);
|
|
272
|
-
// 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
|
|
273
|
-
const messages = [
|
|
274
|
-
{
|
|
275
|
-
role: "user",
|
|
276
|
-
content: {
|
|
277
|
-
type: "text",
|
|
278
|
-
text: "I ran the following command, if there is any output it will be shown below:\n" +
|
|
279
|
-
command,
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
];
|
|
283
|
-
if (stdout) {
|
|
284
|
-
messages.push({
|
|
285
|
-
role: "user",
|
|
286
|
-
content: {
|
|
287
|
-
type: "text",
|
|
288
|
-
text: "STDOUT:\n" + stdout,
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
if (stderr) {
|
|
293
|
-
messages.push({
|
|
294
|
-
role: "user",
|
|
295
|
-
content: {
|
|
296
|
-
type: "text",
|
|
297
|
-
text: "STDERR:\n" + stderr,
|
|
298
|
-
},
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
verbose_log("INFO: PromptResponse", messages);
|
|
302
|
-
return { messages };
|
|
303
|
-
});
|
|
22
|
+
reisterTools(server);
|
|
23
|
+
registerPrompts(server);
|
|
304
24
|
async function main() {
|
|
305
25
|
const transport = new StdioServerTransport();
|
|
306
26
|
await server.connect(transport);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts an ExecResult into an array of TextContent messages.
|
|
3
|
+
*/
|
|
4
|
+
export function messagesFor(result) {
|
|
5
|
+
const messages = [];
|
|
6
|
+
if (result.code !== undefined) {
|
|
7
|
+
messages.push({
|
|
8
|
+
type: "text",
|
|
9
|
+
text: `${result.code}`,
|
|
10
|
+
name: "EXIT_CODE",
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
// PRN any situation where I want to pass .message and/or .cmd?
|
|
14
|
+
// maybe on errors I should? that way there's a chance to make sure the command was as intended
|
|
15
|
+
// and maybe include message when it doesn't contain stderr?
|
|
16
|
+
// FYI if I put these back, start with tests first
|
|
17
|
+
// PRN use a test to add these, sleep 10s maybe and then kill that process?
|
|
18
|
+
// definitely could be useful to know if a command was killed
|
|
19
|
+
// make sure signal is not null, which is what's used when no signal killed the process
|
|
20
|
+
// if (result.signal) {
|
|
21
|
+
// messages.push({
|
|
22
|
+
// type: "text",
|
|
23
|
+
// text: `Signal: ${result.signal}`,
|
|
24
|
+
// name: "SIGNAL",
|
|
25
|
+
// });
|
|
26
|
+
// }
|
|
27
|
+
// if (!!result.killed) {
|
|
28
|
+
// // killed == true is the only time to include this
|
|
29
|
+
// messages.push({
|
|
30
|
+
// type: "text",
|
|
31
|
+
// text: "Process was killed",
|
|
32
|
+
// name: "KILLED",
|
|
33
|
+
// });
|
|
34
|
+
// }
|
|
35
|
+
if (result.stdout) {
|
|
36
|
+
messages.push({
|
|
37
|
+
type: "text",
|
|
38
|
+
text: result.stdout,
|
|
39
|
+
name: "STDOUT",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (result.stderr) {
|
|
43
|
+
messages.push({
|
|
44
|
+
type: "text",
|
|
45
|
+
text: result.stderr,
|
|
46
|
+
name: "STDERR",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return messages;
|
|
50
|
+
}
|
package/build/prompts.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { GetPromptRequestSchema, ListPromptsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { verbose_log } from "./always_log.js";
|
|
3
|
+
import { exec } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
// TODO use .promises? in node api
|
|
7
|
+
export function registerPrompts(server) {
|
|
8
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
9
|
+
verbose_log("INFO: ListPrompts");
|
|
10
|
+
return {
|
|
11
|
+
prompts: [
|
|
12
|
+
// TODO! add prompts for various LLMs that tailor instructions to make them optimize use of run_command tool
|
|
13
|
+
// idea is, users could insert those manually, or perhaps automatically if necessary, depending on context
|
|
14
|
+
// that way you don't need one prompt for everything and certain models won't need any help (i.e. Claude) vs
|
|
15
|
+
// llama4 which struggled with old run_script tool (now just stdin on run_command) so it might need some
|
|
16
|
+
// special instructions and yeah... I think that's a use case for these prompts
|
|
17
|
+
// /prompt llama4 ?
|
|
18
|
+
{
|
|
19
|
+
name: "examples",
|
|
20
|
+
description: "Novel examples of run_command tool use to nudge models to the possibilities. " +
|
|
21
|
+
"Based on assumption that most models understand shell commands/scripts very well.",
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "run_command",
|
|
25
|
+
description: "Include command output in the prompt. " +
|
|
26
|
+
"This is effectively a user tool call.",
|
|
27
|
+
arguments: [
|
|
28
|
+
{
|
|
29
|
+
name: "command",
|
|
30
|
+
required: true,
|
|
31
|
+
},
|
|
32
|
+
// if I care to keep the prompt tools then add stdin?
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
39
|
+
// if (request.params.name == "examples") {
|
|
40
|
+
// return GetExamplePromptMessages();
|
|
41
|
+
// } else
|
|
42
|
+
if (request.params.name !== "run_command") {
|
|
43
|
+
throw new Error("Unknown or not implemented prompt: " + request.params.name);
|
|
44
|
+
}
|
|
45
|
+
verbose_log("INFO: PromptRequest", request);
|
|
46
|
+
const command = String(request.params.arguments?.command);
|
|
47
|
+
if (!command) {
|
|
48
|
+
// TODO is there a format to follow for reporting failure like isError for tools?
|
|
49
|
+
throw new Error("Command is required");
|
|
50
|
+
}
|
|
51
|
+
// Is it possible/feasible to pass a path for the workdir when running the command?
|
|
52
|
+
// - currently it uses / (yikez)
|
|
53
|
+
// - IMO makes more sense to have it be based on the Zed workdir of each project
|
|
54
|
+
// - 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)
|
|
55
|
+
const { stdout, stderr } = await execAsync(command);
|
|
56
|
+
// 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
|
|
57
|
+
const messages = [
|
|
58
|
+
{
|
|
59
|
+
role: "user",
|
|
60
|
+
content: {
|
|
61
|
+
type: "text",
|
|
62
|
+
text: "I ran the following command, if there is any output it will be shown below:\n" +
|
|
63
|
+
command,
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
];
|
|
67
|
+
if (stdout) {
|
|
68
|
+
messages.push({
|
|
69
|
+
role: "user",
|
|
70
|
+
content: {
|
|
71
|
+
type: "text",
|
|
72
|
+
text: "STDOUT:\n" + stdout,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (stderr) {
|
|
77
|
+
messages.push({
|
|
78
|
+
role: "user",
|
|
79
|
+
content: {
|
|
80
|
+
type: "text",
|
|
81
|
+
text: "STDERR:\n" + stderr,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
verbose_log("INFO: PromptResponse", messages);
|
|
86
|
+
return { messages };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
function GetExamplePromptMessages() {
|
|
90
|
+
throw new Error("Function not implemented.");
|
|
91
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { exec } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { execFileWithInput } from "./exec-utils.js";
|
|
4
|
+
import { always_log } from "./always_log.js";
|
|
5
|
+
import { messagesFor } from "./messages.js";
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
async function execute(command, stdin, options) {
|
|
8
|
+
// PRN merge calls to exec into one single paradigm with conditional STDIN handled in one spot?
|
|
9
|
+
// right now no STDIN => exec directly and let it throw to catch failures
|
|
10
|
+
// w/ STDIN => you manually glue together callbacks + promises (i.e. reject)
|
|
11
|
+
// feels sloppy to say the least, notably the error handling with ExecExeption error that has stdin/stderr on it
|
|
12
|
+
if (!stdin) {
|
|
13
|
+
return await execAsync(command, options);
|
|
14
|
+
}
|
|
15
|
+
return await execFileWithInput(command, stdin, options);
|
|
16
|
+
}
|
|
17
|
+
export async function runCommand(args) {
|
|
18
|
+
const command = args?.command;
|
|
19
|
+
if (!command) {
|
|
20
|
+
const message = "Command is required, current value: " + command;
|
|
21
|
+
return {
|
|
22
|
+
isError: true,
|
|
23
|
+
content: [{ type: "text", text: message }],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const options = { encoding: "utf8" };
|
|
27
|
+
if (args?.workdir) {
|
|
28
|
+
options.cwd = String(args.workdir);
|
|
29
|
+
}
|
|
30
|
+
const stdin = args?.stdin;
|
|
31
|
+
try {
|
|
32
|
+
const result = await execute(command, stdin, options);
|
|
33
|
+
return {
|
|
34
|
+
content: messagesFor(result),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
// PRN do I want to differentiate non-command related error (i.e. if messagesFor blows up
|
|
39
|
+
// or presumably if smth else goes wrong with the node code in exec that isn't command related
|
|
40
|
+
// if so, write a test first
|
|
41
|
+
// console.log("ERROR_runCommand", error);
|
|
42
|
+
// ExecException (error + stdout/stderr) merged
|
|
43
|
+
// - IIUC this happens on uncaught failures
|
|
44
|
+
// - but if you catch an exec() promise failure (or use exec's callback) => you get separated values: error, stdout, stderr
|
|
45
|
+
// - which is why I mirror this response type in my reject(error) calls
|
|
46
|
+
//
|
|
47
|
+
// 'error' example:
|
|
48
|
+
// code: 127,
|
|
49
|
+
// killed: false,
|
|
50
|
+
// signal: null,
|
|
51
|
+
// cmd: 'nonexistentcommand',
|
|
52
|
+
// stdout: '',
|
|
53
|
+
// stderr: '/bin/sh: nonexistentcommand: command not found\n'
|
|
54
|
+
const response = {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: messagesFor(error),
|
|
57
|
+
};
|
|
58
|
+
always_log("WARN: run_command failed", response);
|
|
59
|
+
return response;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/build/tools.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { verbose_log } from "./always_log.js";
|
|
4
|
+
import { runCommand } from "./run-command.js";
|
|
5
|
+
export function reisterTools(server) {
|
|
6
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
7
|
+
verbose_log("INFO: ListTools");
|
|
8
|
+
return {
|
|
9
|
+
tools: [
|
|
10
|
+
{
|
|
11
|
+
name: "run_command",
|
|
12
|
+
description: "Run a command on this " + os.platform() + " machine",
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
command: {
|
|
17
|
+
type: "string",
|
|
18
|
+
description: "Command with args",
|
|
19
|
+
},
|
|
20
|
+
workdir: {
|
|
21
|
+
// previous run_command calls can probe the filesystem and find paths to change to
|
|
22
|
+
type: "string",
|
|
23
|
+
description: "Optional, current working directory",
|
|
24
|
+
},
|
|
25
|
+
stdin: {
|
|
26
|
+
type: "string",
|
|
27
|
+
description: "Optional, text to pipe into the command's STDIN. For example, pass a python script to python3. Or, pass text for a new file to the cat command to create it!",
|
|
28
|
+
},
|
|
29
|
+
// args to consider:
|
|
30
|
+
// - env - obscure cases where command takes a param only via an env var?
|
|
31
|
+
// - timeout - lets just hard code this for now
|
|
32
|
+
},
|
|
33
|
+
required: ["command"],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
40
|
+
verbose_log("INFO: ToolRequest", request);
|
|
41
|
+
switch (request.params.name) {
|
|
42
|
+
case "run_command": {
|
|
43
|
+
return await runCommand(request.params.arguments);
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
throw new Error("Unknown tool");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
2
|
+
"name": "mcp-server-commands",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "An MCP server to run arbitrary commands",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-server-commands": "./build/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"build"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"clean": "rm -rf build",
|
|
15
|
+
"build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
|
|
16
|
+
"prepare": "npm run build",
|
|
17
|
+
"watch": "npm run build && tsc --watch",
|
|
18
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
19
|
+
"test": "jest",
|
|
20
|
+
"test:watch": "jest --watch",
|
|
21
|
+
"test:integration": "jest tests/integration"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "1.9.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/jest": "^29.5.14",
|
|
28
|
+
"@types/node": "^22.14.1",
|
|
29
|
+
"jest": "^29.7.0",
|
|
30
|
+
"ts-jest": "^29.3.2",
|
|
31
|
+
"typescript": "^5.8.3"
|
|
32
|
+
}
|
|
33
33
|
}
|