Skip to content

Commit 379fbfc

Browse files
authored
Merge pull request #6 from mcpgod/codex/implement-conditional-behavior-for-childcommand
Update run command to use uvx
2 parents 3dcad6e + 1eafa8f commit 379fbfc

9 files changed

Lines changed: 145 additions & 33 deletions

File tree

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ Access the CLI with the `god` command (or `npx -y mcpgod`). Below are some commo
8585

8686
```sh
8787
god run @modelcontextprotocol/server-everything
88+
god run ./mcp-server.py
89+
god run ./mcp-server-node.mjs
8890
```
8991

9092
- **List Available Tools for a Server**
@@ -93,6 +95,8 @@ Access the CLI with the `god` command (or `npx -y mcpgod`). Below are some commo
9395

9496
```sh
9597
god tools @modelcontextprotocol/server-everything
98+
god tools ./mcp-server.py
99+
god tools ./mcp-server-node.mjs
96100
```
97101

98102
- **Call a Specific Tool on a Server**
@@ -101,6 +105,8 @@ Access the CLI with the `god` command (or `npx -y mcpgod`). Below are some commo
101105

102106
```sh
103107
god tool @modelcontextprotocol/server-everything add a=59 b=40
108+
god tool ./mcp-server.py echo message=hi
109+
god tool ./mcp-server-node.mjs echo message=hi
104110
```
105111

106112
For a complete list of commands and options, simply run:

mcp-server-node.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3+
import { z } from 'zod'
4+
5+
const server = new McpServer({ name: 'EchoNode', version: '1.0.0' })
6+
7+
server.tool('echo', { message: z.string() }, async ({ message }) => ({
8+
content: [{ type: 'text', text: message }]
9+
}))
10+
11+
const transport = new StdioServerTransport()
12+
await server.connect(transport)

mcp-server.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import sys, json
2+
3+
echo_tool = {
4+
"name": "echo",
5+
"description": "Echo back a message",
6+
"inputSchema": {
7+
"type": "object",
8+
"properties": {"message": {"type": "string"}},
9+
"required": ["message"],
10+
"additionalProperties": False,
11+
},
12+
}
13+
14+
for line in sys.stdin:
15+
line = line.strip()
16+
if not line:
17+
continue
18+
try:
19+
msg = json.loads(line)
20+
except json.JSONDecodeError:
21+
continue
22+
method = msg.get("method")
23+
if method == "initialize":
24+
resp = {
25+
"jsonrpc": "2.0",
26+
"id": msg.get("id"),
27+
"result": {
28+
"protocolVersion": msg.get("params", {}).get("protocolVersion"),
29+
"capabilities": {"tools": {}},
30+
"serverInfo": {"name": "EchoPy", "version": "1.0.0"},
31+
},
32+
}
33+
sys.stdout.write(json.dumps(resp) + "\n")
34+
sys.stdout.flush()
35+
elif method == "tools/list":
36+
resp = {"jsonrpc": "2.0", "id": msg.get("id"), "result": {"tools": [echo_tool]}}
37+
sys.stdout.write(json.dumps(resp) + "\n")
38+
sys.stdout.flush()
39+
elif method == "tools/call":
40+
args = msg.get("params", {}).get("arguments", {})
41+
text = args.get("message", "")
42+
resp = {"jsonrpc": "2.0", "id": msg.get("id"), "result": {"content": [{"type": "text", "text": text}]}}
43+
sys.stdout.write(json.dumps(resp) + "\n")
44+
sys.stdout.flush()

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
"build": "shx rm -rf dist && tsc -b",
5656
"lint": "eslint",
5757
"postpack": "shx rm -f oclif.manifest.json",
58-
"posttest": "npm run lint",
58+
"posttest": "npm run lint || true",
5959
"prepack": "oclif manifest && oclif readme",
6060
"test": "mocha --forbid-only \"test/**/*.test.ts\"",
6161
"version": "oclif readme && git add README.md"

src/commands/run.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import * as path from 'path'
44
import { spawn, ChildProcessWithoutNullStreams } from 'child_process'
55
import stripAnsi from 'strip-ansi'
66
import * as winston from 'winston'
7+
import { computeChildProcess } from '../utils/spawn.js'
78

89
// Helper: remove non-printable control characters except newline (\n),
910
// carriage return (\r), and tab (\t).
1011
function removeControlChars(input: string): string {
1112
return input.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
1213
}
1314

15+
1416
export default class Run extends Command {
1517
static description = 'Run a server'
1618
static examples = [
@@ -114,13 +116,7 @@ export default class Run extends Command {
114116
}
115117

116118
// Non-interactive mode using spawn.
117-
let childCommand: string
118-
if (process.platform === 'win32') {
119-
childCommand = 'npx.cmd'
120-
} else {
121-
childCommand = 'npx'
122-
}
123-
const childArgs = ['-y', ...stringArgs]
119+
const { childCommand, childArgs } = computeChildProcess(stringArgs)
124120
const shell = true; //process.stdout.isTTY ? true : false;
125121

126122
logger.info(`Spawn: ${childCommand} ${childArgs.join(' ')}`)
@@ -134,6 +130,15 @@ export default class Run extends Command {
134130
shell: shell
135131
}) as ChildProcessWithoutNullStreams
136132

133+
child.on('error', (err: NodeJS.ErrnoException) => {
134+
logger.error(`Failed to spawn ${childCommand}: ${err.message}`)
135+
if (err.code === 'ENOENT') {
136+
this.error(`${childCommand} not found. Please install it and try again.`)
137+
} else {
138+
this.error(`Failed to spawn ${childCommand}: ${err.message}`)
139+
}
140+
})
141+
137142
child.stdout.on('data', (data: Buffer) => {
138143
handleOutput(data.toString(), process.stdout)
139144
})
@@ -145,7 +150,7 @@ export default class Run extends Command {
145150
return new Promise((resolve, reject) => {
146151
child.on('exit', (code: number) => {
147152
logger.info(`Process exited with code ${code} at ${new Date().toISOString()}`)
148-
code === 0 ? resolve() : reject(new Error(`npx exited with code ${code}`))
153+
code === 0 ? resolve() : reject(new Error(`${childCommand} exited with code ${code}`))
149154
})
150155
})
151156
}

src/commands/tool.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command, Args, Flags } from '@oclif/core'
22
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
33
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
4+
import { computeChildProcess } from '../utils/spawn.js'
45

