ai-cli-mcp 2.11.0 → 2.13.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/.github/workflows/publish.yml +25 -0
- package/CHANGELOG.md +23 -0
- package/README.ja.md +112 -8
- package/README.md +112 -9
- package/dist/__tests__/app-cli.test.js +293 -0
- package/dist/__tests__/cli-bin-smoke.test.js +58 -0
- package/dist/__tests__/cli-builder.test.js +37 -0
- package/dist/__tests__/cli-process-service.test.js +279 -0
- package/dist/__tests__/cli-utils.test.js +140 -0
- package/dist/__tests__/error-cases.test.js +2 -1
- package/dist/__tests__/mcp-contract.test.js +343 -0
- package/dist/__tests__/parsers.test.js +37 -1
- package/dist/__tests__/process-management.test.js +15 -8
- package/dist/__tests__/server.test.js +29 -3
- package/dist/__tests__/wait.test.js +31 -0
- package/dist/app/cli.js +304 -0
- package/dist/app/mcp.js +366 -0
- package/dist/bin/ai-cli-mcp.js +6 -0
- package/dist/bin/ai-cli.js +10 -0
- package/dist/cli-builder.js +15 -6
- package/dist/cli-parse.js +8 -5
- package/dist/cli-process-service.js +332 -0
- package/dist/cli-utils.js +159 -88
- package/dist/cli.js +4 -3
- package/dist/model-catalog.js +53 -0
- package/dist/parsers.js +55 -0
- package/dist/process-service.js +201 -0
- package/dist/server.js +4 -578
- package/docs/cli-architecture.md +275 -0
- package/package.json +4 -3
- package/server.json +1 -1
- package/src/__tests__/app-cli.test.ts +370 -0
- package/src/__tests__/cli-bin-smoke.test.ts +75 -0
- package/src/__tests__/cli-builder.test.ts +47 -0
- package/src/__tests__/cli-process-service.test.ts +334 -0
- package/src/__tests__/cli-utils.test.ts +166 -0
- package/src/__tests__/error-cases.test.ts +3 -4
- package/src/__tests__/mcp-contract.test.ts +422 -0
- package/src/__tests__/parsers.test.ts +44 -1
- package/src/__tests__/process-management.test.ts +15 -9
- package/src/__tests__/server.test.ts +27 -6
- package/src/__tests__/wait.test.ts +38 -0
- package/src/app/cli.ts +373 -0
- package/src/app/mcp.ts +402 -0
- package/src/bin/ai-cli-mcp.ts +7 -0
- package/src/bin/ai-cli.ts +11 -0
- package/src/cli-builder.ts +19 -10
- package/src/cli-parse.ts +8 -5
- package/src/cli-process-service.ts +418 -0
- package/src/cli-utils.ts +205 -99
- package/src/cli.ts +4 -3
- package/src/model-catalog.ts +64 -0
- package/src/parsers.ts +61 -0
- package/src/process-service.ts +263 -0
- package/src/server.ts +4 -668
package/src/server.ts
CHANGED
|
@@ -1,674 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
-
import {
|
|
5
|
-
CallToolRequestSchema,
|
|
6
|
-
ErrorCode,
|
|
7
|
-
ListToolsRequestSchema,
|
|
8
|
-
McpError,
|
|
9
|
-
type ServerResult,
|
|
10
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
11
|
-
import { spawn, ChildProcess } from 'node:child_process';
|
|
12
|
-
import { parseCodexOutput, parseClaudeOutput, parseGeminiOutput } from './parsers.js';
|
|
13
|
-
import { buildCliCommand } from './cli-builder.js';
|
|
14
|
-
import { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
15
|
-
|
|
16
|
-
// Re-export for backward compatibility
|
|
17
|
-
export { debugLog, findClaudeCli, findCodexCli, findGeminiCli } from './cli-utils.js';
|
|
2
|
+
export { debugLog, findClaudeCli, findCodexCli, findForgeCli, findGeminiCli } from './cli-utils.js';
|
|
18
3
|
export { resolveModelAlias } from './cli-builder.js';
|
|
4
|
+
export { ClaudeCodeServer, runMcpServer, spawnAsync } from './app/mcp.js';
|
|
19
5
|
|
|
20
|
-
|
|
21
|
-
const SERVER_VERSION = "2.2.0";
|
|
22
|
-
|
|
23
|
-
// Track if this is the first tool use for version printing
|
|
24
|
-
let isFirstToolUse = true;
|
|
25
|
-
|
|
26
|
-
// Capture server startup time when the module loads
|
|
27
|
-
const serverStartupTime = new Date().toISOString();
|
|
28
|
-
|
|
29
|
-
// Process tracking
|
|
30
|
-
interface ClaudeProcess {
|
|
31
|
-
pid: number;
|
|
32
|
-
process: ChildProcess;
|
|
33
|
-
prompt: string;
|
|
34
|
-
workFolder: string;
|
|
35
|
-
model?: string;
|
|
36
|
-
toolType: 'claude' | 'codex' | 'gemini'; // Identify which CLI tool
|
|
37
|
-
startTime: string;
|
|
38
|
-
stdout: string;
|
|
39
|
-
stderr: string;
|
|
40
|
-
status: 'running' | 'completed' | 'failed';
|
|
41
|
-
exitCode?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Type definition for list_processes return value
|
|
45
|
-
interface ProcessListItem {
|
|
46
|
-
pid: number; // プロセスID
|
|
47
|
-
agent: 'claude' | 'codex' | 'gemini'; // エージェントタイプ
|
|
48
|
-
status: 'running' | 'completed' | 'failed'; // プロセスの状態
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Global process manager
|
|
52
|
-
const processManager = new Map<number, ClaudeProcess>();
|
|
53
|
-
|
|
54
|
-
// Ensure spawnAsync is defined correctly *before* the class
|
|
55
|
-
export async function spawnAsync(command: string, args: string[], options?: { timeout?: number, cwd?: string }): Promise<{ stdout: string; stderr: string }> {
|
|
56
|
-
return new Promise((resolve, reject) => {
|
|
57
|
-
debugLog(`[Spawn] Running command: ${command} ${args.join(' ')}`);
|
|
58
|
-
const process = spawn(command, args, {
|
|
59
|
-
shell: false, // Reverted to false
|
|
60
|
-
timeout: options?.timeout,
|
|
61
|
-
cwd: options?.cwd,
|
|
62
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
let stdout = '';
|
|
66
|
-
let stderr = '';
|
|
67
|
-
|
|
68
|
-
process.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
69
|
-
process.stderr.on('data', (data) => {
|
|
70
|
-
stderr += data.toString();
|
|
71
|
-
debugLog(`[Spawn Stderr Chunk] ${data.toString()}`);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
process.on('error', (error: NodeJS.ErrnoException) => {
|
|
75
|
-
debugLog(`[Spawn Error Event] Full error object:`, error);
|
|
76
|
-
let errorMessage = `Spawn error: ${error.message}`;
|
|
77
|
-
if (error.path) {
|
|
78
|
-
errorMessage += ` | Path: ${error.path}`;
|
|
79
|
-
}
|
|
80
|
-
if (error.syscall) {
|
|
81
|
-
errorMessage += ` | Syscall: ${error.syscall}`;
|
|
82
|
-
}
|
|
83
|
-
errorMessage += `\nStderr: ${stderr.trim()}`;
|
|
84
|
-
reject(new Error(errorMessage));
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
process.on('close', (code) => {
|
|
88
|
-
debugLog(`[Spawn Close] Exit code: ${code}`);
|
|
89
|
-
debugLog(`[Spawn Stderr Full] ${stderr.trim()}`);
|
|
90
|
-
debugLog(`[Spawn Stdout Full] ${stdout.trim()}`);
|
|
91
|
-
if (code === 0) {
|
|
92
|
-
resolve({ stdout, stderr });
|
|
93
|
-
} else {
|
|
94
|
-
reject(new Error(`Command failed with exit code ${code}\nStderr: ${stderr.trim()}\nStdout: ${stdout.trim()}`));
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* MCP Server for Claude Code
|
|
102
|
-
* Provides a simple MCP tool to run Claude CLI in one-shot mode
|
|
103
|
-
*/
|
|
104
|
-
export class ClaudeCodeServer {
|
|
105
|
-
private server: Server;
|
|
106
|
-
private claudeCliPath: string;
|
|
107
|
-
private codexCliPath: string;
|
|
108
|
-
private geminiCliPath: string;
|
|
109
|
-
private sigintHandler?: () => Promise<void>;
|
|
110
|
-
private packageVersion: string;
|
|
111
|
-
|
|
112
|
-
constructor() {
|
|
113
|
-
// Use the simplified findClaudeCli function
|
|
114
|
-
this.claudeCliPath = findClaudeCli(); // Removed debugMode argument
|
|
115
|
-
this.codexCliPath = findCodexCli();
|
|
116
|
-
this.geminiCliPath = findGeminiCli();
|
|
117
|
-
console.error(`[Setup] Using Claude CLI command/path: ${this.claudeCliPath}`);
|
|
118
|
-
console.error(`[Setup] Using Codex CLI command/path: ${this.codexCliPath}`);
|
|
119
|
-
console.error(`[Setup] Using Gemini CLI command/path: ${this.geminiCliPath}`);
|
|
120
|
-
this.packageVersion = SERVER_VERSION;
|
|
121
|
-
|
|
122
|
-
this.server = new Server(
|
|
123
|
-
{
|
|
124
|
-
name: 'ai_cli_mcp',
|
|
125
|
-
version: SERVER_VERSION,
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
capabilities: {
|
|
129
|
-
tools: {},
|
|
130
|
-
},
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
|
|
134
|
-
this.setupToolHandlers();
|
|
135
|
-
|
|
136
|
-
this.server.onerror = (error) => console.error('[Error]', error);
|
|
137
|
-
this.sigintHandler = async () => {
|
|
138
|
-
await this.server.close();
|
|
139
|
-
process.exit(0);
|
|
140
|
-
};
|
|
141
|
-
process.on('SIGINT', this.sigintHandler);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Set up the MCP tool handlers
|
|
146
|
-
*/
|
|
147
|
-
private setupToolHandlers(): void {
|
|
148
|
-
// Define available tools
|
|
149
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
150
|
-
tools: [
|
|
151
|
-
{
|
|
152
|
-
name: 'run',
|
|
153
|
-
description: `AI Agent Runner: Starts a Claude, Codex, or Gemini CLI process in the background and returns a PID immediately. Use list_processes and get_result to monitor progress.
|
|
154
|
-
|
|
155
|
-
• File ops: Create, read, (fuzzy) edit, move, copy, delete, list files, analyze/ocr images, file content analysis
|
|
156
|
-
• Code: Generate / analyse / refactor / fix
|
|
157
|
-
• Git: Stage ▸ commit ▸ push ▸ tag (any workflow)
|
|
158
|
-
• Terminal: Run any CLI cmd or open URLs
|
|
159
|
-
• Web search + summarise content on-the-fly
|
|
160
|
-
• Multi-step workflows & GitHub integration
|
|
161
|
-
|
|
162
|
-
**IMPORTANT**: This tool now returns immediately with a PID. Use other tools to check status and get results.
|
|
163
|
-
|
|
164
|
-
**Supported models**:
|
|
165
|
-
"claude-ultra", "codex-ultra", "gemini-ultra", "sonnet", "sonnet[1m]", "opus", "opusplan", "haiku", "gpt-5.4", "gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex-mini", "gpt-5.1-codex-max", "gpt-5.2", "gpt-5.1", "gpt-5.1-codex", "gpt-5-codex", "gpt-5-codex-mini", "gpt-5", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-3-flash-preview"
|
|
166
|
-
|
|
167
|
-
**Prompt input**: You must provide EITHER prompt (string) OR prompt_file (file path), but not both.
|
|
168
|
-
|
|
169
|
-
**Prompt tips**
|
|
170
|
-
1. Be concise, explicit & step-by-step for complex tasks.
|
|
171
|
-
2. Check process status with list_processes
|
|
172
|
-
3. Get results with get_result using the returned PID
|
|
173
|
-
4. Kill long-running processes with kill_process if needed
|
|
174
|
-
|
|
175
|
-
`,
|
|
176
|
-
inputSchema: {
|
|
177
|
-
type: 'object',
|
|
178
|
-
properties: {
|
|
179
|
-
prompt: {
|
|
180
|
-
type: 'string',
|
|
181
|
-
description: 'The detailed natural language prompt for the agent to execute. Either this or prompt_file is required.',
|
|
182
|
-
},
|
|
183
|
-
prompt_file: {
|
|
184
|
-
type: 'string',
|
|
185
|
-
description: 'Path to a file containing the prompt. Either this or prompt is required. Must be an absolute path or relative to workFolder.',
|
|
186
|
-
},
|
|
187
|
-
workFolder: {
|
|
188
|
-
type: 'string',
|
|
189
|
-
description: 'The working directory for the agent execution. Must be an absolute path.',
|
|
190
|
-
},
|
|
191
|
-
model: {
|
|
192
|
-
type: 'string',
|
|
193
|
-
description: 'The model to use. Aliases: "claude-ultra" (auto high effort), "codex-ultra" (auto xhigh reasoning), "gemini-ultra". Standard: "sonnet", "sonnet[1m]", "opus", "opusplan", "haiku", "gpt-5.4", "gpt-5.3-codex", "gpt-5.2-codex", "gpt-5.1-codex-mini", "gpt-5.1", "gemini-2.5-pro", "gemini-3.1-pro-preview", "gemini-3-pro-preview", "gemini-3-flash-preview", etc.',
|
|
194
|
-
},
|
|
195
|
-
reasoning_effort: {
|
|
196
|
-
type: 'string',
|
|
197
|
-
description: 'Reasoning control for Claude and Codex. Claude uses --effort with "low", "medium", "high". Codex uses model_reasoning_effort with "low", "medium", "high", "xhigh".',
|
|
198
|
-
},
|
|
199
|
-
session_id: {
|
|
200
|
-
type: 'string',
|
|
201
|
-
description: 'Optional session ID to resume a previous session. Supported for: haiku, sonnet, opus, gemini-2.5-pro, gemini-2.5-flash, gemini-3.1-pro-preview, gemini-3-pro-preview, gemini-3-flash-preview.',
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
required: ['workFolder'],
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
{
|
|
208
|
-
name: 'list_processes',
|
|
209
|
-
description: 'List all running and completed AI agent processes. Returns a simple list with PID, agent type, and status for each process.',
|
|
210
|
-
inputSchema: {
|
|
211
|
-
type: 'object',
|
|
212
|
-
properties: {},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
{
|
|
216
|
-
name: 'get_result',
|
|
217
|
-
description: 'Get the current output and status of an AI agent process by PID. Returns the output from the agent including session_id (if applicable), along with process metadata.',
|
|
218
|
-
inputSchema: {
|
|
219
|
-
type: 'object',
|
|
220
|
-
properties: {
|
|
221
|
-
pid: {
|
|
222
|
-
type: 'number',
|
|
223
|
-
description: 'The process ID returned by run tool.',
|
|
224
|
-
},
|
|
225
|
-
verbose: {
|
|
226
|
-
type: 'boolean',
|
|
227
|
-
description: 'Optional: If true, returns detailed execution information including tool usage history. Defaults to false.',
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
required: ['pid'],
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
name: 'wait',
|
|
235
|
-
description: 'Wait for multiple AI agent processes to complete and return their results. Blocks until all specified PIDs finish or timeout occurs.',
|
|
236
|
-
inputSchema: {
|
|
237
|
-
type: 'object',
|
|
238
|
-
properties: {
|
|
239
|
-
pids: {
|
|
240
|
-
type: 'array',
|
|
241
|
-
items: { type: 'number' },
|
|
242
|
-
description: 'List of process IDs to wait for (returned by the run tool).',
|
|
243
|
-
},
|
|
244
|
-
timeout: {
|
|
245
|
-
type: 'number',
|
|
246
|
-
description: 'Optional: Maximum time to wait in seconds. Defaults to 180 (3 minutes).',
|
|
247
|
-
},
|
|
248
|
-
},
|
|
249
|
-
required: ['pids'],
|
|
250
|
-
},
|
|
251
|
-
},
|
|
252
|
-
{
|
|
253
|
-
name: 'kill_process',
|
|
254
|
-
description: 'Terminate a running AI agent process by PID.',
|
|
255
|
-
inputSchema: {
|
|
256
|
-
type: 'object',
|
|
257
|
-
properties: {
|
|
258
|
-
pid: {
|
|
259
|
-
type: 'number',
|
|
260
|
-
description: 'The process ID to terminate.',
|
|
261
|
-
},
|
|
262
|
-
},
|
|
263
|
-
required: ['pid'],
|
|
264
|
-
},
|
|
265
|
-
},
|
|
266
|
-
{
|
|
267
|
-
name: 'cleanup_processes',
|
|
268
|
-
description: 'Remove all completed and failed processes from the process list to free up memory.',
|
|
269
|
-
inputSchema: {
|
|
270
|
-
type: 'object',
|
|
271
|
-
properties: {},
|
|
272
|
-
},
|
|
273
|
-
}
|
|
274
|
-
],
|
|
275
|
-
}));
|
|
276
|
-
|
|
277
|
-
// Handle tool calls
|
|
278
|
-
const executionTimeoutMs = 1800000; // 30 minutes timeout
|
|
279
|
-
|
|
280
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (args, call): Promise<ServerResult> => {
|
|
281
|
-
debugLog('[Debug] Handling CallToolRequest:', args);
|
|
282
|
-
|
|
283
|
-
const toolName = args.params.name;
|
|
284
|
-
const toolArguments = args.params.arguments || {};
|
|
285
|
-
|
|
286
|
-
switch (toolName) {
|
|
287
|
-
case 'run':
|
|
288
|
-
return this.handleRun(toolArguments);
|
|
289
|
-
case 'list_processes':
|
|
290
|
-
return this.handleListProcesses();
|
|
291
|
-
case 'get_result':
|
|
292
|
-
return this.handleGetResult(toolArguments);
|
|
293
|
-
case 'wait':
|
|
294
|
-
return this.handleWait(toolArguments);
|
|
295
|
-
case 'kill_process':
|
|
296
|
-
return this.handleKillProcess(toolArguments);
|
|
297
|
-
case 'cleanup_processes':
|
|
298
|
-
return this.handleCleanupProcesses();
|
|
299
|
-
default:
|
|
300
|
-
throw new McpError(ErrorCode.MethodNotFound, `Tool ${toolName} not found`);
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/**
|
|
306
|
-
* Handle run tool - starts Claude or Codex process and returns PID immediately
|
|
307
|
-
*/
|
|
308
|
-
private async handleRun(toolArguments: any): Promise<ServerResult> {
|
|
309
|
-
// Print version on first use
|
|
310
|
-
if (isFirstToolUse) {
|
|
311
|
-
console.error(`ai_cli_mcp v${SERVER_VERSION} started at ${serverStartupTime}`);
|
|
312
|
-
isFirstToolUse = false;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Build CLI command (validation + args assembly)
|
|
316
|
-
let cmd;
|
|
317
|
-
try {
|
|
318
|
-
cmd = buildCliCommand({
|
|
319
|
-
prompt: toolArguments.prompt,
|
|
320
|
-
prompt_file: toolArguments.prompt_file,
|
|
321
|
-
workFolder: toolArguments.workFolder,
|
|
322
|
-
model: toolArguments.model,
|
|
323
|
-
session_id: toolArguments.session_id,
|
|
324
|
-
reasoning_effort: toolArguments.reasoning_effort,
|
|
325
|
-
cliPaths: {
|
|
326
|
-
claude: this.claudeCliPath,
|
|
327
|
-
codex: this.codexCliPath,
|
|
328
|
-
gemini: this.geminiCliPath,
|
|
329
|
-
},
|
|
330
|
-
});
|
|
331
|
-
} catch (error: any) {
|
|
332
|
-
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const { cliPath, args: processArgs, cwd: effectiveCwd, agent, prompt } = cmd;
|
|
336
|
-
|
|
337
|
-
// Spawn process without waiting
|
|
338
|
-
const childProcess = spawn(cliPath, processArgs, {
|
|
339
|
-
cwd: effectiveCwd,
|
|
340
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
341
|
-
detached: false
|
|
342
|
-
});
|
|
343
|
-
|
|
344
|
-
const pid = childProcess.pid;
|
|
345
|
-
if (!pid) {
|
|
346
|
-
throw new McpError(ErrorCode.InternalError, `Failed to start ${agent} CLI process`);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Create process tracking entry
|
|
350
|
-
const processEntry: ClaudeProcess = {
|
|
351
|
-
pid,
|
|
352
|
-
process: childProcess,
|
|
353
|
-
prompt,
|
|
354
|
-
workFolder: effectiveCwd,
|
|
355
|
-
model: toolArguments.model,
|
|
356
|
-
toolType: agent,
|
|
357
|
-
startTime: new Date().toISOString(),
|
|
358
|
-
stdout: '',
|
|
359
|
-
stderr: '',
|
|
360
|
-
status: 'running'
|
|
361
|
-
};
|
|
362
|
-
|
|
363
|
-
// Track the process
|
|
364
|
-
processManager.set(pid, processEntry);
|
|
365
|
-
|
|
366
|
-
// Set up output collection
|
|
367
|
-
childProcess.stdout.on('data', (data) => {
|
|
368
|
-
const entry = processManager.get(pid);
|
|
369
|
-
if (entry) {
|
|
370
|
-
entry.stdout += data.toString();
|
|
371
|
-
}
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
childProcess.stderr.on('data', (data) => {
|
|
375
|
-
const entry = processManager.get(pid);
|
|
376
|
-
if (entry) {
|
|
377
|
-
entry.stderr += data.toString();
|
|
378
|
-
}
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
childProcess.on('close', (code) => {
|
|
382
|
-
const entry = processManager.get(pid);
|
|
383
|
-
if (entry) {
|
|
384
|
-
entry.status = code === 0 ? 'completed' : 'failed';
|
|
385
|
-
entry.exitCode = code !== null ? code : undefined;
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
childProcess.on('error', (error) => {
|
|
390
|
-
const entry = processManager.get(pid);
|
|
391
|
-
if (entry) {
|
|
392
|
-
entry.status = 'failed';
|
|
393
|
-
entry.stderr += `\nProcess error: ${error.message}`;
|
|
394
|
-
}
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// Return PID immediately
|
|
398
|
-
return {
|
|
399
|
-
content: [{
|
|
400
|
-
type: 'text',
|
|
401
|
-
text: JSON.stringify({
|
|
402
|
-
pid,
|
|
403
|
-
status: 'started',
|
|
404
|
-
agent,
|
|
405
|
-
message: `${agent} process started successfully`
|
|
406
|
-
}, null, 2)
|
|
407
|
-
}]
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/**
|
|
412
|
-
* Handle list_processes tool
|
|
413
|
-
*/
|
|
414
|
-
private async handleListProcesses(): Promise<ServerResult> {
|
|
415
|
-
const processes: ProcessListItem[] = [];
|
|
416
|
-
|
|
417
|
-
for (const [pid, process] of processManager.entries()) {
|
|
418
|
-
const processInfo: ProcessListItem = {
|
|
419
|
-
pid,
|
|
420
|
-
agent: process.toolType,
|
|
421
|
-
status: process.status
|
|
422
|
-
};
|
|
423
|
-
|
|
424
|
-
processes.push(processInfo);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
content: [{
|
|
429
|
-
type: 'text',
|
|
430
|
-
text: JSON.stringify(processes, null, 2)
|
|
431
|
-
}]
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Helper to get process result object
|
|
437
|
-
*/
|
|
438
|
-
private getProcessResultHelper(pid: number, verbose: boolean = false): any {
|
|
439
|
-
const process = processManager.get(pid);
|
|
440
|
-
|
|
441
|
-
if (!process) {
|
|
442
|
-
throw new McpError(ErrorCode.InvalidParams, `Process with PID ${pid} not found`);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
// Parse output based on agent type
|
|
446
|
-
let agentOutput: any = null;
|
|
447
|
-
if (process.toolType === 'codex') {
|
|
448
|
-
// Codex may output structured logs to stderr
|
|
449
|
-
const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
|
|
450
|
-
agentOutput = parseCodexOutput(combinedOutput);
|
|
451
|
-
} else if (process.stdout) {
|
|
452
|
-
if (process.toolType === 'claude') {
|
|
453
|
-
agentOutput = parseClaudeOutput(process.stdout);
|
|
454
|
-
} else if (process.toolType === 'gemini') {
|
|
455
|
-
agentOutput = parseGeminiOutput(process.stdout);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
// Construct response with agent's output and process metadata
|
|
460
|
-
const response: any = {
|
|
461
|
-
pid,
|
|
462
|
-
agent: process.toolType,
|
|
463
|
-
status: process.status,
|
|
464
|
-
exitCode: process.exitCode,
|
|
465
|
-
startTime: process.startTime,
|
|
466
|
-
workFolder: process.workFolder,
|
|
467
|
-
prompt: process.prompt,
|
|
468
|
-
model: process.model
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
// If we have valid output from agent, include it
|
|
472
|
-
if (agentOutput) {
|
|
473
|
-
// Filter out tools if not verbose
|
|
474
|
-
if (!verbose && agentOutput.tools) {
|
|
475
|
-
const { tools, ...rest } = agentOutput;
|
|
476
|
-
response.agentOutput = rest;
|
|
477
|
-
} else {
|
|
478
|
-
response.agentOutput = agentOutput;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
// Extract session_id if available
|
|
482
|
-
if (agentOutput.session_id) {
|
|
483
|
-
response.session_id = agentOutput.session_id;
|
|
484
|
-
}
|
|
485
|
-
} else {
|
|
486
|
-
// Fallback to raw output
|
|
487
|
-
response.stdout = process.stdout;
|
|
488
|
-
response.stderr = process.stderr;
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
return response;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Handle get_result tool
|
|
496
|
-
*/
|
|
497
|
-
private async handleGetResult(toolArguments: any): Promise<ServerResult> {
|
|
498
|
-
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
499
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const pid = toolArguments.pid;
|
|
503
|
-
const verbose = !!toolArguments.verbose;
|
|
504
|
-
const response = this.getProcessResultHelper(pid, verbose);
|
|
505
|
-
|
|
506
|
-
return {
|
|
507
|
-
content: [{
|
|
508
|
-
type: 'text',
|
|
509
|
-
text: JSON.stringify(response, null, 2)
|
|
510
|
-
}]
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Handle wait tool
|
|
516
|
-
*/
|
|
517
|
-
private async handleWait(toolArguments: any): Promise<ServerResult> {
|
|
518
|
-
if (!toolArguments.pids || !Array.isArray(toolArguments.pids) || toolArguments.pids.length === 0) {
|
|
519
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pids (must be a non-empty array of numbers)');
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
const pids: number[] = toolArguments.pids;
|
|
523
|
-
// Default timeout: 3 minutes (180 seconds)
|
|
524
|
-
const timeoutSeconds = typeof toolArguments.timeout === 'number' ? toolArguments.timeout : 180;
|
|
525
|
-
const timeoutMs = timeoutSeconds * 1000;
|
|
526
|
-
|
|
527
|
-
// Validate all PIDs exist first
|
|
528
|
-
for (const pid of pids) {
|
|
529
|
-
if (!processManager.has(pid)) {
|
|
530
|
-
throw new McpError(ErrorCode.InvalidParams, `Process with PID ${pid} not found`);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Create promises for each process
|
|
535
|
-
const waitPromises = pids.map(pid => {
|
|
536
|
-
const processEntry = processManager.get(pid)!;
|
|
537
|
-
|
|
538
|
-
if (processEntry.status !== 'running') {
|
|
539
|
-
return Promise.resolve();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
return new Promise<void>((resolve) => {
|
|
543
|
-
processEntry.process.once('close', () => {
|
|
544
|
-
resolve();
|
|
545
|
-
});
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
// Create a timeout promise
|
|
550
|
-
const timeoutPromise = new Promise<void>((_, reject) => {
|
|
551
|
-
setTimeout(() => {
|
|
552
|
-
reject(new Error(`Timed out after ${timeoutSeconds} seconds waiting for processes`));
|
|
553
|
-
}, timeoutMs);
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
// Wait for all processes to finish or timeout
|
|
558
|
-
await Promise.race([Promise.all(waitPromises), timeoutPromise]);
|
|
559
|
-
} catch (error: any) {
|
|
560
|
-
throw new McpError(ErrorCode.InternalError, error.message);
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// Collect results (verbose=false for wait)
|
|
564
|
-
const results = pids.map(pid => this.getProcessResultHelper(pid, false));
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
content: [{
|
|
568
|
-
type: 'text',
|
|
569
|
-
text: JSON.stringify(results, null, 2)
|
|
570
|
-
}]
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* Handle kill_process tool
|
|
576
|
-
*/
|
|
577
|
-
|
|
578
|
-
private async handleKillProcess(toolArguments: any): Promise<ServerResult> {
|
|
579
|
-
if (!toolArguments.pid || typeof toolArguments.pid !== 'number') {
|
|
580
|
-
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
const pid = toolArguments.pid;
|
|
584
|
-
const processEntry = processManager.get(pid);
|
|
585
|
-
|
|
586
|
-
if (!processEntry) {
|
|
587
|
-
throw new McpError(ErrorCode.InvalidParams, `Process with PID ${pid} not found`);
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
if (processEntry.status !== 'running') {
|
|
591
|
-
return {
|
|
592
|
-
content: [{
|
|
593
|
-
type: 'text',
|
|
594
|
-
text: JSON.stringify({
|
|
595
|
-
pid,
|
|
596
|
-
status: processEntry.status,
|
|
597
|
-
message: 'Process already terminated'
|
|
598
|
-
}, null, 2)
|
|
599
|
-
}]
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
try {
|
|
604
|
-
processEntry.process.kill('SIGTERM');
|
|
605
|
-
processEntry.status = 'failed';
|
|
606
|
-
processEntry.stderr += '\nProcess terminated by user';
|
|
607
|
-
|
|
608
|
-
return {
|
|
609
|
-
content: [{
|
|
610
|
-
type: 'text',
|
|
611
|
-
text: JSON.stringify({
|
|
612
|
-
pid,
|
|
613
|
-
status: 'terminated',
|
|
614
|
-
message: 'Process terminated successfully'
|
|
615
|
-
}, null, 2)
|
|
616
|
-
}]
|
|
617
|
-
};
|
|
618
|
-
} catch (error: any) {
|
|
619
|
-
throw new McpError(ErrorCode.InternalError, `Failed to terminate process: ${error.message}`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Handle cleanup_processes tool
|
|
625
|
-
*/
|
|
626
|
-
private async handleCleanupProcesses(): Promise<ServerResult> {
|
|
627
|
-
const removedPids: number[] = [];
|
|
628
|
-
|
|
629
|
-
// Iterate through all processes and collect PIDs to remove
|
|
630
|
-
for (const [pid, process] of processManager.entries()) {
|
|
631
|
-
if (process.status === 'completed' || process.status === 'failed') {
|
|
632
|
-
removedPids.push(pid);
|
|
633
|
-
processManager.delete(pid);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
return {
|
|
638
|
-
content: [{
|
|
639
|
-
type: 'text',
|
|
640
|
-
text: JSON.stringify({
|
|
641
|
-
removed: removedPids.length,
|
|
642
|
-
removedPids,
|
|
643
|
-
message: `Cleaned up ${removedPids.length} finished process(es)`
|
|
644
|
-
}, null, 2)
|
|
645
|
-
}]
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
/**
|
|
650
|
-
* Start the MCP server
|
|
651
|
-
*/
|
|
652
|
-
async run(): Promise<void> {
|
|
653
|
-
// Revert to original server start logic if listen caused errors
|
|
654
|
-
const transport = new StdioServerTransport();
|
|
655
|
-
await this.server.connect(transport);
|
|
656
|
-
console.error('AI CLI MCP server running on stdio');
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Clean up resources (for testing)
|
|
661
|
-
*/
|
|
662
|
-
async cleanup(): Promise<void> {
|
|
663
|
-
if (this.sigintHandler) {
|
|
664
|
-
process.removeListener('SIGINT', this.sigintHandler);
|
|
665
|
-
}
|
|
666
|
-
await this.server.close();
|
|
667
|
-
}
|
|
668
|
-
}
|
|
6
|
+
import { runMcpServer } from './app/mcp.js';
|
|
669
7
|
|
|
670
|
-
// Create and run the server (skip during tests)
|
|
671
8
|
if (!process.env.VITEST) {
|
|
672
|
-
|
|
673
|
-
server.run().catch(console.error);
|
|
9
|
+
runMcpServer().catch(console.error);
|
|
674
10
|
}
|