ai-cli-mcp 2.4.0 → 2.5.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/.gemini/settings.json +11 -0
- package/.mcp.json +2 -1
- package/CHANGELOG.md +9 -0
- package/dist/__tests__/parsers.test.js +98 -0
- package/dist/parsers.js +90 -4
- package/dist/server.js +25 -11
- package/package.json +1 -1
- package/src/__tests__/parsers.test.ts +108 -0
- package/src/parsers.ts +96 -4
- package/src/server.ts +24 -10
package/.mcp.json
CHANGED
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# [2.5.0](https://github.com/mkXultra/claude-code-mcp/compare/v2.4.0...v2.5.0) (2026-01-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* enhance output parsers for Codex and Claude with tool usage extraction ([b6410a1](https://github.com/mkXultra/claude-code-mcp/commit/b6410a104666eca592735acea093b877c0f03f64))
|
|
7
|
+
* track command execution in Codex output and include .gemini config ([91f7f06](https://github.com/mkXultra/claude-code-mcp/commit/91f7f067a1d453fd8e3a5a95bb90f21b7df0af8a))
|
|
8
|
+
* update Claude CLI args to stream-json and add verbose option to get_result ([b7f9abc](https://github.com/mkXultra/claude-code-mcp/commit/b7f9abc11c56ad0c8c95e90a614d1d869d8a3bfa))
|
|
9
|
+
|
|
1
10
|
# [2.4.0](https://github.com/mkXultra/claude-code-mcp/compare/v2.3.3...v2.4.0) (2026-01-24)
|
|
2
11
|
|
|
3
12
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexOutput, parseClaudeOutput } from '../parsers.js';
|
|
3
|
+
describe('parseCodexOutput', () => {
|
|
4
|
+
it('should parse basic Codex output with message and session_id', () => {
|
|
5
|
+
const output = `
|
|
6
|
+
{"type":"thread.started","thread_id":"test-session-id"}
|
|
7
|
+
{"type":"turn.started"}
|
|
8
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Hello world"}}
|
|
9
|
+
{"type":"turn.completed"}
|
|
10
|
+
`;
|
|
11
|
+
const result = parseCodexOutput(output);
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
message: "Hello world",
|
|
14
|
+
session_id: "test-session-id",
|
|
15
|
+
token_count: null,
|
|
16
|
+
tools: undefined
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('should extract MCP tool calls', () => {
|
|
20
|
+
const output = `
|
|
21
|
+
{"type":"thread.started","thread_id":"tool-test-id"}
|
|
22
|
+
{"type":"turn.started"}
|
|
23
|
+
{"type":"item.completed","item":{"id":"item_1","type":"mcp_tool_call","server":"acm","tool":"run","arguments":{"model":"gemini-2.5-flash","prompt":"hi"},"result":{"content":[{"text":"started","type":"text"}]},"status":"completed"}}
|
|
24
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Tool executed"}}
|
|
25
|
+
{"type":"turn.completed"}
|
|
26
|
+
`;
|
|
27
|
+
const result = parseCodexOutput(output);
|
|
28
|
+
expect(result.message).toBe("Tool executed");
|
|
29
|
+
expect(result.session_id).toBe("tool-test-id");
|
|
30
|
+
expect(result.tools).toHaveLength(1);
|
|
31
|
+
expect(result.tools[0]).toEqual({
|
|
32
|
+
tool: "run",
|
|
33
|
+
server: "acm",
|
|
34
|
+
input: { model: "gemini-2.5-flash", prompt: "hi" },
|
|
35
|
+
output: { content: [{ text: "started", type: "text" }] }
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('should handle multiple tool calls', () => {
|
|
39
|
+
const output = `
|
|
40
|
+
{"type":"item.completed","item":{"type":"mcp_tool_call","tool":"tool1","arguments":{"arg":1},"result":"res1"}}
|
|
41
|
+
{"type":"item.completed","item":{"type":"mcp_tool_call","tool":"tool2","arguments":{"arg":2},"result":"res2"}}
|
|
42
|
+
`;
|
|
43
|
+
const result = parseCodexOutput(output);
|
|
44
|
+
expect(result.tools).toHaveLength(2);
|
|
45
|
+
expect(result.tools[0].tool).toBe("tool1");
|
|
46
|
+
expect(result.tools[1].tool).toBe("tool2");
|
|
47
|
+
});
|
|
48
|
+
it('should return null for empty input', () => {
|
|
49
|
+
expect(parseCodexOutput("")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
it('should handle invalid JSON gracefully', () => {
|
|
52
|
+
const output = `
|
|
53
|
+
{"type":"valid"}
|
|
54
|
+
INVALID_JSON
|
|
55
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Still parses valid lines"}}
|
|
56
|
+
`;
|
|
57
|
+
const result = parseCodexOutput(output);
|
|
58
|
+
expect(result.message).toBe("Still parses valid lines");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
describe('parseClaudeOutput', () => {
|
|
62
|
+
it('should parse legacy JSON output', () => {
|
|
63
|
+
const output = JSON.stringify({
|
|
64
|
+
content: [{ type: 'text', text: 'Hello' }]
|
|
65
|
+
});
|
|
66
|
+
const result = parseClaudeOutput(output);
|
|
67
|
+
expect(result).toEqual({
|
|
68
|
+
content: [{ type: 'text', text: 'Hello' }]
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it('should parse stream-json (NDJSON) output', () => {
|
|
72
|
+
const output = `
|
|
73
|
+
{"type":"system","session_id":"test-claude-session"}
|
|
74
|
+
{"type":"assistant","message":{"content":[{"type":"text","text":"Thinking..."}]}}
|
|
75
|
+
{"type":"assistant","message":{"content":[{"type":"tool_use","id":"call_1","name":"mcp__acm__run","input":{"prompt":"hi"}}]}}
|
|
76
|
+
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"call_1","content":"done"}]}}
|
|
77
|
+
{"type":"result","result":"Final Answer","is_error":false}
|
|
78
|
+
`;
|
|
79
|
+
const result = parseClaudeOutput(output);
|
|
80
|
+
expect(result.message).toBe("Final Answer");
|
|
81
|
+
expect(result.session_id).toBe("test-claude-session");
|
|
82
|
+
expect(result.tools).toHaveLength(1);
|
|
83
|
+
expect(result.tools[0]).toEqual({
|
|
84
|
+
tool: "mcp__acm__run",
|
|
85
|
+
input: { prompt: "hi" },
|
|
86
|
+
output: "done"
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
it('should handle invalid NDJSON lines gracefully', () => {
|
|
90
|
+
const output = `
|
|
91
|
+
{"type":"system"}
|
|
92
|
+
INVALID_LINE
|
|
93
|
+
{"type":"result","result":"Success"}
|
|
94
|
+
`;
|
|
95
|
+
const result = parseClaudeOutput(output);
|
|
96
|
+
expect(result.message).toBe("Success");
|
|
97
|
+
});
|
|
98
|
+
});
|
package/dist/parsers.js
CHANGED
|
@@ -10,6 +10,7 @@ export function parseCodexOutput(stdout) {
|
|
|
10
10
|
let lastMessage = null;
|
|
11
11
|
let tokenCount = null;
|
|
12
12
|
let threadId = null;
|
|
13
|
+
const tools = [];
|
|
13
14
|
for (const line of lines) {
|
|
14
15
|
if (line.trim()) {
|
|
15
16
|
try {
|
|
@@ -29,6 +30,22 @@ export function parseCodexOutput(stdout) {
|
|
|
29
30
|
else if (parsed.msg?.type === 'token_count') {
|
|
30
31
|
tokenCount = parsed.msg;
|
|
31
32
|
}
|
|
33
|
+
else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
|
|
34
|
+
tools.push({
|
|
35
|
+
server: parsed.item.server,
|
|
36
|
+
tool: parsed.item.tool,
|
|
37
|
+
input: parsed.item.arguments, // Map arguments to input to match common patterns
|
|
38
|
+
output: parsed.item.result
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
|
|
42
|
+
tools.push({
|
|
43
|
+
tool: 'command_execution',
|
|
44
|
+
input: { command: parsed.item.command },
|
|
45
|
+
output: parsed.item.aggregated_output,
|
|
46
|
+
exit_code: parsed.item.exit_code
|
|
47
|
+
});
|
|
48
|
+
}
|
|
32
49
|
}
|
|
33
50
|
catch (e) {
|
|
34
51
|
// Skip invalid JSON lines
|
|
@@ -36,11 +53,12 @@ export function parseCodexOutput(stdout) {
|
|
|
36
53
|
}
|
|
37
54
|
}
|
|
38
55
|
}
|
|
39
|
-
if (lastMessage || tokenCount || threadId) {
|
|
56
|
+
if (lastMessage || tokenCount || threadId || tools.length > 0) {
|
|
40
57
|
return {
|
|
41
58
|
message: lastMessage,
|
|
42
59
|
token_count: tokenCount,
|
|
43
|
-
session_id: threadId
|
|
60
|
+
session_id: threadId,
|
|
61
|
+
tools: tools.length > 0 ? tools : undefined
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
64
|
}
|
|
@@ -50,18 +68,86 @@ export function parseCodexOutput(stdout) {
|
|
|
50
68
|
return null;
|
|
51
69
|
}
|
|
52
70
|
/**
|
|
53
|
-
* Parse Claude JSON
|
|
71
|
+
* Parse Claude Output (supports both JSON and stream-json/NDJSON)
|
|
54
72
|
*/
|
|
55
73
|
export function parseClaudeOutput(stdout) {
|
|
56
74
|
if (!stdout)
|
|
57
75
|
return null;
|
|
76
|
+
// First try parsing as a single JSON object (backward compatibility)
|
|
58
77
|
try {
|
|
59
78
|
return JSON.parse(stdout);
|
|
60
79
|
}
|
|
61
80
|
catch (e) {
|
|
62
|
-
|
|
81
|
+
// If not valid single JSON, proceed to parse as NDJSON
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const lines = stdout.trim().split('\n');
|
|
85
|
+
let lastMessage = null;
|
|
86
|
+
let sessionId = null;
|
|
87
|
+
const toolsMap = new Map(); // Map by tool_use id for matching results
|
|
88
|
+
for (const line of lines) {
|
|
89
|
+
if (!line.trim())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(line);
|
|
93
|
+
// Extract session ID from any message that has it
|
|
94
|
+
if (parsed.session_id) {
|
|
95
|
+
sessionId = parsed.session_id;
|
|
96
|
+
}
|
|
97
|
+
// Extract final result message
|
|
98
|
+
if (parsed.type === 'result' && parsed.result) {
|
|
99
|
+
lastMessage = parsed.result;
|
|
100
|
+
}
|
|
101
|
+
// Extract tool usage from assistant messages
|
|
102
|
+
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
103
|
+
for (const content of parsed.message.content) {
|
|
104
|
+
if (content.type === 'tool_use') {
|
|
105
|
+
toolsMap.set(content.id, {
|
|
106
|
+
tool: content.name,
|
|
107
|
+
input: content.input,
|
|
108
|
+
output: null // Will be filled when tool_result is found
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Match tool results from user messages
|
|
114
|
+
if (parsed.type === 'user' && parsed.message?.content) {
|
|
115
|
+
for (const content of parsed.message.content) {
|
|
116
|
+
if (content.type === 'tool_result' && content.tool_use_id) {
|
|
117
|
+
const tool = toolsMap.get(content.tool_use_id);
|
|
118
|
+
if (tool) {
|
|
119
|
+
// Extract text from content array
|
|
120
|
+
if (Array.isArray(content.content)) {
|
|
121
|
+
const textContent = content.content.find((c) => c.type === 'text');
|
|
122
|
+
tool.output = textContent?.text || null;
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
tool.output = content.content;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Convert Map to array
|
|
137
|
+
const tools = Array.from(toolsMap.values());
|
|
138
|
+
if (lastMessage || sessionId || tools.length > 0) {
|
|
139
|
+
return {
|
|
140
|
+
message: lastMessage, // This is the final result text
|
|
141
|
+
session_id: sessionId,
|
|
142
|
+
tools: tools.length > 0 ? tools : undefined
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
|
|
63
148
|
return null;
|
|
64
149
|
}
|
|
150
|
+
return null;
|
|
65
151
|
}
|
|
66
152
|
/**
|
|
67
153
|
* Parse Gemini JSON output
|
package/dist/server.js
CHANGED
|
@@ -329,6 +329,10 @@ export class ClaudeCodeServer {
|
|
|
329
329
|
type: 'number',
|
|
330
330
|
description: 'The process ID returned by run tool.',
|
|
331
331
|
},
|
|
332
|
+
verbose: {
|
|
333
|
+
type: 'boolean',
|
|
334
|
+
description: 'Optional: If true, returns detailed execution information including tool usage history. Defaults to false.',
|
|
335
|
+
}
|
|
332
336
|
},
|
|
333
337
|
required: ['pid'],
|
|
334
338
|
},
|
|
@@ -506,7 +510,7 @@ export class ClaudeCodeServer {
|
|
|
506
510
|
else {
|
|
507
511
|
// Handle Claude (default)
|
|
508
512
|
cliPath = this.claudeCliPath;
|
|
509
|
-
processArgs = ['--dangerously-skip-permissions', '--output-format', 'json'];
|
|
513
|
+
processArgs = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
|
510
514
|
// Add session_id if provided (Claude only)
|
|
511
515
|
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
512
516
|
processArgs.push('-r', toolArguments.session_id);
|
|
@@ -604,18 +608,20 @@ export class ClaudeCodeServer {
|
|
|
604
608
|
/**
|
|
605
609
|
* Helper to get process result object
|
|
606
610
|
*/
|
|
607
|
-
getProcessResultHelper(pid) {
|
|
611
|
+
getProcessResultHelper(pid, verbose = false) {
|
|
608
612
|
const process = processManager.get(pid);
|
|
609
613
|
if (!process) {
|
|
610
614
|
throw new McpError(ErrorCode.InvalidParams, `Process with PID ${pid} not found`);
|
|
611
615
|
}
|
|
612
616
|
// Parse output based on agent type
|
|
613
617
|
let agentOutput = null;
|
|
614
|
-
if (process.
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
618
|
+
if (process.toolType === 'codex') {
|
|
619
|
+
// Codex may output structured logs to stderr
|
|
620
|
+
const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
|
|
621
|
+
agentOutput = parseCodexOutput(combinedOutput);
|
|
622
|
+
}
|
|
623
|
+
else if (process.stdout) {
|
|
624
|
+
if (process.toolType === 'claude') {
|
|
619
625
|
agentOutput = parseClaudeOutput(process.stdout);
|
|
620
626
|
}
|
|
621
627
|
else if (process.toolType === 'gemini') {
|
|
@@ -635,7 +641,14 @@ export class ClaudeCodeServer {
|
|
|
635
641
|
};
|
|
636
642
|
// If we have valid output from agent, include it
|
|
637
643
|
if (agentOutput) {
|
|
638
|
-
|
|
644
|
+
// Filter out tools if not verbose
|
|
645
|
+
if (!verbose && agentOutput.tools) {
|
|
646
|
+
const { tools, ...rest } = agentOutput;
|
|
647
|
+
response.agentOutput = rest;
|
|
648
|
+
}
|
|
649
|
+
else {
|
|
650
|
+
response.agentOutput = agentOutput;
|
|
651
|
+
}
|
|
639
652
|
// Extract session_id if available
|
|
640
653
|
if (agentOutput.session_id) {
|
|
641
654
|
response.session_id = agentOutput.session_id;
|
|
@@ -656,7 +669,8 @@ export class ClaudeCodeServer {
|
|
|
656
669
|
throw new McpError(ErrorCode.InvalidParams, 'Missing or invalid required parameter: pid');
|
|
657
670
|
}
|
|
658
671
|
const pid = toolArguments.pid;
|
|
659
|
-
const
|
|
672
|
+
const verbose = !!toolArguments.verbose;
|
|
673
|
+
const response = this.getProcessResultHelper(pid, verbose);
|
|
660
674
|
return {
|
|
661
675
|
content: [{
|
|
662
676
|
type: 'text',
|
|
@@ -706,8 +720,8 @@ export class ClaudeCodeServer {
|
|
|
706
720
|
catch (error) {
|
|
707
721
|
throw new McpError(ErrorCode.InternalError, error.message);
|
|
708
722
|
}
|
|
709
|
-
// Collect results
|
|
710
|
-
const results = pids.map(pid => this.getProcessResultHelper(pid));
|
|
723
|
+
// Collect results (verbose=false for wait)
|
|
724
|
+
const results = pids.map(pid => this.getProcessResultHelper(pid, false));
|
|
711
725
|
return {
|
|
712
726
|
content: [{
|
|
713
727
|
type: 'text',
|
package/package.json
CHANGED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { parseCodexOutput, parseClaudeOutput } from '../parsers.js';
|
|
3
|
+
|
|
4
|
+
describe('parseCodexOutput', () => {
|
|
5
|
+
it('should parse basic Codex output with message and session_id', () => {
|
|
6
|
+
const output = `
|
|
7
|
+
{"type":"thread.started","thread_id":"test-session-id"}
|
|
8
|
+
{"type":"turn.started"}
|
|
9
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Hello world"}}
|
|
10
|
+
{"type":"turn.completed"}
|
|
11
|
+
`;
|
|
12
|
+
const result = parseCodexOutput(output);
|
|
13
|
+
expect(result).toEqual({
|
|
14
|
+
message: "Hello world",
|
|
15
|
+
session_id: "test-session-id",
|
|
16
|
+
token_count: null,
|
|
17
|
+
tools: undefined
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should extract MCP tool calls', () => {
|
|
22
|
+
const output = `
|
|
23
|
+
{"type":"thread.started","thread_id":"tool-test-id"}
|
|
24
|
+
{"type":"turn.started"}
|
|
25
|
+
{"type":"item.completed","item":{"id":"item_1","type":"mcp_tool_call","server":"acm","tool":"run","arguments":{"model":"gemini-2.5-flash","prompt":"hi"},"result":{"content":[{"text":"started","type":"text"}]},"status":"completed"}}
|
|
26
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Tool executed"}}
|
|
27
|
+
{"type":"turn.completed"}
|
|
28
|
+
`;
|
|
29
|
+
const result = parseCodexOutput(output);
|
|
30
|
+
|
|
31
|
+
expect(result.message).toBe("Tool executed");
|
|
32
|
+
expect(result.session_id).toBe("tool-test-id");
|
|
33
|
+
expect(result.tools).toHaveLength(1);
|
|
34
|
+
expect(result.tools[0]).toEqual({
|
|
35
|
+
tool: "run",
|
|
36
|
+
server: "acm",
|
|
37
|
+
input: { model: "gemini-2.5-flash", prompt: "hi" },
|
|
38
|
+
output: { content: [{ text: "started", type: "text" }] }
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should handle multiple tool calls', () => {
|
|
43
|
+
const output = `
|
|
44
|
+
{"type":"item.completed","item":{"type":"mcp_tool_call","tool":"tool1","arguments":{"arg":1},"result":"res1"}}
|
|
45
|
+
{"type":"item.completed","item":{"type":"mcp_tool_call","tool":"tool2","arguments":{"arg":2},"result":"res2"}}
|
|
46
|
+
`;
|
|
47
|
+
const result = parseCodexOutput(output);
|
|
48
|
+
expect(result.tools).toHaveLength(2);
|
|
49
|
+
expect(result.tools[0].tool).toBe("tool1");
|
|
50
|
+
expect(result.tools[1].tool).toBe("tool2");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should return null for empty input', () => {
|
|
54
|
+
expect(parseCodexOutput("")).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should handle invalid JSON gracefully', () => {
|
|
58
|
+
const output = `
|
|
59
|
+
{"type":"valid"}
|
|
60
|
+
INVALID_JSON
|
|
61
|
+
{"type":"item.completed","item":{"type":"agent_message","text":"Still parses valid lines"}}
|
|
62
|
+
`;
|
|
63
|
+
const result = parseCodexOutput(output);
|
|
64
|
+
expect(result.message).toBe("Still parses valid lines");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('parseClaudeOutput', () => {
|
|
69
|
+
it('should parse legacy JSON output', () => {
|
|
70
|
+
const output = JSON.stringify({
|
|
71
|
+
content: [{ type: 'text', text: 'Hello' }]
|
|
72
|
+
});
|
|
73
|
+
const result = parseClaudeOutput(output);
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
content: [{ type: 'text', text: 'Hello' }]
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should parse stream-json (NDJSON) output', () => {
|
|
80
|
+
const output = `
|
|
81
|
+
{"type":"system","session_id":"test-claude-session"}
|
|
82
|
+
{"type":"assistant","message":{"content":[{"type":"text","text":"Thinking..."}]}}
|
|
83
|
+
{"type":"assistant","message":{"content":[{"type":"tool_use","id":"call_1","name":"mcp__acm__run","input":{"prompt":"hi"}}]}}
|
|
84
|
+
{"type":"user","message":{"content":[{"type":"tool_result","tool_use_id":"call_1","content":"done"}]}}
|
|
85
|
+
{"type":"result","result":"Final Answer","is_error":false}
|
|
86
|
+
`;
|
|
87
|
+
const result = parseClaudeOutput(output);
|
|
88
|
+
|
|
89
|
+
expect(result.message).toBe("Final Answer");
|
|
90
|
+
expect(result.session_id).toBe("test-claude-session");
|
|
91
|
+
expect(result.tools).toHaveLength(1);
|
|
92
|
+
expect(result.tools[0]).toEqual({
|
|
93
|
+
tool: "mcp__acm__run",
|
|
94
|
+
input: { prompt: "hi" },
|
|
95
|
+
output: "done"
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should handle invalid NDJSON lines gracefully', () => {
|
|
100
|
+
const output = `
|
|
101
|
+
{"type":"system"}
|
|
102
|
+
INVALID_LINE
|
|
103
|
+
{"type":"result","result":"Success"}
|
|
104
|
+
`;
|
|
105
|
+
const result = parseClaudeOutput(output);
|
|
106
|
+
expect(result.message).toBe("Success");
|
|
107
|
+
});
|
|
108
|
+
});
|
package/src/parsers.ts
CHANGED
|
@@ -11,6 +11,7 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
11
11
|
let lastMessage = null;
|
|
12
12
|
let tokenCount = null;
|
|
13
13
|
let threadId = null;
|
|
14
|
+
const tools: any[] = [];
|
|
14
15
|
|
|
15
16
|
for (const line of lines) {
|
|
16
17
|
if (line.trim()) {
|
|
@@ -26,6 +27,20 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
26
27
|
// Ignore reasoning-only items for message selection.
|
|
27
28
|
} else if (parsed.msg?.type === 'token_count') {
|
|
28
29
|
tokenCount = parsed.msg;
|
|
30
|
+
} else if (parsed.type === 'item.completed' && parsed.item?.type === 'mcp_tool_call') {
|
|
31
|
+
tools.push({
|
|
32
|
+
server: parsed.item.server,
|
|
33
|
+
tool: parsed.item.tool,
|
|
34
|
+
input: parsed.item.arguments, // Map arguments to input to match common patterns
|
|
35
|
+
output: parsed.item.result
|
|
36
|
+
});
|
|
37
|
+
} else if (parsed.type === 'item.completed' && parsed.item?.type === 'command_execution') {
|
|
38
|
+
tools.push({
|
|
39
|
+
tool: 'command_execution',
|
|
40
|
+
input: { command: parsed.item.command },
|
|
41
|
+
output: parsed.item.aggregated_output,
|
|
42
|
+
exit_code: parsed.item.exit_code
|
|
43
|
+
});
|
|
29
44
|
}
|
|
30
45
|
} catch (e) {
|
|
31
46
|
// Skip invalid JSON lines
|
|
@@ -34,11 +49,12 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
34
49
|
}
|
|
35
50
|
}
|
|
36
51
|
|
|
37
|
-
if (lastMessage || tokenCount || threadId) {
|
|
52
|
+
if (lastMessage || tokenCount || threadId || tools.length > 0) {
|
|
38
53
|
return {
|
|
39
54
|
message: lastMessage,
|
|
40
55
|
token_count: tokenCount,
|
|
41
|
-
session_id: threadId
|
|
56
|
+
session_id: threadId,
|
|
57
|
+
tools: tools.length > 0 ? tools : undefined
|
|
42
58
|
};
|
|
43
59
|
}
|
|
44
60
|
} catch (e) {
|
|
@@ -49,17 +65,93 @@ export function parseCodexOutput(stdout: string): any {
|
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/**
|
|
52
|
-
* Parse Claude JSON
|
|
68
|
+
* Parse Claude Output (supports both JSON and stream-json/NDJSON)
|
|
53
69
|
*/
|
|
54
70
|
export function parseClaudeOutput(stdout: string): any {
|
|
55
71
|
if (!stdout) return null;
|
|
56
72
|
|
|
73
|
+
// First try parsing as a single JSON object (backward compatibility)
|
|
57
74
|
try {
|
|
58
75
|
return JSON.parse(stdout);
|
|
59
76
|
} catch (e) {
|
|
60
|
-
|
|
77
|
+
// If not valid single JSON, proceed to parse as NDJSON
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const lines = stdout.trim().split('\n');
|
|
82
|
+
let lastMessage = null;
|
|
83
|
+
let sessionId = null;
|
|
84
|
+
const toolsMap = new Map<string, any>(); // Map by tool_use id for matching results
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
if (!line.trim()) continue;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(line);
|
|
91
|
+
|
|
92
|
+
// Extract session ID from any message that has it
|
|
93
|
+
if (parsed.session_id) {
|
|
94
|
+
sessionId = parsed.session_id;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract final result message
|
|
98
|
+
if (parsed.type === 'result' && parsed.result) {
|
|
99
|
+
lastMessage = parsed.result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Extract tool usage from assistant messages
|
|
103
|
+
if (parsed.type === 'assistant' && parsed.message?.content) {
|
|
104
|
+
for (const content of parsed.message.content) {
|
|
105
|
+
if (content.type === 'tool_use') {
|
|
106
|
+
toolsMap.set(content.id, {
|
|
107
|
+
tool: content.name,
|
|
108
|
+
input: content.input,
|
|
109
|
+
output: null // Will be filled when tool_result is found
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Match tool results from user messages
|
|
116
|
+
if (parsed.type === 'user' && parsed.message?.content) {
|
|
117
|
+
for (const content of parsed.message.content) {
|
|
118
|
+
if (content.type === 'tool_result' && content.tool_use_id) {
|
|
119
|
+
const tool = toolsMap.get(content.tool_use_id);
|
|
120
|
+
if (tool) {
|
|
121
|
+
// Extract text from content array
|
|
122
|
+
if (Array.isArray(content.content)) {
|
|
123
|
+
const textContent = content.content.find((c: any) => c.type === 'text');
|
|
124
|
+
tool.output = textContent?.text || null;
|
|
125
|
+
} else {
|
|
126
|
+
tool.output = content.content;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
} catch (e) {
|
|
134
|
+
debugLog(`[Debug] Skipping invalid JSON line in Claude output: ${line}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Convert Map to array
|
|
139
|
+
const tools = Array.from(toolsMap.values());
|
|
140
|
+
|
|
141
|
+
if (lastMessage || sessionId || tools.length > 0) {
|
|
142
|
+
return {
|
|
143
|
+
message: lastMessage, // This is the final result text
|
|
144
|
+
session_id: sessionId,
|
|
145
|
+
tools: tools.length > 0 ? tools : undefined
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
} catch (e) {
|
|
150
|
+
debugLog(`[Debug] Failed to parse Claude NDJSON output: ${e}`);
|
|
61
151
|
return null;
|
|
62
152
|
}
|
|
153
|
+
|
|
154
|
+
return null;
|
|
63
155
|
}
|
|
64
156
|
|
|
65
157
|
/**
|
package/src/server.ts
CHANGED
|
@@ -429,6 +429,10 @@ export class ClaudeCodeServer {
|
|
|
429
429
|
type: 'number',
|
|
430
430
|
description: 'The process ID returned by run tool.',
|
|
431
431
|
},
|
|
432
|
+
verbose: {
|
|
433
|
+
type: 'boolean',
|
|
434
|
+
description: 'Optional: If true, returns detailed execution information including tool usage history. Defaults to false.',
|
|
435
|
+
}
|
|
432
436
|
},
|
|
433
437
|
required: ['pid'],
|
|
434
438
|
},
|
|
@@ -626,7 +630,7 @@ export class ClaudeCodeServer {
|
|
|
626
630
|
} else {
|
|
627
631
|
// Handle Claude (default)
|
|
628
632
|
cliPath = this.claudeCliPath;
|
|
629
|
-
processArgs = ['--dangerously-skip-permissions', '--output-format', 'json'];
|
|
633
|
+
processArgs = ['--dangerously-skip-permissions', '--output-format', 'stream-json', '--verbose'];
|
|
630
634
|
|
|
631
635
|
// Add session_id if provided (Claude only)
|
|
632
636
|
if (toolArguments.session_id && typeof toolArguments.session_id === 'string') {
|
|
@@ -740,7 +744,7 @@ export class ClaudeCodeServer {
|
|
|
740
744
|
/**
|
|
741
745
|
* Helper to get process result object
|
|
742
746
|
*/
|
|
743
|
-
private getProcessResultHelper(pid: number): any {
|
|
747
|
+
private getProcessResultHelper(pid: number, verbose: boolean = false): any {
|
|
744
748
|
const process = processManager.get(pid);
|
|
745
749
|
|
|
746
750
|
if (!process) {
|
|
@@ -749,10 +753,12 @@ export class ClaudeCodeServer {
|
|
|
749
753
|
|
|
750
754
|
// Parse output based on agent type
|
|
751
755
|
let agentOutput: any = null;
|
|
752
|
-
if (process.
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
+
if (process.toolType === 'codex') {
|
|
757
|
+
// Codex may output structured logs to stderr
|
|
758
|
+
const combinedOutput = (process.stdout || '') + '\n' + (process.stderr || '');
|
|
759
|
+
agentOutput = parseCodexOutput(combinedOutput);
|
|
760
|
+
} else if (process.stdout) {
|
|
761
|
+
if (process.toolType === 'claude') {
|
|
756
762
|
agentOutput = parseClaudeOutput(process.stdout);
|
|
757
763
|
} else if (process.toolType === 'gemini') {
|
|
758
764
|
agentOutput = parseGeminiOutput(process.stdout);
|
|
@@ -773,7 +779,14 @@ export class ClaudeCodeServer {
|
|
|
773
779
|
|
|
774
780
|
// If we have valid output from agent, include it
|
|
775
781
|
if (agentOutput) {
|
|
776
|
-
|
|
782
|
+
// Filter out tools if not verbose
|
|
783
|
+
if (!verbose && agentOutput.tools) {
|
|
784
|
+
const { tools, ...rest } = agentOutput;
|
|
785
|
+
response.agentOutput = rest;
|
|
786
|
+
} else {
|
|
787
|
+
response.agentOutput = agentOutput;
|
|
788
|
+
}
|
|
789
|
+
|
|
777
790
|
// Extract session_id if available
|
|
778
791
|
if (agentOutput.session_id) {
|
|
779
792
|
response.session_id = agentOutput.session_id;
|
|
@@ -796,7 +809,8 @@ export class ClaudeCodeServer {
|
|
|
796
809
|
}
|
|
797
810
|
|
|
798
811
|
const pid = toolArguments.pid;
|
|
799
|
-
const
|
|
812
|
+
const verbose = !!toolArguments.verbose;
|
|
813
|
+
const response = this.getProcessResultHelper(pid, verbose);
|
|
800
814
|
|
|
801
815
|
return {
|
|
802
816
|
content: [{
|
|
@@ -855,8 +869,8 @@ export class ClaudeCodeServer {
|
|
|
855
869
|
throw new McpError(ErrorCode.InternalError, error.message);
|
|
856
870
|
}
|
|
857
871
|
|
|
858
|
-
// Collect results
|
|
859
|
-
const results = pids.map(pid => this.getProcessResultHelper(pid));
|
|
872
|
+
// Collect results (verbose=false for wait)
|
|
873
|
+
const results = pids.map(pid => this.getProcessResultHelper(pid, false));
|
|
860
874
|
|
|
861
875
|
return {
|
|
862
876
|
content: [{
|