mcp-server-commands 0.7.4 → 0.8.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 +13 -0
- package/build/exec-utils.js +141 -27
- package/build/index.js +2 -2
- package/build/logging.js +51 -0
- package/build/prompts.js +1 -5
- package/build/tools.js +16 -39
- package/package.json +1 -1
- package/build/always_log.js +0 -52
- package/build/run_process.js +0 -90
package/README.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## `runProcess` tool
|
|
2
|
+
|
|
3
|
+
The `runProcess` tool runs processes on the host machine. There are two mutually exclusive ways to invoke it:
|
|
4
|
+
|
|
5
|
+
1. **`command_line`** (string) — Executed via the system's default shell (just like typing into `bash`/`fish`/`pwsh`/etc). Shell features like pipes, redirects, and variable expansion all work.
|
|
6
|
+
2. **`argv`** (string array) — Direct executable invocation. `argv[0]` is the executable, the rest are arguments. No shell interpretation.
|
|
7
|
+
|
|
8
|
+
You cannot pass both. The tool infers whether to use a shell from which parameter you provide.
|
|
9
|
+
|
|
10
|
+
If you want your model to use specific shell(s) on a system, I would list them in your system prompt. Or, maybe in your tool instructions, though models tend to pay better attention to examples in a system prompt.
|
|
11
|
+
|
|
12
|
+
Let me know if you encounter problems!
|
|
13
|
+
|
|
1
14
|
## Tools
|
|
2
15
|
|
|
3
16
|
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.
|
package/build/exec-utils.js
CHANGED
|
@@ -1,17 +1,82 @@
|
|
|
1
1
|
// TODO cleanup exec usages once spawn is ready
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { performance } from "perf_hooks";
|
|
4
|
+
import { is_verbose, verbose_log } from "./logging.js";
|
|
5
|
+
import { resultFor } from "./messages.js";
|
|
6
|
+
import { errorResult } from "./messages.js";
|
|
7
|
+
export function runProcess(runProcessArgs) {
|
|
8
|
+
const startTime = performance.now();
|
|
9
|
+
const options = {
|
|
10
|
+
// spawn options: https://nodejs.org/api/child_process.html#child_processspawncommand-args-options
|
|
11
|
+
encoding: "utf8"
|
|
12
|
+
};
|
|
13
|
+
if (runProcessArgs?.cwd) {
|
|
14
|
+
options.cwd = String(runProcessArgs.cwd);
|
|
15
|
+
}
|
|
16
|
+
const stdin = runProcessArgs?.stdin ? String(runProcessArgs.stdin) : undefined;
|
|
17
|
+
// ---------------------------------------------------------------------
|
|
18
|
+
// RunProcess argument handling – determine the actual command and args.
|
|
19
|
+
// ---------------------------------------------------------------------
|
|
20
|
+
const isShellMode = Boolean(runProcessArgs?.command_line);
|
|
21
|
+
const isExecutableMode = Array.isArray(runProcessArgs?.argv) && (runProcessArgs?.argv).length > 0;
|
|
22
|
+
if (isShellMode && isExecutableMode) {
|
|
23
|
+
return Promise.resolve(errorResult("Cannot pass both 'command_line' and 'argv'. Use one or the other."));
|
|
24
|
+
}
|
|
25
|
+
if (!isShellMode && !isExecutableMode) {
|
|
26
|
+
return Promise.resolve(errorResult("Either 'command_line' (string) or 'argv' (array) is required."));
|
|
27
|
+
}
|
|
28
|
+
// Resolve the command/args based on the mode.
|
|
29
|
+
let execCommand = "";
|
|
30
|
+
let execArgs = [];
|
|
31
|
+
if (isShellMode) {
|
|
32
|
+
options.shell = true;
|
|
33
|
+
execCommand = String(runProcessArgs.command_line);
|
|
34
|
+
execArgs = [];
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
options.shell = false;
|
|
38
|
+
const argv = runProcessArgs?.argv;
|
|
39
|
+
execCommand = argv[0];
|
|
40
|
+
execArgs = argv.slice(1);
|
|
41
|
+
}
|
|
42
|
+
const logWithElapsedTime = (msg, ...rest) => {
|
|
43
|
+
if (!is_verbose)
|
|
44
|
+
return;
|
|
45
|
+
const elapsed = ((performance.now() - startTime) / 1000).toFixed(3);
|
|
46
|
+
verbose_log(`[${elapsed}s] ${msg}`, ...rest, execCommand, execArgs);
|
|
47
|
+
};
|
|
48
|
+
let child_pid;
|
|
49
|
+
const promise = new Promise((resolve, reject) => {
|
|
5
50
|
if (!stdin) {
|
|
6
|
-
//
|
|
7
|
-
// '
|
|
8
|
-
//
|
|
51
|
+
// PRN windowsHide on Windows, signal, killSignal
|
|
52
|
+
// FYI spawn_options.stdio => default is perfect ['pipe', 'pipe', 'pipe']
|
|
53
|
+
// order: [STDIN, STDOUT, STDERR]
|
|
54
|
+
// https://nodejs.org/api/child_process.html#optionsstdio
|
|
55
|
+
// 'ignore' attaches /dev/null
|
|
56
|
+
// do not set 'inherit' (causes ripgrep to see STDIN socket and search it, thus hanging)
|
|
9
57
|
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
58
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
//
|
|
59
|
+
// remove timeout on spawn options (if set) so the built‑in spawn timeout does not interfere
|
|
60
|
+
delete options.timeout;
|
|
61
|
+
// Use a detached child so we can kill the entire process group.
|
|
62
|
+
options.detached = true;
|
|
63
|
+
let settled = false;
|
|
64
|
+
const settle = (result, isError) => {
|
|
65
|
+
if (settled)
|
|
66
|
+
return;
|
|
67
|
+
settled = true;
|
|
68
|
+
if (timer)
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
if (isError) {
|
|
71
|
+
resolve(resultFor(result));
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
resolve(resultFor(result));
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const child = spawn(execCommand, execArgs, options);
|
|
78
|
+
logWithElapsedTime(`START SPAWN child.pid: ${child.pid}`);
|
|
79
|
+
child_pid = child.pid;
|
|
15
80
|
let stdout = "";
|
|
16
81
|
let stderr = "";
|
|
17
82
|
if (child.stdin && stdin) {
|
|
@@ -28,45 +93,94 @@ export async function spawn_wrapped(command, args, stdin, options) {
|
|
|
28
93
|
stderr += chunk.toString();
|
|
29
94
|
});
|
|
30
95
|
}
|
|
31
|
-
|
|
96
|
+
child.on("exit", (code, signal) => {
|
|
97
|
+
// child process streams MAY still be open when EXIT is emitted (use close if need to ensure they're closed)
|
|
98
|
+
// "close" will come after "exit" once process is terminated + streams are closed
|
|
99
|
+
// so for now use "close" to determine if process was terminated too, that way you can access STDOUT/STDERR reliably for returning full output to agent
|
|
100
|
+
logWithElapsedTime("EXIT", { code, signal });
|
|
101
|
+
});
|
|
102
|
+
child.on("spawn", () => {
|
|
103
|
+
// emitted after child process starts successfully
|
|
104
|
+
// if child doesn't start, error emitted instead
|
|
105
|
+
// emitted BEFORE any data received via stdout/stderr
|
|
106
|
+
logWithElapsedTime("SPAWN");
|
|
107
|
+
});
|
|
108
|
+
// Timeout handling – kill the whole process group after the supplied timeout.
|
|
109
|
+
let timer = null;
|
|
110
|
+
let timeoutMs = Number(runProcessArgs?.timeout_ms);
|
|
111
|
+
if (Number.isNaN(timeoutMs)) {
|
|
112
|
+
timeoutMs = 30_000;
|
|
113
|
+
}
|
|
114
|
+
timer = setTimeout(() => {
|
|
115
|
+
if (process.platform !== "win32") {
|
|
116
|
+
if (child.pid) {
|
|
117
|
+
try {
|
|
118
|
+
process.kill(-child.pid, "SIGTERM");
|
|
119
|
+
}
|
|
120
|
+
catch (_) { }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
child.kill("SIGTERM");
|
|
125
|
+
}
|
|
126
|
+
const killTimeout = setTimeout(() => {
|
|
127
|
+
if (process.platform !== "win32") {
|
|
128
|
+
if (child.pid) {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(-child.pid, "SIGKILL");
|
|
131
|
+
}
|
|
132
|
+
catch (_) { }
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
child.kill("SIGKILL");
|
|
137
|
+
}
|
|
138
|
+
}, 2000);
|
|
139
|
+
const clearKill = () => clearTimeout(killTimeout);
|
|
140
|
+
child.once("exit", clearKill);
|
|
141
|
+
child.once("close", clearKill);
|
|
142
|
+
}, timeoutMs);
|
|
32
143
|
child.on("error", (err) => {
|
|
144
|
+
logWithElapsedTime("ERROR");
|
|
33
145
|
// ChildProcess 'error' docs: https://nodejs.org/api/child_process.html#event-error
|
|
34
146
|
// error running process
|
|
35
147
|
// IIUC not just b/c of command failed w/ non-zero exit code
|
|
36
148
|
const result = {
|
|
37
|
-
stdout
|
|
38
|
-
stderr
|
|
149
|
+
stdout,
|
|
150
|
+
stderr,
|
|
39
151
|
//
|
|
40
|
-
|
|
41
|
-
|
|
152
|
+
// one of these will always be non-null
|
|
153
|
+
code: err.code, // set if process exited, else null
|
|
154
|
+
signal: err.signal, // set if process was terminated by signal, else null
|
|
42
155
|
//
|
|
43
156
|
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)
|
|
157
|
+
// ? killed: (err as any).killed
|
|
46
158
|
};
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
reject(result);
|
|
159
|
+
logWithElapsedTime("ERROR_RESULT", result);
|
|
160
|
+
settle(result, true);
|
|
50
161
|
});
|
|
51
162
|
child.on("close", (code, signal) => {
|
|
163
|
+
logWithElapsedTime("CLOSE");
|
|
52
164
|
// ChildProcess 'close' docs: https://nodejs.org/api/child_process.html#event-close
|
|
53
165
|
// 'close' is after child process ends AND stdio streams are closed
|
|
54
166
|
// - after 'exit' or 'error'
|
|
55
|
-
//
|
|
56
|
-
// * do not resolve if 'error' already called (promise is already resolved)
|
|
57
|
-
if (errored)
|
|
58
|
-
return;
|
|
59
167
|
// either code is set, or signal, but NOT BOTH
|
|
60
168
|
// signal if process killed
|
|
61
169
|
// FYI close does not mean code==0
|
|
62
170
|
const result = {
|
|
63
|
-
stdout
|
|
64
|
-
stderr
|
|
171
|
+
stdout,
|
|
172
|
+
stderr,
|
|
173
|
+
//
|
|
65
174
|
code: code ?? undefined,
|
|
66
175
|
signal: signal ?? undefined,
|
|
176
|
+
//
|
|
67
177
|
};
|
|
68
|
-
|
|
69
|
-
|
|
178
|
+
logWithElapsedTime("CLOSE_RESULT", result);
|
|
179
|
+
settle(result, false);
|
|
70
180
|
});
|
|
71
181
|
});
|
|
182
|
+
// FYI later (when needed) I can map this onto the promise that comes back from runProcess too (and tie into that unit test I have that needs pid to terminate it)
|
|
183
|
+
// Resolve the underlying spawn result, then map to CallToolResult including PID.
|
|
184
|
+
promise.pid = child_pid;
|
|
185
|
+
return promise;
|
|
72
186
|
}
|
package/build/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { createRequire } from "module";
|
|
6
6
|
import { registerPrompts } from "./prompts.js";
|
|
7
|
-
import {
|
|
7
|
+
import { registerTools } from "./tools.js";
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const { name: package_name, version: package_version, } = require("../package.json");
|
|
10
10
|
const server = new Server({
|
|
@@ -19,7 +19,7 @@ const server = new Server({
|
|
|
19
19
|
//logging: {}, // for logging messages that don't seem to work yet or I am doing them wrong
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
|
-
|
|
22
|
+
registerTools(server);
|
|
23
23
|
registerPrompts(server);
|
|
24
24
|
async function main() {
|
|
25
25
|
const transport = new StdioServerTransport();
|
package/build/logging.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
export let is_verbose = false;
|
|
3
|
+
if (process.argv.includes("--verbose")) {
|
|
4
|
+
is_verbose = true;
|
|
5
|
+
}
|
|
6
|
+
const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
|
|
7
|
+
if (isJest) {
|
|
8
|
+
// is_verbose = true; // comment out to disable verbose logging in JEST test runner
|
|
9
|
+
}
|
|
10
|
+
if (is_verbose) {
|
|
11
|
+
always_log("INFO: verbose logging enabled");
|
|
12
|
+
}
|
|
13
|
+
export function verbose_log(message, data) {
|
|
14
|
+
if (is_verbose) {
|
|
15
|
+
always_log(message, data);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function always_log(message, data) {
|
|
19
|
+
// LOGGING NOTES:
|
|
20
|
+
// https://modelcontextprotocol.io/docs/tools/debugging#implementing-logging
|
|
21
|
+
// * STDIO transport => STDERR captured by host app (i.e. Claude Desktop which writes to ~/Library/Logs/Claude/mcp.log)
|
|
22
|
+
// * Streamable HTTP transport => suggests server side capture (i.e. log file)
|
|
23
|
+
// * and/or log to MCP client regardless of transport
|
|
24
|
+
// server.sendLoggingMessage({
|
|
25
|
+
// level: "info",
|
|
26
|
+
// data: message,
|
|
27
|
+
// });
|
|
28
|
+
// // sends a message like:
|
|
29
|
+
// server.notification({
|
|
30
|
+
// method: "notifications/message",
|
|
31
|
+
// params: {
|
|
32
|
+
// level: "warning",
|
|
33
|
+
// logger: "mcp-server-commands",
|
|
34
|
+
// data: "ListToolsRequest2",
|
|
35
|
+
// },
|
|
36
|
+
// });
|
|
37
|
+
if (isJest) {
|
|
38
|
+
// * JEST => log to console so I can see in test runner output
|
|
39
|
+
console.log(`${message}${data ? ": " + JSON.stringify(data) : ""}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// * all transports => server side log file
|
|
43
|
+
const timestamp = new Date().toISOString();
|
|
44
|
+
const logMessage = `[${timestamp}] ${message}${data ? ": " + JSON.stringify(data) : ""}`;
|
|
45
|
+
const shareDir = process.env.HOME + "/.local/share/mcp-server-commands/";
|
|
46
|
+
const logFile = shareDir + "/commands.log";
|
|
47
|
+
fs.mkdirSync(shareDir, { recursive: true });
|
|
48
|
+
fs.appendFileSync(logFile, logMessage + "\n");
|
|
49
|
+
// TODO any hail mary if this logging fails? just use console.error then?
|
|
50
|
+
// else still can get seemingly hung tool calls
|
|
51
|
+
}
|
package/build/prompts.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { GetPromptRequestSchema, ListPromptsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { verbose_log } from "./
|
|
2
|
+
import { verbose_log } from "./logging.js";
|
|
3
3
|
import { exec } from "node:child_process";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
const execAsync = promisify(exec);
|
|
@@ -20,10 +20,8 @@ export function registerPrompts(server) {
|
|
|
20
20
|
description: "Include command output in the prompt. " +
|
|
21
21
|
"This is effectively a user tool call.",
|
|
22
22
|
arguments: [
|
|
23
|
-
{ name: "mode", },
|
|
24
23
|
{ name: "command_line", },
|
|
25
24
|
{ name: "argv", },
|
|
26
|
-
{ name: "dry_run", },
|
|
27
25
|
// ? other args?
|
|
28
26
|
],
|
|
29
27
|
},
|
|
@@ -41,8 +39,6 @@ export function registerPrompts(server) {
|
|
|
41
39
|
throw new Error("run_process not yet ported to prompts");
|
|
42
40
|
const command_line = String(request.params.arguments?.command_line);
|
|
43
41
|
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
42
|
// Is it possible/feasible to pass a path for the workdir when running the command?
|
|
47
43
|
// - currently it uses / (yikez)
|
|
48
44
|
// - IMO makes more sense to have it be based on the Zed workdir of each project
|
package/build/tools.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import os from "os";
|
|
2
2
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
-
import { verbose_log } from "./
|
|
4
|
-
import { runProcess } from "./
|
|
5
|
-
export function
|
|
3
|
+
import { verbose_log } from "./logging.js";
|
|
4
|
+
import { runProcess } from "./exec-utils.js";
|
|
5
|
+
export function registerTools(server) {
|
|
6
6
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
7
7
|
verbose_log("INFO: ListTools");
|
|
8
8
|
return {
|
|
@@ -10,6 +10,8 @@ export function reisterTools(server) {
|
|
|
10
10
|
// https://modelcontextprotocol.io/docs/learn/architecture#understanding-the-tool-execution-request // tool request/response
|
|
11
11
|
// typescript SDK docs:
|
|
12
12
|
// servers: https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/server.md
|
|
13
|
+
// TODO upgrade to newer version AND check if STDIO delimiter style has changed to include content-length "header" before responses?
|
|
14
|
+
// OR is there an opt-in for this style vs what I get with my simple nvim uv.spawn nvim client... where \n terminates/delimits each message
|
|
13
15
|
tools: [
|
|
14
16
|
{
|
|
15
17
|
// TODO RUN_PROCESS MIGRATION! provide examples in system message, that way it is very clear how to use these!
|
|
@@ -19,23 +21,15 @@ export function reisterTools(server) {
|
|
|
19
21
|
type: "object",
|
|
20
22
|
properties: {
|
|
21
23
|
// 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
24
|
command_line: {
|
|
31
25
|
type: "string",
|
|
32
|
-
description: "Shell command line
|
|
26
|
+
description: "Shell mode: a shell command line executed via the system's default shell. Supports pipes, redirects, globbing. Cannot be combined with 'argv'."
|
|
33
27
|
},
|
|
34
28
|
argv: {
|
|
35
29
|
minItems: 1, // * made up too
|
|
36
30
|
type: "array",
|
|
37
31
|
items: { type: "string" },
|
|
38
|
-
description: "Executable
|
|
32
|
+
description: "Executable mode: directly spawn a process. argv[0] is the executable, followed by arguments passed verbatim (no shell interpretation). Cannot be combined with 'command_line'."
|
|
39
33
|
},
|
|
40
34
|
cwd: {
|
|
41
35
|
// or "workdir" like before? => eval model behavior w/ each name?
|
|
@@ -49,33 +43,10 @@ export function reisterTools(server) {
|
|
|
49
43
|
timeout_ms: {
|
|
50
44
|
type: "number",
|
|
51
45
|
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
46
|
}
|
|
62
|
-
// MAYBEs:
|
|
63
|
-
// - env - obscure cases where command takes a param only via an env var?
|
|
64
47
|
},
|
|
65
|
-
required
|
|
66
|
-
//
|
|
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
|
-
// ]
|
|
48
|
+
// FYI no required arg top level and I am not gonna fret about specifiying one or the other, the tool definition is fine with that distinction in the descriptions, plus it is intuitive.
|
|
49
|
+
// and back when I had mode=shell/executable required, models would still forget to add it so why bother with a huge complexity in tool definition
|
|
79
50
|
},
|
|
80
51
|
},
|
|
81
52
|
],
|
|
@@ -85,7 +56,13 @@ export function reisterTools(server) {
|
|
|
85
56
|
verbose_log("INFO: ToolRequest", request);
|
|
86
57
|
switch (request.params.name) {
|
|
87
58
|
case "run_process": {
|
|
88
|
-
|
|
59
|
+
if (!request.params.arguments) {
|
|
60
|
+
throw new Error("Missing arguments for run_process");
|
|
61
|
+
}
|
|
62
|
+
const result = await runProcess(request.params.arguments);
|
|
63
|
+
// FYI logging this response is INVALUABLE! found a problem with my neovim MCP STDIO client!
|
|
64
|
+
verbose_log("INFO: ToolResponse", result);
|
|
65
|
+
return result;
|
|
89
66
|
}
|
|
90
67
|
default:
|
|
91
68
|
throw new Error("Unknown tool");
|
package/package.json
CHANGED
package/build/always_log.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
const isJest = typeof process !== 'undefined' && !!process.env.JEST_WORKER_ID;
|
|
43
|
-
if (isJest) {
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
if (data) {
|
|
47
|
-
console.error(message + ": " + JSON.stringify(data));
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
console.error(message);
|
|
51
|
-
}
|
|
52
|
-
}
|
package/build/run_process.js
DELETED
|
@@ -1,90 +0,0 @@
|
|
|
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
|
-
}
|