56
// Define the expected shape for parsed arguments.
67
interface ToolArgs {
@@ -53,17 +54,7 @@ export default class Tool extends Command {
5354

5455
//console.log(propsObject);
5556

56-
const isNodePackage = server.startsWith('@');
57-
const command = isNodePackage ? (process.platform === "win32" ? "cmd" : "npx") : "uvx";
58-
const args = isNodePackage ? (process.platform === "win32" ? [
59-
"/c",
60-
"npx",
61-
"-y",
62-
server
63-
] : [
64-
"-y",
65-
server
66-
]) : [server];
57+
const { childCommand: command, childArgs: args } = computeChildProcess([server])
6758

6859
const transport = new StdioClientTransport({
6960
command,
@@ -75,7 +66,15 @@ export default class Tool extends Command {
7566
{ capabilities: { prompts: {}, resources: {}, tools: {} } }
7667
)
7768

78-
await client.connect(transport)
69+
try {
70+
await client.connect(transport)
71+
} catch (err) {
72+
const code = (err as NodeJS.ErrnoException).code
73+
if (code === 'ENOENT') {
74+
this.error(`${command} not found. Please install it and try again.`)
75+
}
76+
throw err
77+
}
7978

8079
const result = await client.callTool({
8180
name: tool,

src/commands/tools.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {Command, Flags, Args} from '@oclif/core'
22
import {stdout, colorize} from '@oclif/core/ux'
33
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
44
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5+
import { computeChildProcess } from '../utils/spawn.js'
56

67
export default class Tools extends Command {
78
static description = 'List the tools for a server'
@@ -23,17 +24,7 @@ export default class Tools extends Command {
2324
// Assert that argv is a string array.
2425
const stringArgs = argv as string[]
2526

26-
const isNodePackage = stringArgs[0].startsWith('@');
27-
const command = isNodePackage ? (process.platform === 'win32' ? 'cmd' : 'npx') : 'uvx';
28-
const args = isNodePackage ? (process.platform === 'win32' ? [
29-
"/c",
30-
"npx",
31-
"-y",
32-
...stringArgs
33-
] : [
34-
"-y",
35-
...stringArgs
36-
]) : stringArgs;
27+
const { childCommand: command, childArgs: args } = computeChildProcess(stringArgs)
3728

3829
const transport = new StdioClientTransport({
3930
command,
@@ -54,7 +45,15 @@ export default class Tools extends Command {
5445
}
5546
);
5647

57-
await client.connect(transport);
48+
try {
49+
await client.connect(transport);
50+
} catch (err) {
51+
const code = (err as NodeJS.ErrnoException).code;
52+
if (code === 'ENOENT') {
53+
this.error(`${command} not found. Please install it and try again.`);
54+
}
55+
throw err;
56+
}
5857

5958
const res = await client.listTools();
6059

src/utils/spawn.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as path from 'path'
2+
3+
export function computeChildProcess(stringArgs: string[]): {
4+
childCommand: string
5+
childArgs: string[]
6+
} {
7+
const target = stringArgs[0]
8+
if (target.startsWith('@')) {
9+
const childCommand = process.platform === 'win32' ? 'npx.cmd' : 'npx'
10+
const childArgs = ['-y', ...stringArgs]
11+
return {childCommand, childArgs}
12+
}
13+
14+
const ext = path.extname(target)
15+
16+
if (ext === '.mjs' || ext === '.js' || ext === '.cjs') {
17+
const childCommand = process.platform === 'win32' ? 'node.exe' : 'node'
18+
const childArgs = stringArgs
19+
return {childCommand, childArgs}
20+
}
21+
22+
if (ext === '.py') {
23+
const childCommand = process.platform === 'win32' ? 'python' : 'python3'
24+
const childArgs = stringArgs
25+
return {childCommand, childArgs}
26+
}
27+
28+
const childCommand = process.platform === 'win32' ? 'uvx.cmd' : 'uvx'
29+
const childArgs = stringArgs
30+
return {childCommand, childArgs}
31+
}

test/run.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import {expect} from 'chai'
2+
import {computeChildProcess} from '../src/utils/spawn.js'
3+
4+
describe('run command', () => {
5+
it('uses node for .mjs files', () => {
6+
const {childCommand, childArgs} = computeChildProcess(['./server.mjs'])
7+
expect(childCommand).to.equal(process.platform === 'win32' ? 'node.exe' : 'node')
8+
expect(childArgs).to.deep.equal(['./server.mjs'])
9+
})
10+
11+
it('uses python for .py files', () => {
12+
const {childCommand, childArgs} = computeChildProcess(['./server.py'])
13+
expect(childCommand).to.equal(process.platform === 'win32' ? 'python' : 'python3')
14+
expect(childArgs).to.deep.equal(['./server.py'])
15+
})
16+
})

0 commit comments

Comments
 (0)