agentxchain 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/package.json
CHANGED
package/src/commands/step.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* - local_cli: implemented via subprocess dispatch + staged turn result
|
|
18
18
|
* - api_proxy: implemented for synchronous review-only turns and stages
|
|
19
19
|
* provider-backed JSON before validation/acceptance
|
|
20
|
-
* - mcp: implemented for synchronous MCP stdio tool dispatch
|
|
20
|
+
* - mcp: implemented for synchronous MCP stdio or streamable_http tool dispatch
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import chalk from 'chalk';
|
|
@@ -46,7 +46,7 @@ import {
|
|
|
46
46
|
saveDispatchLogs,
|
|
47
47
|
resolvePromptTransport,
|
|
48
48
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
49
|
-
import { dispatchMcp } from '../lib/adapters/mcp-adapter.js';
|
|
49
|
+
import { describeMcpRuntimeTarget, dispatchMcp, resolveMcpTransport } from '../lib/adapters/mcp-adapter.js';
|
|
50
50
|
import {
|
|
51
51
|
getDispatchAssignmentPath,
|
|
52
52
|
getDispatchContextPath,
|
|
@@ -426,7 +426,8 @@ export async function stepCommand(opts) {
|
|
|
426
426
|
}
|
|
427
427
|
console.log('');
|
|
428
428
|
} else if (runtimeType === 'mcp') {
|
|
429
|
-
|
|
429
|
+
const mcpTransport = resolveMcpTransport(runtime);
|
|
430
|
+
console.log(chalk.cyan(`Dispatching to MCP ${mcpTransport}: ${describeMcpRuntimeTarget(runtime)}`));
|
|
430
431
|
console.log(chalk.dim(`Turn: ${turn.turn_id} Role: ${roleId} Phase: ${state.phase} Tool: ${runtime?.tool_name || 'agentxchain_turn'}`));
|
|
431
432
|
|
|
432
433
|
const mcpResult = await dispatchMcp(root, state, config, {
|
|
@@ -2,6 +2,7 @@ import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
4
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
5
6
|
import {
|
|
6
7
|
getDispatchAssignmentPath,
|
|
7
8
|
getDispatchContextPath,
|
|
@@ -13,12 +14,13 @@ import {
|
|
|
13
14
|
import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
14
15
|
|
|
15
16
|
export const DEFAULT_MCP_TOOL_NAME = 'agentxchain_turn';
|
|
17
|
+
export const DEFAULT_MCP_TRANSPORT = 'stdio';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
|
-
* Dispatch a governed turn to an MCP server
|
|
20
|
+
* Dispatch a governed turn to an MCP server.
|
|
19
21
|
*
|
|
20
|
-
*
|
|
21
|
-
* - stdio transport
|
|
22
|
+
* Current scope:
|
|
23
|
+
* - stdio or streamable_http transport
|
|
22
24
|
* - single tool call per turn
|
|
23
25
|
* - required governed-turn tool contract
|
|
24
26
|
* - synchronous dispatch/wait flow (like api_proxy)
|
|
@@ -55,7 +57,8 @@ export async function dispatchMcp(root, state, config, options = {}) {
|
|
|
55
57
|
const prompt = readFileSync(promptPath, 'utf8');
|
|
56
58
|
const context = existsSync(contextPath) ? readFileSync(contextPath, 'utf8') : '';
|
|
57
59
|
const { command, args } = resolveMcpCommand(runtime);
|
|
58
|
-
|
|
60
|
+
const transportType = resolveMcpTransport(runtime);
|
|
61
|
+
if (transportType === 'stdio' && !command) {
|
|
59
62
|
return { ok: false, error: `Cannot resolve MCP command for runtime "${runtimeId}". Expected "command" field in runtime config.` };
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -68,20 +71,16 @@ export async function dispatchMcp(root, state, config, options = {}) {
|
|
|
68
71
|
const stagingDir = join(root, getTurnStagingDir(turn.turn_id));
|
|
69
72
|
mkdirSync(stagingDir, { recursive: true });
|
|
70
73
|
|
|
71
|
-
const transport =
|
|
74
|
+
const transport = buildMcpClientTransport({
|
|
75
|
+
root,
|
|
76
|
+
runtime,
|
|
72
77
|
command,
|
|
73
78
|
args,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
stderr: 'pipe',
|
|
79
|
+
logs,
|
|
80
|
+
onStderr,
|
|
77
81
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
transport.stderr.on('data', (chunk) => {
|
|
81
|
-
const text = chunk.toString();
|
|
82
|
-
logs.push(`[stderr] ${text}`);
|
|
83
|
-
if (onStderr) onStderr(text);
|
|
84
|
-
});
|
|
82
|
+
if (!transport.ok) {
|
|
83
|
+
return { ok: false, error: transport.error, logs };
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
const client = new Client({
|
|
@@ -100,8 +99,8 @@ export async function dispatchMcp(root, state, config, options = {}) {
|
|
|
100
99
|
return { ok: false, aborted: true, logs };
|
|
101
100
|
}
|
|
102
101
|
|
|
103
|
-
onStatus?.(`Connecting to MCP
|
|
104
|
-
await client.connect(transport);
|
|
102
|
+
onStatus?.(`Connecting to MCP ${transportType} server (${describeMcpRuntimeTarget(runtime)})`);
|
|
103
|
+
await client.connect(transport.transport);
|
|
105
104
|
|
|
106
105
|
if (signal?.aborted) {
|
|
107
106
|
return { ok: false, aborted: true, logs };
|
|
@@ -182,7 +181,7 @@ export async function dispatchMcp(root, state, config, options = {}) {
|
|
|
182
181
|
logs,
|
|
183
182
|
};
|
|
184
183
|
} finally {
|
|
185
|
-
await safeCloseClient(client, transport);
|
|
184
|
+
await safeCloseClient(client, transport.transport);
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
187
|
|
|
@@ -206,6 +205,18 @@ export function resolveMcpToolName(runtime) {
|
|
|
206
205
|
: DEFAULT_MCP_TOOL_NAME;
|
|
207
206
|
}
|
|
208
207
|
|
|
208
|
+
export function resolveMcpTransport(runtime) {
|
|
209
|
+
return typeof runtime?.transport === 'string' && runtime.transport.trim()
|
|
210
|
+
? runtime.transport.trim()
|
|
211
|
+
: DEFAULT_MCP_TRANSPORT;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function describeMcpRuntimeTarget(runtime) {
|
|
215
|
+
return resolveMcpTransport(runtime) === 'streamable_http'
|
|
216
|
+
? runtime?.url || '(unknown)'
|
|
217
|
+
: resolveMcpCommand(runtime).command || '(unknown)';
|
|
218
|
+
}
|
|
219
|
+
|
|
209
220
|
export function extractTurnResultFromMcpToolResult(toolResult) {
|
|
210
221
|
const directCandidates = [
|
|
211
222
|
toolResult?.structuredContent,
|
|
@@ -268,6 +279,58 @@ function buildTransportEnv(env) {
|
|
|
268
279
|
return result;
|
|
269
280
|
}
|
|
270
281
|
|
|
282
|
+
function buildMcpClientTransport({ root, runtime, command, args, logs, onStderr }) {
|
|
283
|
+
if (resolveMcpTransport(runtime) === 'streamable_http') {
|
|
284
|
+
try {
|
|
285
|
+
const requestHeaders = buildRequestHeaders(runtime?.headers);
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
transport: new StreamableHTTPClientTransport(new URL(runtime.url), {
|
|
289
|
+
requestInit: requestHeaders ? { headers: requestHeaders } : undefined,
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
logs.push(`[transport-error] ${error.message}`);
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
error: `Cannot resolve MCP streamable_http runtime: ${error.message}`,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const transport = new StdioClientTransport({
|
|
302
|
+
command,
|
|
303
|
+
args,
|
|
304
|
+
cwd: runtime.cwd ? join(root, runtime.cwd) : root,
|
|
305
|
+
env: buildTransportEnv(process.env),
|
|
306
|
+
stderr: 'pipe',
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (transport.stderr) {
|
|
310
|
+
transport.stderr.on('data', (chunk) => {
|
|
311
|
+
const text = chunk.toString();
|
|
312
|
+
logs.push(`[stderr] ${text}`);
|
|
313
|
+
if (onStderr) onStderr(text);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { ok: true, transport };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildRequestHeaders(headers) {
|
|
321
|
+
if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const result = {};
|
|
326
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
327
|
+
if (typeof value === 'string') {
|
|
328
|
+
result[key] = value;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
271
334
|
function isPlainObject(value) {
|
|
272
335
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
273
336
|
}
|
|
@@ -19,6 +19,7 @@ const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
|
19
19
|
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp'];
|
|
20
20
|
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
21
21
|
const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
22
|
+
const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
|
|
22
23
|
const VALID_PHASES = ['planning', 'implementation', 'qa'];
|
|
23
24
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
24
25
|
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
@@ -47,8 +48,60 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
|
|
|
47
48
|
];
|
|
48
49
|
|
|
49
50
|
function validateMcpRuntime(runtimeId, runtime, errors) {
|
|
51
|
+
const transport = typeof runtime?.transport === 'string' && runtime.transport.trim()
|
|
52
|
+
? runtime.transport.trim()
|
|
53
|
+
: 'stdio';
|
|
50
54
|
const command = runtime?.command;
|
|
51
55
|
|
|
56
|
+
if (!VALID_MCP_TRANSPORTS.includes(transport)) {
|
|
57
|
+
errors.push(`Runtime "${runtimeId}": mcp transport must be one of: ${VALID_MCP_TRANSPORTS.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ('tool_name' in runtime && (typeof runtime.tool_name !== 'string' || !runtime.tool_name.trim())) {
|
|
61
|
+
errors.push(`Runtime "${runtimeId}": mcp tool_name must be a non-empty string`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (transport === 'streamable_http') {
|
|
65
|
+
if (typeof runtime?.url !== 'string' || !runtime.url.trim()) {
|
|
66
|
+
errors.push(`Runtime "${runtimeId}": mcp streamable_http requires "url"`);
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = new URL(runtime.url);
|
|
70
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
71
|
+
errors.push(`Runtime "${runtimeId}": mcp url must use http or https`);
|
|
72
|
+
}
|
|
73
|
+
} catch {
|
|
74
|
+
errors.push(`Runtime "${runtimeId}": mcp url must be a valid absolute URL`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if ('headers' in runtime) {
|
|
79
|
+
if (!runtime.headers || typeof runtime.headers !== 'object' || Array.isArray(runtime.headers)) {
|
|
80
|
+
errors.push(`Runtime "${runtimeId}": mcp headers must be an object of string values`);
|
|
81
|
+
} else {
|
|
82
|
+
for (const [key, value] of Object.entries(runtime.headers)) {
|
|
83
|
+
if (typeof value !== 'string') {
|
|
84
|
+
errors.push(`Runtime "${runtimeId}": mcp headers["${key}"] must be a string`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if ('command' in runtime) {
|
|
91
|
+
errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "command"`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if ('args' in runtime) {
|
|
95
|
+
errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "args"`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if ('cwd' in runtime) {
|
|
99
|
+
errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "cwd"`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
52
105
|
if (typeof command === 'string') {
|
|
53
106
|
if (!command.trim()) {
|
|
54
107
|
errors.push(`Runtime "${runtimeId}": mcp command must be a non-empty string`);
|
|
@@ -67,13 +120,17 @@ function validateMcpRuntime(runtimeId, runtime, errors) {
|
|
|
67
120
|
}
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
if ('tool_name' in runtime && (typeof runtime.tool_name !== 'string' || !runtime.tool_name.trim())) {
|
|
71
|
-
errors.push(`Runtime "${runtimeId}": mcp tool_name must be a non-empty string`);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
123
|
if ('cwd' in runtime && (typeof runtime.cwd !== 'string' || !runtime.cwd.trim())) {
|
|
75
124
|
errors.push(`Runtime "${runtimeId}": mcp cwd must be a non-empty string`);
|
|
76
125
|
}
|
|
126
|
+
|
|
127
|
+
if ('url' in runtime) {
|
|
128
|
+
errors.push(`Runtime "${runtimeId}": mcp stdio does not accept "url"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if ('headers' in runtime) {
|
|
132
|
+
errors.push(`Runtime "${runtimeId}": mcp stdio does not accept "headers"`);
|
|
133
|
+
}
|
|
77
134
|
}
|
|
78
135
|
|
|
79
136
|
function validateApiProxyRetryPolicy(runtimeId, retryPolicy, errors) {
|