mcp-server-commands 0.5.0 → 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/README.md CHANGED
@@ -82,6 +82,42 @@ Make sure to run `npm run build`
82
82
  }
83
83
  ```
84
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
+
85
121
  ### Logging
86
122
 
87
123
  Claude Desktop app writes logs to `~/Library/Logs/Claude/mcp-server-mcp-server-commands.log`
@@ -1,3 +1,43 @@
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) {
2
42
  if (data) {
3
43
  console.error(message + ": " + JSON.stringify(data));
@@ -3,7 +3,7 @@ import { exec } from "child_process";
3
3
  * Executes a file with the given arguments, piping input to stdin.
4
4
  * @param {string} interpreter - The file to execute.
5
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.
6
+ * @returns {Promise<ExecResult>}
7
7
  */
8
8
  function execFileWithInput(interpreter, stdin, options) {
9
9
  // FYI for now, using `exec()` so the interpreter can have cmd+args AIO
@@ -16,9 +16,14 @@ function execFileWithInput(interpreter, stdin, options) {
16
16
  return new Promise((resolve, reject) => {
17
17
  const child = exec(interpreter, options, (error, stdout, stderr) => {
18
18
  if (error) {
19
- reject({ message: error.message, stdout, stderr });
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
  });
@@ -42,7 +47,11 @@ async function fishWorkaround(interpreter, stdin, options) {
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
- reject({ message: error.message, stdout, stderr });
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,22 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import os from 'os';
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 { always_log } from "./always_log.js";
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
- if (verbose) {
33
- always_log("INFO: verbose logging enabled");
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
@@ -3,13 +3,35 @@
3
3
  */
4
4
  export function messagesFor(result) {
5
5
  const messages = [];
6
- if (result.message) {
6
+ if (result.code !== undefined) {
7
7
  messages.push({
8
8
  type: "text",
9
- text: result.message,
10
- name: "ERROR",
9
+ text: `${result.code}`,
10
+ name: "EXIT_CODE",
11
11
  });
12
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
+ // }
13
35
  if (result.stdout) {
14
36
  messages.push({
15
37
  type: "text",
@@ -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
+ }
@@ -5,14 +5,15 @@ import { always_log } from "./always_log.js";
5
5
  import { messagesFor } from "./messages.js";
6
6
  const execAsync = promisify(exec);
7
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
8
12
  if (!stdin) {
9
13
  return await execAsync(command, options);
10
14
  }
11
15
  return await execFileWithInput(command, stdin, options);
12
16
  }
13
- /**
14
- * Executes a command and returns the result as CallToolResult.
15
- */
16
17
  export async function runCommand(args) {
17
18
  const command = args?.command;
18
19
  if (!command) {
@@ -34,6 +35,22 @@ export async function runCommand(args) {
34
35
  };
35
36
  }
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'
37
54
  const response = {
38
55
  isError: true,
39
56
  content: messagesFor(error),
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,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-server-commands",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "An MCP server to run arbitrary commands",
5
5
  "private": false,
6
6
  "type": "module",