mcp-server-commands 0.5.0 → 0.7.3
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 +42 -4
- package/build/always_log.js +44 -0
- package/build/decide/spawn-sync.js +31 -0
- package/build/decide/spawn.js +92 -0
- package/build/exec-utils.js +65 -46
- package/build/index.js +5 -158
- package/build/messages.js +58 -7
- package/build/prompts.js +87 -0
- package/build/run_process.js +90 -0
- package/build/tools.js +94 -0
- package/package.json +5 -2
- package/build/run-command.js +0 -44
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
## Tools
|
|
2
2
|
|
|
3
|
-
Tools are for LLMs to request. Claude Sonnet 3.5 intelligently uses `
|
|
3
|
+
Tools are for LLMs to request. Claude Sonnet 3.5 intelligently uses `run_process`. And, initial testing shows promising results with [Groq Desktop with MCP](https://github.com/groq/groq-desktop-beta) and `llama4` models.
|
|
4
4
|
|
|
5
5
|
Currently, just one command to rule them all!
|
|
6
6
|
|
|
7
|
-
- `
|
|
7
|
+
- `run_process` - run a command, i.e. `hostname` or `ls -al` or `echo "hello world"` etc
|
|
8
8
|
- Returns `STDOUT` and `STDERR` as text
|
|
9
9
|
- Optional `stdin` parameter means your LLM can
|
|
10
|
-
- pass
|
|
10
|
+
- pass scripts over `STDIN` to commands like `fish`, `bash`, `zsh`, `python`
|
|
11
11
|
- create files with `cat >> foo/bar.txt` from the text in `stdin`
|
|
12
12
|
|
|
13
13
|
> [!WARNING]
|
|
@@ -24,7 +24,9 @@ Currently, just one command to rule them all!
|
|
|
24
24
|
|
|
25
25
|
Prompts are for users to include in chat history, i.e. via `Zed`'s slash commands (in its AI Chat panel)
|
|
26
26
|
|
|
27
|
-
- `
|
|
27
|
+
- `run_process` - generate a prompt message with the command output
|
|
28
|
+
|
|
29
|
+
* FYI this was mostly a learning exercise... I see this as a user requested tool call. That's a fancy way to say, it's a template for running a command and passing the outputs to the model!
|
|
28
30
|
|
|
29
31
|
## Development
|
|
30
32
|
|
|
@@ -82,6 +84,42 @@ Make sure to run `npm run build`
|
|
|
82
84
|
}
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
## Local Models
|
|
88
|
+
|
|
89
|
+
- Most models are trained such that they don't think they can run commands for you.
|
|
90
|
+
- Sometimes, they use tools w/o hesitation... other times, I have to coax them.
|
|
91
|
+
- Use a system prompt or prompt template to instruct that they should follow user requests. Including to use `run_processs` without double checking.
|
|
92
|
+
- Ollama is a great way to run a model locally (w/ Open-WebUI)
|
|
93
|
+
|
|
94
|
+
```sh
|
|
95
|
+
# NOTE: make sure to review variants and sizes, so the model fits in your VRAM to perform well!
|
|
96
|
+
|
|
97
|
+
# Probably the best so far is [OpenHands LM](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model)
|
|
98
|
+
ollama pull https://huggingface.co/lmstudio-community/openhands-lm-32b-v0.1-GGUF
|
|
99
|
+
|
|
100
|
+
# https://ollama.com/library/devstral
|
|
101
|
+
ollama pull devstral
|
|
102
|
+
|
|
103
|
+
# Qwen2.5-Coder has tool use but you have to coax it
|
|
104
|
+
ollama pull qwen2.5-coder
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### HTTP / OpenAPI
|
|
108
|
+
|
|
109
|
+
The server is implemented with the `STDIO` transport.
|
|
110
|
+
For `HTTP`, use [`mcpo`](https://github.com/open-webui/mcpo) for an `OpenAPI` compatible web server interface.
|
|
111
|
+
This works with [`Open-WebUI`](https://github.com/open-webui/open-webui)
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uvx mcpo --port 3010 --api-key "supersecret" -- npx mcp-server-commands
|
|
115
|
+
|
|
116
|
+
# uvx runs mcpo => mcpo run npx => npx runs mcp-server-commands
|
|
117
|
+
# then, mcpo bridges STDIO <=> HTTP
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
> [!WARNING]
|
|
121
|
+
> I briefly used `mcpo` with `open-webui`, make sure to vet it for security concerns.
|
|
122
|
+
|
|
85
123
|
### Logging
|
|
86
124
|
|
|
87
125
|
Claude Desktop app writes logs to `~/Library/Logs/Claude/mcp-server-mcp-server-commands.log`
|
package/build/always_log.js
CHANGED
|
@@ -1,4 +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
|
+
}
|
|
1
41
|
export function always_log(message, data) {
|
|
42
|
+
const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
|
|
43
|
+
if (isJest) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
2
46
|
if (data) {
|
|
3
47
|
console.error(message + ": " + JSON.stringify(data));
|
|
4
48
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
// NOTES:
|
|
3
|
+
// - blocks event loop in exchange for convenience
|
|
4
|
+
async function runSyncWithStdio(stdioValue) {
|
|
5
|
+
console.log("\n\n" + "=".repeat(20) + " run " + "=".repeat(20) + "\n");
|
|
6
|
+
const options = {
|
|
7
|
+
stdio: stdioValue,
|
|
8
|
+
};
|
|
9
|
+
console.log("options", options);
|
|
10
|
+
const command = "ls";
|
|
11
|
+
const args = ["-lh", "/usr"];
|
|
12
|
+
const result = spawnSync(command, args, options);
|
|
13
|
+
const chalk = (await import('chalk')).default;
|
|
14
|
+
console.log(chalk.green("\n## spawnSync COMPLETE\n"));
|
|
15
|
+
console.log("\n## spawnSync COMPLETE\n");
|
|
16
|
+
console.log("result", result);
|
|
17
|
+
if (result.stdout && result.stdout.byteLength > 0) {
|
|
18
|
+
console.log(`stdout (#${result.stdout.byteLength}):`, result.stdout.toString());
|
|
19
|
+
}
|
|
20
|
+
if (result.stderr && result.stderr.byteLength > 0) {
|
|
21
|
+
// PRN look for diff .length vs .byteLength => see if can find scenarios where they aren't mismatched in my codebases (for other purposes, i.e. RAG matches column offset calculations)
|
|
22
|
+
console.log(`stderr (#${result.stderr.byteLength}):`, result.stderr.toString());
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
// *** KEY setting => DO NOT USE "inherit" type behavior w/ MCP server
|
|
27
|
+
// b/c then it is writing to the STDOUT/ERR and reading from STDIN that are intended for client to server JSON based comms only!
|
|
28
|
+
//
|
|
29
|
+
runSyncWithStdio("inherit"); // ls output is BEFORE result returned (b/c inherit means use parent process STDIN/OUT/ERR... so it writes immediately to STDOUT!
|
|
30
|
+
// buffer STDOUT/ERR
|
|
31
|
+
runSyncWithStdio("pipe"); // ls output is obtained after result returned (b/c piped to buffer)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { once } from 'node:events';
|
|
3
|
+
async function ls() {
|
|
4
|
+
const ls = spawn('ls', ['-lh', '/usr']);
|
|
5
|
+
// NOTES:
|
|
6
|
+
// - spawn/spawnSync are the foundation of all other methods
|
|
7
|
+
// - async, non-blocking (event loop)
|
|
8
|
+
ls.stdout.on('data', (data) => {
|
|
9
|
+
console.log(`stdout: ${data}`);
|
|
10
|
+
});
|
|
11
|
+
ls.stderr.on('data', (data) => {
|
|
12
|
+
console.error(`stderr: ${data}`);
|
|
13
|
+
});
|
|
14
|
+
const [code] = await once(ls, 'close');
|
|
15
|
+
console.log(`child process exited with code ${code}`);
|
|
16
|
+
}
|
|
17
|
+
// await ls();
|
|
18
|
+
async function cat_hello() {
|
|
19
|
+
const cat = spawn('cat');
|
|
20
|
+
cat.stdout.on('data', (data) => {
|
|
21
|
+
console.log(`stdout: ${data}`);
|
|
22
|
+
});
|
|
23
|
+
cat.stderr.on('data', (data) => {
|
|
24
|
+
console.error(`stderr: ${data}`);
|
|
25
|
+
});
|
|
26
|
+
cat.stdin.write("HEY FUCKTURD");
|
|
27
|
+
cat.stdin.end();
|
|
28
|
+
const [code] = await once(cat, 'close');
|
|
29
|
+
console.log(`child process exited with code ${code}`);
|
|
30
|
+
}
|
|
31
|
+
// await cat_hello();
|
|
32
|
+
async function sleep_abort() {
|
|
33
|
+
const aborter = new AbortController();
|
|
34
|
+
const options = {};
|
|
35
|
+
// options.shell = ""
|
|
36
|
+
options.signal = aborter.signal;
|
|
37
|
+
const cat = spawn('sleep', ["5"], options);
|
|
38
|
+
if (cat.stdout) {
|
|
39
|
+
cat.stdout.on('data', (data) => {
|
|
40
|
+
console.log(`stdout: ${data}`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
if (cat.stderr) {
|
|
44
|
+
cat.stderr.on('data', (data) => {
|
|
45
|
+
console.error(`stderr: ${data}`);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
cat.on('error', (err) => {
|
|
49
|
+
console.log("ERR", err);
|
|
50
|
+
});
|
|
51
|
+
aborter.abort();
|
|
52
|
+
const [code] = await once(cat, 'close');
|
|
53
|
+
console.log(`child process exited with code ${code}`);
|
|
54
|
+
}
|
|
55
|
+
// await sleep_abort();
|
|
56
|
+
async function shells(child_process) {
|
|
57
|
+
if (child_process.stdout) {
|
|
58
|
+
child_process.stdout.on('data', (data) => {
|
|
59
|
+
console.log(`stdout: ${data}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (child_process.stderr) {
|
|
63
|
+
child_process.stderr.on('data', (data) => {
|
|
64
|
+
console.error(`stderr: ${data}`);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
child_process.on('error', (err) => {
|
|
68
|
+
console.log("ERR", err);
|
|
69
|
+
});
|
|
70
|
+
const [code] = await once(child_process, 'close');
|
|
71
|
+
console.log(`child process exited with code ${code}`);
|
|
72
|
+
}
|
|
73
|
+
await shells(spawn('echo', ["5"], { shell: false })); // works
|
|
74
|
+
// await shells(spawn('echo 55', { shell: false })); // fails
|
|
75
|
+
// *** pass only command arg w/ cmd+args and shell=true
|
|
76
|
+
await shells(spawn('echo 66', { shell: true })); // works
|
|
77
|
+
await shells(spawn('echo $fish_pid', { shell: true })); // no fish_pid (not fish shell)
|
|
78
|
+
await shells(spawn('echo $fish_pid', { shell: "fish" })); // works! fish shell!
|
|
79
|
+
// await shells(spawn('echo', ["5"], { shell: true })); // works but w/ warning about args are just concatenated (not sanitized)
|
|
80
|
+
// default shell info
|
|
81
|
+
await shells(spawn("echo", ['$PATH'], { shell: false })); // verbatim prints "$PATH" b/c no shell
|
|
82
|
+
await shells(spawn("echo", ['$PATH'], { shell: true })); // shows PATH variable value // warning too (first call w/ cmd,args array)
|
|
83
|
+
await shells(spawn("echo", ['$$'], { shell: false })); // verbatim $$ printed (no shell)
|
|
84
|
+
await shells(spawn("echo", ['$$'], { shell: true })); // prints PID of default shell
|
|
85
|
+
// await shells(spawn("set", { shell: true })); // prints PID of default shell
|
|
86
|
+
await shells(spawn("set -x; echo $0; echo $PATH; echo $BASH_VERSION", { shell: true })); // /bin/sh => bash in POSIX mode
|
|
87
|
+
// $0 = /bin/sh (on mac/linux)
|
|
88
|
+
// $BASH_VERSION
|
|
89
|
+
await shells(spawn("declare", { shell: true })); // dump default shell variables
|
|
90
|
+
// await shells(spawn("ps", { shell: false })); // default shell info
|
|
91
|
+
console.log("HERE:");
|
|
92
|
+
await shells(spawn("echo", ['$PATH'])); // default is shell=false
|
package/build/exec-utils.js
CHANGED
|
@@ -1,53 +1,72 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* @param {string} interpreter - The file to execute.
|
|
5
|
-
* @param {string} stdin - 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, 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, options);
|
|
15
|
-
}
|
|
1
|
+
// TODO cleanup exec usages once spawn is ready
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
export async function spawn_wrapped(command, args, stdin, options) {
|
|
16
4
|
return new Promise((resolve, reject) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
if (!stdin) {
|
|
6
|
+
// FYI default is all 'pipe' (works when stdin is provided)
|
|
7
|
+
// 'ignore' attaches /dev/null
|
|
8
|
+
// order: [STDIN, STDOUT, STDERR]
|
|
9
|
+
options.stdio = ['ignore', 'pipe', 'pipe'];
|
|
10
|
+
// ? I wonder if this was related to fishWorkaround issue w/ STDIN (see commit history)... I was using base64 encoding to pass STDIN
|
|
11
|
+
}
|
|
12
|
+
const child = spawn(command, args, options);
|
|
13
|
+
// PRN return pid to callers?
|
|
14
|
+
console.log(`child.pid: ${child.pid}`);
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
if (child.stdin && stdin) {
|
|
30
18
|
child.stdin.write(stdin);
|
|
31
19
|
child.stdin.end();
|
|
32
20
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
21
|
+
if (child.stdout) {
|
|
22
|
+
child.stdout.on("data", (chunk) => {
|
|
23
|
+
stdout += chunk.toString();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (child.stderr) {
|
|
27
|
+
child.stderr.on("data", (chunk) => {
|
|
28
|
+
stderr += chunk.toString();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
let errored = false;
|
|
32
|
+
child.on("error", (err) => {
|
|
33
|
+
// ChildProcess 'error' docs: https://nodejs.org/api/child_process.html#event-error
|
|
34
|
+
// error running process
|
|
35
|
+
// IIUC not just b/c of command failed w/ non-zero exit code
|
|
36
|
+
const result = {
|
|
37
|
+
stdout: stdout,
|
|
38
|
+
stderr: stderr,
|
|
39
|
+
//
|
|
40
|
+
code: err.code,
|
|
41
|
+
signal: err.signal,
|
|
42
|
+
//
|
|
43
|
+
message: err.message,
|
|
44
|
+
// killed: (err as any).killed,
|
|
45
|
+
cmd: command, // TODO does error have .cmd on it? is it the composite result of processing in spawn too? (or same as I passed)
|
|
46
|
+
};
|
|
47
|
+
// console.log("ON_ERROR", result);
|
|
48
|
+
errored = true;
|
|
49
|
+
reject(result);
|
|
50
|
+
});
|
|
51
|
+
child.on("close", (code, signal) => {
|
|
52
|
+
// ChildProcess 'close' docs: https://nodejs.org/api/child_process.html#event-close
|
|
53
|
+
// 'close' is after child process ends AND stdio streams are closed
|
|
54
|
+
// - after 'exit' or 'error'
|
|
55
|
+
//
|
|
56
|
+
// * do not resolve if 'error' already called (promise is already resolved)
|
|
57
|
+
if (errored)
|
|
58
|
+
return;
|
|
59
|
+
// either code is set, or signal, but NOT BOTH
|
|
60
|
+
// signal if process killed
|
|
61
|
+
// FYI close does not mean code==0
|
|
62
|
+
const result = {
|
|
63
|
+
stdout: stdout,
|
|
64
|
+
stderr: stderr,
|
|
65
|
+
code: code ?? undefined,
|
|
66
|
+
signal: signal ?? undefined,
|
|
67
|
+
};
|
|
68
|
+
// console.log("ON_CLOSE", result);
|
|
69
|
+
resolve(result);
|
|
50
70
|
});
|
|
51
71
|
});
|
|
52
72
|
}
|
|
53
|
-
export { execFileWithInput };
|
package/build/index.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import os from
|
|
2
|
+
import os from "os";
|
|
3
3
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
-
import { exec } from "node:child_process";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
|
-
import { runCommand } from "./run-command.js";
|
|
9
5
|
import { createRequire } from "module";
|
|
10
|
-
import {
|
|
6
|
+
import { registerPrompts } from "./prompts.js";
|
|
7
|
+
import { reisterTools } from "./tools.js";
|
|
11
8
|
const require = createRequire(import.meta.url);
|
|
12
9
|
const { name: package_name, version: package_version, } = require("../package.json");
|
|
13
|
-
// TODO use .promises? in node api
|
|
14
|
-
const execAsync = promisify(exec);
|
|
15
|
-
let verbose = false;
|
|
16
|
-
// check CLI args:
|
|
17
|
-
if (process.argv.includes("--verbose")) {
|
|
18
|
-
verbose = true;
|
|
19
|
-
}
|
|
20
10
|
const server = new Server({
|
|
21
11
|
name: package_name,
|
|
22
12
|
version: package_version,
|
|
@@ -29,151 +19,8 @@ const server = new Server({
|
|
|
29
19
|
//logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
|
|
30
20
|
},
|
|
31
21
|
});
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
always_log("INFO: verbose logging disabled, enable it with --verbose");
|
|
37
|
-
}
|
|
38
|
-
function verbose_log(message, data) {
|
|
39
|
-
// https://modelcontextprotocol.io/docs/tools/debugging - mentions various ways to debug/troubleshoot (including dev tools)
|
|
40
|
-
//
|
|
41
|
-
// remember STDIO transport means can't log over STDOUT (client expects JSON messages per the spec)
|
|
42
|
-
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
43
|
-
// mentions STDERR is captured by the host app (i.e. Claude Desktop app)
|
|
44
|
-
// server.sendLoggingMessage is captured by MCP client (not Claude Desktop app)
|
|
45
|
-
// SO, IIUC use STDERR for logging into Claude Desktop app logs in:
|
|
46
|
-
// '~/Library/Logs/Claude/mcp.log'
|
|
47
|
-
if (verbose) {
|
|
48
|
-
always_log(message, data);
|
|
49
|
-
}
|
|
50
|
-
// inspector, catches these logs and shows them on left hand side of screen (sidebar)
|
|
51
|
-
// TODO add verbose parameter (CLI arg?)
|
|
52
|
-
// IF I wanted to log via MCP client logs (not sure what those are/do):
|
|
53
|
-
// I do not see inspector catching these logs :(, there is a server notifications section and it remains empty
|
|
54
|
-
//server.sendLoggingMessage({
|
|
55
|
-
// level: "info",
|
|
56
|
-
// data: message,
|
|
57
|
-
//});
|
|
58
|
-
// which results in something like:
|
|
59
|
-
//server.notification({
|
|
60
|
-
// method: "notifications/message",
|
|
61
|
-
// params: {
|
|
62
|
-
// level: "warning",
|
|
63
|
-
// logger: "mcp-server-commands",
|
|
64
|
-
// data: "ListToolsRequest2",
|
|
65
|
-
// },
|
|
66
|
-
//});
|
|
67
|
-
//
|
|
68
|
-
// FYI client should also requets a log level from the server, so that needs to be here at some point too
|
|
69
|
-
}
|
|
70
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
71
|
-
verbose_log("INFO: ListTools");
|
|
72
|
-
return {
|
|
73
|
-
tools: [
|
|
74
|
-
{
|
|
75
|
-
name: "run_command",
|
|
76
|
-
description: "Run a command on this " + os.platform() + " machine",
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: "object",
|
|
79
|
-
properties: {
|
|
80
|
-
command: {
|
|
81
|
-
type: "string",
|
|
82
|
-
description: "Command with args",
|
|
83
|
-
},
|
|
84
|
-
workdir: {
|
|
85
|
-
// previous run_command calls can probe the filesystem and find paths to change to
|
|
86
|
-
type: "string",
|
|
87
|
-
description: "Optional, current working directory",
|
|
88
|
-
},
|
|
89
|
-
stdin: {
|
|
90
|
-
type: "string",
|
|
91
|
-
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!",
|
|
92
|
-
},
|
|
93
|
-
// args to consider:
|
|
94
|
-
// - env - obscure cases where command takes a param only via an env var?
|
|
95
|
-
// - timeout - lets just hard code this for now
|
|
96
|
-
},
|
|
97
|
-
required: ["command"],
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
};
|
|
102
|
-
});
|
|
103
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
104
|
-
verbose_log("INFO: ToolRequest", request);
|
|
105
|
-
switch (request.params.name) {
|
|
106
|
-
case "run_command": {
|
|
107
|
-
return await runCommand(request.params.arguments);
|
|
108
|
-
}
|
|
109
|
-
default:
|
|
110
|
-
throw new Error("Unknown tool");
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
114
|
-
verbose_log("INFO: ListPrompts");
|
|
115
|
-
return {
|
|
116
|
-
prompts: [
|
|
117
|
-
{
|
|
118
|
-
name: "run_command",
|
|
119
|
-
description: "Include command output in the prompt. Instead of a tool call, the user decides what commands are relevant.",
|
|
120
|
-
arguments: [
|
|
121
|
-
{
|
|
122
|
-
name: "command",
|
|
123
|
-
required: true,
|
|
124
|
-
},
|
|
125
|
-
// if I care to keep the prompt tools then add stdin?
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
};
|
|
130
|
-
});
|
|
131
|
-
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
132
|
-
if (request.params.name !== "run_command") {
|
|
133
|
-
throw new Error("Unknown prompt");
|
|
134
|
-
}
|
|
135
|
-
verbose_log("INFO: PromptRequest", request);
|
|
136
|
-
const command = String(request.params.arguments?.command);
|
|
137
|
-
if (!command) {
|
|
138
|
-
throw new Error("Command is required");
|
|
139
|
-
}
|
|
140
|
-
// Is it possible/feasible to pass a path for the workdir when running the command?
|
|
141
|
-
// - currently it uses / (yikez)
|
|
142
|
-
// - IMO makes more sense to have it be based on the Zed workdir of each project
|
|
143
|
-
// - 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)
|
|
144
|
-
const { stdout, stderr } = await execAsync(command);
|
|
145
|
-
// 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
|
|
146
|
-
const messages = [
|
|
147
|
-
{
|
|
148
|
-
role: "user",
|
|
149
|
-
content: {
|
|
150
|
-
type: "text",
|
|
151
|
-
text: "I ran the following command, if there is any output it will be shown below:\n" +
|
|
152
|
-
command,
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
];
|
|
156
|
-
if (stdout) {
|
|
157
|
-
messages.push({
|
|
158
|
-
role: "user",
|
|
159
|
-
content: {
|
|
160
|
-
type: "text",
|
|
161
|
-
text: "STDOUT:\n" + stdout,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
if (stderr) {
|
|
166
|
-
messages.push({
|
|
167
|
-
role: "user",
|
|
168
|
-
content: {
|
|
169
|
-
type: "text",
|
|
170
|
-
text: "STDERR:\n" + stderr,
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
verbose_log("INFO: PromptResponse", messages);
|
|
175
|
-
return { messages };
|
|
176
|
-
});
|
|
22
|
+
reisterTools(server);
|
|
23
|
+
registerPrompts(server);
|
|
177
24
|
async function main() {
|
|
178
25
|
const transport = new StdioServerTransport();
|
|
179
26
|
await server.connect(transport);
|
package/build/messages.js
CHANGED
|
@@ -1,28 +1,79 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
export function resultFor(spawn_result) {
|
|
2
|
+
const result_obj = {
|
|
3
|
+
content: messagesFor(spawn_result),
|
|
4
|
+
};
|
|
5
|
+
if (spawn_result.code !== 0) {
|
|
6
|
+
result_obj.isError = true;
|
|
7
|
+
}
|
|
8
|
+
return result_obj;
|
|
9
|
+
}
|
|
4
10
|
export function messagesFor(result) {
|
|
5
11
|
const messages = [];
|
|
6
|
-
if (result.
|
|
12
|
+
if (result.code !== undefined) {
|
|
13
|
+
// FYI include EXIT_CODE always, to make EXPLICIT when it is NOT a FAILURE!
|
|
14
|
+
// will double underscore when a comman fails vs not stating if it was a failure
|
|
15
|
+
// some commands give no other indication of success!
|
|
16
|
+
messages.push({
|
|
17
|
+
name: "EXIT_CODE",
|
|
18
|
+
type: "text",
|
|
19
|
+
text: String(result.code),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// // PRN map COMMAND for failures so model can adjust what it passes vs what actually ran?
|
|
23
|
+
// if ("cmd" in result && result.cmd) {
|
|
24
|
+
// messages.push({
|
|
25
|
+
// name: "COMMAND",
|
|
26
|
+
// type: "text",
|
|
27
|
+
// text: result.cmd,
|
|
28
|
+
// });
|
|
29
|
+
// }
|
|
30
|
+
if ("message" in result && result.message) {
|
|
31
|
+
// at least need error.message from spawn errors
|
|
7
32
|
messages.push({
|
|
33
|
+
name: "MESSAGE",
|
|
8
34
|
type: "text",
|
|
9
35
|
text: result.message,
|
|
10
|
-
name: "ERROR",
|
|
11
36
|
});
|
|
12
37
|
}
|
|
38
|
+
if (result.signal) {
|
|
39
|
+
messages.push({
|
|
40
|
+
name: "SIGNAL",
|
|
41
|
+
type: "text",
|
|
42
|
+
text: result.signal,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
// // when is this set? what conditions? I tried `kill -9` and didn't trigger "error" event
|
|
46
|
+
// if ("killed" in result && result.killed) {
|
|
47
|
+
// // killed == true is the only time to include this
|
|
48
|
+
// messages.push({
|
|
49
|
+
// name: "KILLED",
|
|
50
|
+
// type: "text",
|
|
51
|
+
// text: "Process was killed",
|
|
52
|
+
// });
|
|
53
|
+
// }
|
|
13
54
|
if (result.stdout) {
|
|
14
55
|
messages.push({
|
|
56
|
+
name: "STDOUT",
|
|
15
57
|
type: "text",
|
|
16
58
|
text: result.stdout,
|
|
17
|
-
name: "STDOUT",
|
|
18
59
|
});
|
|
19
60
|
}
|
|
20
61
|
if (result.stderr) {
|
|
21
62
|
messages.push({
|
|
63
|
+
name: "STDERR",
|
|
22
64
|
type: "text",
|
|
23
65
|
text: result.stderr,
|
|
24
|
-
name: "STDERR",
|
|
25
66
|
});
|
|
26
67
|
}
|
|
27
68
|
return messages;
|
|
28
69
|
}
|
|
70
|
+
export function errorResult(message) {
|
|
71
|
+
return {
|
|
72
|
+
isError: true,
|
|
73
|
+
content: [{
|
|
74
|
+
name: "ERROR",
|
|
75
|
+
type: "text",
|
|
76
|
+
text: message,
|
|
77
|
+
}],
|
|
78
|
+
};
|
|
79
|
+
}
|
package/build/prompts.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
// ?? add prompts for various LLMs that tailor instructions to make them optimize use of run_process 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_process) 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: "run_process",
|
|
20
|
+
description: "Include command output in the prompt. " +
|
|
21
|
+
"This is effectively a user tool call.",
|
|
22
|
+
arguments: [
|
|
23
|
+
{ name: "mode", },
|
|
24
|
+
{ name: "command_line", },
|
|
25
|
+
{ name: "argv", },
|
|
26
|
+
{ name: "dry_run", },
|
|
27
|
+
// ? other args?
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
34
|
+
// if (request.params.name == "examples") {
|
|
35
|
+
// return GetExamplePromptMessages();
|
|
36
|
+
// } else
|
|
37
|
+
if (request.params.name !== "run_process") {
|
|
38
|
+
throw new Error("Unknown or not implemented prompt: " + request.params.name);
|
|
39
|
+
}
|
|
40
|
+
verbose_log("INFO: PromptRequest", request);
|
|
41
|
+
throw new Error("run_process not yet ported to prompts");
|
|
42
|
+
const command_line = String(request.params.arguments?.command_line);
|
|
43
|
+
const argv = String(request.params.arguments?.argv);
|
|
44
|
+
const mode = String(request.params.arguments?.mode);
|
|
45
|
+
const dry_run = Boolean(request.params.arguments?.dry_run);
|
|
46
|
+
// Is it possible/feasible to pass a path for the workdir when running the command?
|
|
47
|
+
// - currently it uses / (yikez)
|
|
48
|
+
// - IMO makes more sense to have it be based on the Zed workdir of each project
|
|
49
|
+
// - 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)
|
|
50
|
+
// TODO RUN_PROCESS MIGRATION - finish rest of migrtation to run_process
|
|
51
|
+
const { stdout, stderr } = await execAsync(command_line);
|
|
52
|
+
// 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
|
|
53
|
+
const messages = [
|
|
54
|
+
{
|
|
55
|
+
role: "user",
|
|
56
|
+
content: {
|
|
57
|
+
type: "text",
|
|
58
|
+
text: "I ran the following command, if there is any output it will be shown below:\n" +
|
|
59
|
+
command_line,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
if (stdout) {
|
|
64
|
+
messages.push({
|
|
65
|
+
role: "user",
|
|
66
|
+
content: {
|
|
67
|
+
type: "text",
|
|
68
|
+
text: "STDOUT:\n" + stdout,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (stderr) {
|
|
73
|
+
messages.push({
|
|
74
|
+
role: "user",
|
|
75
|
+
content: {
|
|
76
|
+
type: "text",
|
|
77
|
+
text: "STDERR:\n" + stderr,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
verbose_log("INFO: PromptResponse", messages);
|
|
82
|
+
return { messages };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function GetExamplePromptMessages() {
|
|
86
|
+
throw new Error("Function not implemented.");
|
|
87
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { spawn_wrapped } from "./exec-utils.js";
|
|
2
|
+
import { always_log } from "./always_log.js";
|
|
3
|
+
import { errorResult, messagesFor, resultFor } from "./messages.js";
|
|
4
|
+
export async function runProcess(args) {
|
|
5
|
+
const command_line = args?.command_line;
|
|
6
|
+
const argv = args?.argv;
|
|
7
|
+
const mode = String(args?.mode);
|
|
8
|
+
if (!mode) {
|
|
9
|
+
return errorResult("Mode is required");
|
|
10
|
+
}
|
|
11
|
+
const isShell = mode === "shell";
|
|
12
|
+
const isExecutable = mode === "executable";
|
|
13
|
+
if (!isShell && !isExecutable) {
|
|
14
|
+
return errorResult(`Invalid mode '${mode}'. Allowed values are 'shell' or 'executable'.`);
|
|
15
|
+
}
|
|
16
|
+
// * shared args
|
|
17
|
+
const spawn_options = {
|
|
18
|
+
// spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
|
|
19
|
+
encoding: "utf8"
|
|
20
|
+
};
|
|
21
|
+
if (args?.cwd) {
|
|
22
|
+
spawn_options.cwd = String(args.cwd);
|
|
23
|
+
}
|
|
24
|
+
// PRN args.env
|
|
25
|
+
if (args?.timeout_ms) {
|
|
26
|
+
spawn_options.timeout = Number(args.timeout_ms);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// default timeout after 30s
|
|
30
|
+
spawn_options.timeout = 30000;
|
|
31
|
+
}
|
|
32
|
+
// PRN windowsHide on Windows, signal, killSignal
|
|
33
|
+
// FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe'] https://nodejs.org/api/child_process.html#optionsstdio
|
|
34
|
+
// do not set inherit (this is what causes ripgrep to see STDIN socket and search it, thus hanging)
|
|
35
|
+
const stdin = args?.stdin ? String(args.stdin) : undefined; // TODO
|
|
36
|
+
const dryRun = Boolean(args?.dry_run);
|
|
37
|
+
try {
|
|
38
|
+
if (dryRun) {
|
|
39
|
+
// Build a descriptive plan without executing anything
|
|
40
|
+
let plan = '';
|
|
41
|
+
if (isShell) {
|
|
42
|
+
const cmd = String(args?.command_line);
|
|
43
|
+
const shell = process.env.SHELL || (process.platform === 'win32' ? 'cmd.exe' : '/bin/sh');
|
|
44
|
+
plan = `Shell mode: will execute command_line via ${shell}: ${cmd}`;
|
|
45
|
+
}
|
|
46
|
+
else if (isExecutable) {
|
|
47
|
+
const argv = args?.argv;
|
|
48
|
+
plan = `Executable mode: will spawn '${argv[0]}' with arguments ${JSON.stringify(argv.slice(1))}`;
|
|
49
|
+
}
|
|
50
|
+
return { content: [{ name: "PLAN", type: "text", text: plan }] };
|
|
51
|
+
}
|
|
52
|
+
if (isShell) {
|
|
53
|
+
if (!args?.command_line) {
|
|
54
|
+
return errorResult("Mode 'shell' requires a non‑empty 'command_line' parameter.");
|
|
55
|
+
}
|
|
56
|
+
if (args?.argv) {
|
|
57
|
+
return errorResult("Mode 'shell' does not accept an 'argv' parameter.");
|
|
58
|
+
}
|
|
59
|
+
spawn_options.shell = true;
|
|
60
|
+
const command_line = String(args?.command_line);
|
|
61
|
+
const result = await spawn_wrapped(command_line, [], stdin, spawn_options);
|
|
62
|
+
return resultFor(result);
|
|
63
|
+
}
|
|
64
|
+
if (isExecutable) {
|
|
65
|
+
const argv = args?.argv;
|
|
66
|
+
if (!Array.isArray(argv) || argv.length === 0) {
|
|
67
|
+
return errorResult("Mode 'executable' requires a non‑empty 'argv' array.");
|
|
68
|
+
}
|
|
69
|
+
if (args?.command_line) {
|
|
70
|
+
return errorResult("Mode 'executable' does not accept a 'command_line' parameter.");
|
|
71
|
+
}
|
|
72
|
+
spawn_options.shell = false;
|
|
73
|
+
const command = argv[0];
|
|
74
|
+
const commandArgs = argv.slice(1);
|
|
75
|
+
const result = await spawn_wrapped(command, commandArgs, stdin, spawn_options);
|
|
76
|
+
return resultFor(result);
|
|
77
|
+
}
|
|
78
|
+
// TODO... fish shell workaround (see exec-utils.ts) - ADD or FIX via a TEST FIRST
|
|
79
|
+
return errorResult("Unexpected execution path in runProcess, should not be possible");
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// TODO check if not SpawnFailure => i.e. test aborting/killing
|
|
83
|
+
const response = {
|
|
84
|
+
isError: true,
|
|
85
|
+
content: messagesFor(error),
|
|
86
|
+
};
|
|
87
|
+
always_log("WARN: run_process failed", error);
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
}
|
package/build/tools.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { verbose_log } from "./always_log.js";
|
|
4
|
+
import { runProcess } from "./run_process.js";
|
|
5
|
+
export function reisterTools(server) {
|
|
6
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
7
|
+
verbose_log("INFO: ListTools");
|
|
8
|
+
return {
|
|
9
|
+
// https://modelcontextprotocol.io/specification/2025-06-18/server/tools#tool // tool definition
|
|
10
|
+
// https://modelcontextprotocol.io/docs/learn/architecture#understanding-the-tool-execution-request // tool request/response
|
|
11
|
+
// typescript SDK docs:
|
|
12
|
+
// servers: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md
|
|
13
|
+
tools: [
|
|
14
|
+
{
|
|
15
|
+
// TODO RUN_PROCESS MIGRATION! provide examples in system message, that way it is very clear how to use these!
|
|
16
|
+
name: "run_process",
|
|
17
|
+
description: "Run a process on this " + os.platform() + " machine",
|
|
18
|
+
inputSchema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
// ListToolsResult => Tool type (in protocol) => https://modelcontextprotocol.io/specification/2025-06-18/schema#tool
|
|
22
|
+
// * appears I can do w/e I want to describe properties (as long as give a string key (name)) => hence enum below
|
|
23
|
+
mode: {
|
|
24
|
+
enum: ["shell", "executable"], // * I made this up
|
|
25
|
+
description: "What are you running, two choices: 'shell' or 'executable' (required, no default)",
|
|
26
|
+
// FYI only use default system shell, if the model wants a different shell, can explicitly run it with command_line/argv
|
|
27
|
+
// DO NOT duplicate the mechanism of specifying what runs by adding yet another executable field!
|
|
28
|
+
// spawn's options.shell is bool/string... string is command_name/path... that duplicates and complicates needlessly!
|
|
29
|
+
},
|
|
30
|
+
command_line: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "Shell command line. Required when mode='shell'. Forbidden when mode='executable'."
|
|
33
|
+
},
|
|
34
|
+
argv: {
|
|
35
|
+
minItems: 1, // * made up too
|
|
36
|
+
type: "array",
|
|
37
|
+
items: { type: "string" },
|
|
38
|
+
description: "Executable and arguments. argv[0] is the executable. Required when mode='executable'. Forbidden when mode='shell'."
|
|
39
|
+
},
|
|
40
|
+
cwd: {
|
|
41
|
+
// or "workdir" like before? => eval model behavior w/ each name?
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Optional to set working directory",
|
|
44
|
+
},
|
|
45
|
+
stdin: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "Optional text written to STDIN (written fully, then closed). Useful for heredoc-style input or file contents."
|
|
48
|
+
},
|
|
49
|
+
timeout_ms: {
|
|
50
|
+
type: "number",
|
|
51
|
+
description: "Optional timeout in milliseconds, defaults to 30,000ms",
|
|
52
|
+
},
|
|
53
|
+
dry_run: {
|
|
54
|
+
type: "boolean",
|
|
55
|
+
description: "Optional explain what would run, defaults to False."
|
|
56
|
+
// FYI this can help avoid the need for logging this information, I can call this as a user too!
|
|
57
|
+
// TODO, dump info about shell (if applicable) and program versions!
|
|
58
|
+
// - explain the node calls too, i.e. spawn('echo foo')
|
|
59
|
+
// - show paths of resolved resources (i.e. shell)
|
|
60
|
+
// - i.e. could run command --version to see its version (attempt various flags until one works, and/or have a lookup of common tools to know how to find their version)
|
|
61
|
+
}
|
|
62
|
+
// MAYBEs:
|
|
63
|
+
// - env - obscure cases where command takes a param only via an env var?
|
|
64
|
+
},
|
|
65
|
+
required: ["mode"],
|
|
66
|
+
// PRN add proprietary constraints explanation? only if model seems to struggle and there isn't a better fix...
|
|
67
|
+
// oneOf: [
|
|
68
|
+
// {
|
|
69
|
+
// properties: { mode: { const: "shell" } },
|
|
70
|
+
// required: ["command_line"],
|
|
71
|
+
// not: { required: ["argv"] }
|
|
72
|
+
// },
|
|
73
|
+
// {
|
|
74
|
+
// properties: { mode: { const: "executable" } },
|
|
75
|
+
// required: ["argv"],
|
|
76
|
+
// not: { required: ["command_line"] }
|
|
77
|
+
// }
|
|
78
|
+
// ]
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
85
|
+
verbose_log("INFO: ToolRequest", request);
|
|
86
|
+
switch (request.params.name) {
|
|
87
|
+
case "run_process": {
|
|
88
|
+
return await runProcess(request.params.arguments);
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
throw new Error("Unknown tool");
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-server-commands",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.3",
|
|
4
4
|
"description": "An MCP server to run arbitrary commands",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
19
19
|
"test": "jest",
|
|
20
20
|
"test:watch": "jest --watch",
|
|
21
|
-
"test:integration": "jest tests/integration"
|
|
21
|
+
"test:integration-all": "jest tests/integration",
|
|
22
|
+
"test:watch:integration-all": "jest --watch tests/integration",
|
|
23
|
+
"test:watch:integration-run_process": "jest --watch tests/integration/run_process.test.ts"
|
|
22
24
|
},
|
|
23
25
|
"dependencies": {
|
|
24
26
|
"@modelcontextprotocol/sdk": "1.9.0"
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
"devDependencies": {
|
|
27
29
|
"@types/jest": "^29.5.14",
|
|
28
30
|
"@types/node": "^22.14.1",
|
|
31
|
+
"chalk": "^5.6.2",
|
|
29
32
|
"jest": "^29.7.0",
|
|
30
33
|
"ts-jest": "^29.3.2",
|
|
31
34
|
"typescript": "^5.8.3"
|
package/build/run-command.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
if (!stdin) {
|
|
9
|
-
return await execAsync(command, options);
|
|
10
|
-
}
|
|
11
|
-
return await execFileWithInput(command, stdin, options);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* Executes a command and returns the result as CallToolResult.
|
|
15
|
-
*/
|
|
16
|
-
export async function runCommand(args) {
|
|
17
|
-
const command = args?.command;
|
|
18
|
-
if (!command) {
|
|
19
|
-
const message = "Command is required, current value: " + command;
|
|
20
|
-
return {
|
|
21
|
-
isError: true,
|
|
22
|
-
content: [{ type: "text", text: message }],
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
const options = { encoding: "utf8" };
|
|
26
|
-
if (args?.workdir) {
|
|
27
|
-
options.cwd = String(args.workdir);
|
|
28
|
-
}
|
|
29
|
-
const stdin = args?.stdin;
|
|
30
|
-
try {
|
|
31
|
-
const result = await execute(command, stdin, options);
|
|
32
|
-
return {
|
|
33
|
-
content: messagesFor(result),
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
const response = {
|
|
38
|
-
isError: true,
|
|
39
|
-
content: messagesFor(error),
|
|
40
|
-
};
|
|
41
|
-
always_log("WARN: run_command failed", response);
|
|
42
|
-
return response;
|
|
43
|
-
}
|
|
44
|
-
}
|