mcp-agents 0.3.1
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 +90 -0
- package/package.json +36 -0
- package/server.js +339 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# mcp-agents
|
|
2
|
+
|
|
3
|
+
MCP server that wraps AI CLI tools — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [Codex CLI](https://github.com/openai/codex) — so any MCP client can call them as tools.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Node.js >= 18**
|
|
8
|
+
- At least one of the following CLIs installed and on your `$PATH`:
|
|
9
|
+
|
|
10
|
+
| CLI | Install |
|
|
11
|
+
|-----|---------|
|
|
12
|
+
| `claude` | [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code) |
|
|
13
|
+
| `gemini` | `npm install -g @anthropic-ai/gemini-cli` |
|
|
14
|
+
| `codex` | `npm install -g @openai/codex` |
|
|
15
|
+
|
|
16
|
+
Only the CLI you select with `--provider` needs to be present.
|
|
17
|
+
|
|
18
|
+
## Quick test
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Default provider (codex)
|
|
22
|
+
npx mcp-agents
|
|
23
|
+
|
|
24
|
+
# Specific provider
|
|
25
|
+
npx mcp-agents --provider claude
|
|
26
|
+
npx mcp-agents --provider gemini
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The server speaks [JSON-RPC over stdio](https://modelcontextprotocol.io/docs/concepts/transports#stdio). It prints `[mcp-agents] ready (provider: <name>)` to stderr when it's listening.
|
|
30
|
+
|
|
31
|
+
## Providers & Tools
|
|
32
|
+
|
|
33
|
+
Each `--provider` flag maps to a single exposed tool:
|
|
34
|
+
|
|
35
|
+
| Provider | Tool name | CLI command |
|
|
36
|
+
|----------|-----------|-------------|
|
|
37
|
+
| `claude` | `claude_code` | `claude -p <prompt>` |
|
|
38
|
+
| `gemini` | `gemini` | `gemini [-s] -p <prompt>` |
|
|
39
|
+
|
|
40
|
+
### `claude_code` parameters
|
|
41
|
+
|
|
42
|
+
| Parameter | Type | Required | Description |
|
|
43
|
+
|-----------|------|----------|-------------|
|
|
44
|
+
| `prompt` | `string` | yes | The prompt to send to Claude Code |
|
|
45
|
+
| `timeout_ms` | `integer` | no | Timeout in ms (default: 120 000) |
|
|
46
|
+
|
|
47
|
+
### `gemini` parameters
|
|
48
|
+
|
|
49
|
+
| Parameter | Type | Required | Description |
|
|
50
|
+
|-----------|------|----------|-------------|
|
|
51
|
+
| `prompt` | `string` | yes | The prompt to send to Gemini CLI |
|
|
52
|
+
| `sandbox` | `boolean` | no | Run in sandbox mode (`-s` flag) |
|
|
53
|
+
| `timeout_ms` | `integer` | no | Timeout in ms (default: 120 000) |
|
|
54
|
+
|
|
55
|
+
### `codex` parameters
|
|
56
|
+
|
|
57
|
+
| Parameter | Type | Required | Description |
|
|
58
|
+
|-----------|------|----------|-------------|
|
|
59
|
+
| `prompt` | `string` | yes | The prompt to send to Codex CLI |
|
|
60
|
+
| `timeout_ms` | `integer` | no | Timeout in ms (default: 120 000) |
|
|
61
|
+
|
|
62
|
+
## Integration with OpenAI Codex
|
|
63
|
+
|
|
64
|
+
Add two entries to `~/.codex/config.toml` — one per provider you want available:
|
|
65
|
+
|
|
66
|
+
```toml
|
|
67
|
+
[mcp_servers.claude-code]
|
|
68
|
+
command = "npx"
|
|
69
|
+
args = ["-y", "mcp-agents", "--provider", "claude"]
|
|
70
|
+
|
|
71
|
+
[mcp_servers.gemini]
|
|
72
|
+
command = "npx"
|
|
73
|
+
args = ["-y", "mcp-agents", "--provider", "gemini"]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Then in a Codex session you can call the `claude_code` or `gemini` tools, which shell out to the respective CLIs.
|
|
77
|
+
|
|
78
|
+
## How it works
|
|
79
|
+
|
|
80
|
+
1. An MCP client connects over stdio
|
|
81
|
+
2. The server reads `--provider <name>` from its argv (defaults to `codex`)
|
|
82
|
+
3. It registers a single tool matching that provider's CLI
|
|
83
|
+
4. Client calls `tools/call` with the tool name and a `prompt`
|
|
84
|
+
5. The server runs the CLI as a child process and returns stdout (or stderr) as the tool result
|
|
85
|
+
|
|
86
|
+
The server includes a keepalive timer to prevent Node.js from exiting prematurely when stdin reaches EOF before the async subprocess registers an active handle.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-agents",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "MCP server that wraps AI CLI tools (Claude Code, Gemini CLI, Codex CLI) for use by any MCP client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-agents": "./server.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"server.js",
|
|
11
|
+
"package.json"
|
|
12
|
+
],
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"claude",
|
|
16
|
+
"gemini",
|
|
17
|
+
"codex",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"google",
|
|
20
|
+
"openai",
|
|
21
|
+
"ai",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/thomaswitt/mcp-agents"
|
|
27
|
+
},
|
|
28
|
+
"author": "Thomas Witt",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import {
|
|
12
|
+
CallToolRequestSchema,
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const VERSION = JSON.parse(
|
|
18
|
+
readFileSync(join(__dirname, "package.json"), "utf8"),
|
|
19
|
+
).version;
|
|
20
|
+
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
22
|
+
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// CLI Backend Definitions
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const CLI_BACKENDS = {
|
|
29
|
+
claude: {
|
|
30
|
+
command: "claude",
|
|
31
|
+
toolName: "claude_code",
|
|
32
|
+
description: "Run Claude Code CLI (claude -p) with a prompt.",
|
|
33
|
+
buildArgs: (prompt) => ["-p", prompt],
|
|
34
|
+
extraProperties: {},
|
|
35
|
+
},
|
|
36
|
+
gemini: {
|
|
37
|
+
command: "gemini",
|
|
38
|
+
toolName: "gemini",
|
|
39
|
+
description: "Run Gemini CLI (gemini -p) with a prompt.",
|
|
40
|
+
buildArgs: (prompt, opts) => {
|
|
41
|
+
const args = [];
|
|
42
|
+
if (opts.sandbox !== false) args.push("-s");
|
|
43
|
+
args.push("-p", prompt);
|
|
44
|
+
return args;
|
|
45
|
+
},
|
|
46
|
+
extraProperties: {
|
|
47
|
+
sandbox: {
|
|
48
|
+
type: "boolean",
|
|
49
|
+
default: true,
|
|
50
|
+
description: "Run in sandbox mode (-s flag). Defaults to true.",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
codex: {
|
|
55
|
+
command: "codex",
|
|
56
|
+
toolName: "codex",
|
|
57
|
+
description: "Run Codex CLI (codex exec) with a prompt.",
|
|
58
|
+
buildArgs: (prompt) => ["exec", prompt],
|
|
59
|
+
extraProperties: {},
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Helpers
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Never write debug logs to stdout (it breaks MCP stdio transport).
|
|
69
|
+
* Use stderr only.
|
|
70
|
+
*/
|
|
71
|
+
function logErr(message) {
|
|
72
|
+
process.stderr.write(`${message}\n`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Defensive string conversion for tool args.
|
|
77
|
+
* @param {unknown} value
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function toStringArg(value) {
|
|
81
|
+
if (typeof value === "string") return value;
|
|
82
|
+
if (value == null) return "";
|
|
83
|
+
return String(value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Print usage information to stdout.
|
|
88
|
+
*/
|
|
89
|
+
function printHelp() {
|
|
90
|
+
const providers = Object.keys(CLI_BACKENDS).join(", ");
|
|
91
|
+
console.log(`mcp-agents v${VERSION}
|
|
92
|
+
|
|
93
|
+
Usage: mcp-agents [options]
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--provider <name> CLI backend to use (${providers}) [default: codex]
|
|
97
|
+
--help, -h Show this help message
|
|
98
|
+
--version, -v Show version number`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse CLI flags from process.argv.
|
|
103
|
+
* Handles --help, --version, --provider, and unknown flags.
|
|
104
|
+
* @returns {string | null} Provider name, or null if the process should exit.
|
|
105
|
+
*/
|
|
106
|
+
function parseArgs() {
|
|
107
|
+
const args = process.argv.slice(2);
|
|
108
|
+
let provider = "codex";
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < args.length; i++) {
|
|
111
|
+
switch (args[i]) {
|
|
112
|
+
case "--help":
|
|
113
|
+
case "-h":
|
|
114
|
+
printHelp();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
break;
|
|
117
|
+
case "--version":
|
|
118
|
+
case "-v":
|
|
119
|
+
console.log(`mcp-agents v${VERSION}`);
|
|
120
|
+
process.exit(0);
|
|
121
|
+
break;
|
|
122
|
+
case "--provider":
|
|
123
|
+
if (i + 1 >= args.length) {
|
|
124
|
+
process.stderr.write("error: --provider requires a value\n");
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
provider = args[++i];
|
|
128
|
+
break;
|
|
129
|
+
default:
|
|
130
|
+
process.stderr.write(`error: unknown option: ${args[i]}\n`);
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return provider;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Run a CLI command and return stdout (or stderr if stdout is empty).
|
|
140
|
+
* @param {string} command
|
|
141
|
+
* @param {string[]} args
|
|
142
|
+
* @param {{ timeoutMs?: number }} [opts]
|
|
143
|
+
* @returns {Promise<string>}
|
|
144
|
+
*/
|
|
145
|
+
function runCli(command, args, opts = {}) {
|
|
146
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const child = execFile(
|
|
150
|
+
command,
|
|
151
|
+
args,
|
|
152
|
+
{
|
|
153
|
+
timeout: timeoutMs,
|
|
154
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
155
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
156
|
+
},
|
|
157
|
+
(error, stdout, stderr) => {
|
|
158
|
+
if (error) {
|
|
159
|
+
const details = [
|
|
160
|
+
`${command} failed: ${error.message}`,
|
|
161
|
+
stderr ? `stderr:\n${stderr}` : null,
|
|
162
|
+
]
|
|
163
|
+
.filter(Boolean)
|
|
164
|
+
.join("\n");
|
|
165
|
+
|
|
166
|
+
reject(new Error(details));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const out = (stdout || stderr || "").trimEnd();
|
|
171
|
+
resolve(out);
|
|
172
|
+
},
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Close stdin immediately so the child process doesn't wait for piped input.
|
|
176
|
+
// execFile creates a pipe for stdin by default; leaving it open causes
|
|
177
|
+
// the child to hang indefinitely waiting for EOF.
|
|
178
|
+
child.stdin?.end();
|
|
179
|
+
|
|
180
|
+
child.on("error", (err) => {
|
|
181
|
+
reject(new Error(`Failed to start ${command}: ${err.message}`));
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Main
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
async function main() {
|
|
191
|
+
const providerName = parseArgs();
|
|
192
|
+
const backend = CLI_BACKENDS[providerName];
|
|
193
|
+
|
|
194
|
+
if (!backend) {
|
|
195
|
+
logErr(`[mcp-agents] Unknown provider: ${providerName}`);
|
|
196
|
+
logErr(
|
|
197
|
+
`[mcp-agents] Available: ${Object.keys(CLI_BACKENDS).join(", ")}`,
|
|
198
|
+
);
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const server = new Server(
|
|
204
|
+
{ name: "mcp-agents", version: VERSION },
|
|
205
|
+
{ capabilities: { tools: {} } },
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const properties = {
|
|
209
|
+
prompt: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: `Prompt for ${backend.command}`,
|
|
212
|
+
},
|
|
213
|
+
timeout_ms: {
|
|
214
|
+
type: "integer",
|
|
215
|
+
minimum: 1,
|
|
216
|
+
description: `Optional timeout override (default ${DEFAULT_TIMEOUT_MS})`,
|
|
217
|
+
},
|
|
218
|
+
...backend.extraProperties,
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
222
|
+
tools: [
|
|
223
|
+
{
|
|
224
|
+
name: "ping",
|
|
225
|
+
description:
|
|
226
|
+
"Connectivity test. Returns 'pong' instantly without calling the CLI.",
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: "object",
|
|
229
|
+
additionalProperties: false,
|
|
230
|
+
properties: {},
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: backend.toolName,
|
|
235
|
+
description: backend.description,
|
|
236
|
+
inputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
additionalProperties: false,
|
|
239
|
+
properties,
|
|
240
|
+
required: ["prompt"],
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
}));
|
|
245
|
+
|
|
246
|
+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
247
|
+
if (params.name === "ping") {
|
|
248
|
+
return { content: [{ type: "text", text: "pong" }] };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (params.name !== backend.toolName) {
|
|
252
|
+
return {
|
|
253
|
+
content: [
|
|
254
|
+
{
|
|
255
|
+
type: "text",
|
|
256
|
+
text: `Unknown tool: ${params.name}`,
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
isError: true,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const prompt = toStringArg(params.arguments?.prompt);
|
|
264
|
+
const timeoutMsRaw = params.arguments?.timeout_ms;
|
|
265
|
+
const timeoutMs = Number.isInteger(timeoutMsRaw)
|
|
266
|
+
? timeoutMsRaw
|
|
267
|
+
: DEFAULT_TIMEOUT_MS;
|
|
268
|
+
|
|
269
|
+
if (!prompt.trim()) {
|
|
270
|
+
return {
|
|
271
|
+
content: [
|
|
272
|
+
{
|
|
273
|
+
type: "text",
|
|
274
|
+
text: "Missing required argument: prompt",
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
isError: true,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const extraOpts = {};
|
|
282
|
+
for (const key of Object.keys(backend.extraProperties)) {
|
|
283
|
+
if (params.arguments?.[key] != null) {
|
|
284
|
+
extraOpts[key] = params.arguments[key];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const cliArgs = backend.buildArgs(prompt, extraOpts);
|
|
289
|
+
|
|
290
|
+
logErr(`[mcp-agents] tools/call: running ${backend.command} …`);
|
|
291
|
+
try {
|
|
292
|
+
const output = await runCli(backend.command, cliArgs, { timeoutMs });
|
|
293
|
+
logErr("[mcp-agents] tools/call: done");
|
|
294
|
+
return {
|
|
295
|
+
content: [{ type: "text", text: output || "" }],
|
|
296
|
+
};
|
|
297
|
+
} catch (err) {
|
|
298
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
299
|
+
logErr(msg);
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: msg }],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const transport = new StdioServerTransport();
|
|
308
|
+
await server.connect(transport);
|
|
309
|
+
|
|
310
|
+
// Prevent premature exit when stdin EOF arrives before async
|
|
311
|
+
// request handlers (tools/call -> execFile) register active handles.
|
|
312
|
+
// The SDK transport doesn't listen for stdin 'end', so the event
|
|
313
|
+
// loop loses its only handle when the pipe closes.
|
|
314
|
+
const keepAlive = setInterval(() => {}, 60_000);
|
|
315
|
+
const origOnClose = transport.onclose;
|
|
316
|
+
transport.onclose = () => {
|
|
317
|
+
clearInterval(keepAlive);
|
|
318
|
+
origOnClose?.();
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
logErr(`[mcp-agents] ready (provider: ${providerName})`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
process.on("unhandledRejection", (reason) => {
|
|
325
|
+
logErr(
|
|
326
|
+
`UnhandledRejection: ${reason instanceof Error ? reason.stack : reason}`,
|
|
327
|
+
);
|
|
328
|
+
process.exitCode = 1;
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
process.on("uncaughtException", (err) => {
|
|
332
|
+
logErr(`UncaughtException: ${err.stack || err.message}`);
|
|
333
|
+
process.exitCode = 1;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
main().catch((err) => {
|
|
337
|
+
logErr(err.stack || err.message);
|
|
338
|
+
process.exitCode = 1;
|
|
339
|
+
});
|