@zhixuan92/multi-model-agent-mcp 2.8.0 → 2.8.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 +11 -207
- package/dist/cli.js +6 -599
- package/package.json +8 -49
- package/dist/cli.d.ts +0 -79
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/headline.d.ts +0 -25
- package/dist/headline.d.ts.map +0 -1
- package/dist/headline.js +0 -58
- package/dist/headline.js.map +0 -1
- package/dist/http/auth.d.ts +0 -8
- package/dist/http/auth.d.ts.map +0 -1
- package/dist/http/auth.js +0 -43
- package/dist/http/auth.js.map +0 -1
- package/dist/http/cwd-validator.d.ts +0 -11
- package/dist/http/cwd-validator.d.ts.map +0 -1
- package/dist/http/cwd-validator.js +0 -31
- package/dist/http/cwd-validator.js.map +0 -1
- package/dist/http/lifecycle-handlers.d.ts +0 -12
- package/dist/http/lifecycle-handlers.d.ts.map +0 -1
- package/dist/http/lifecycle-handlers.js +0 -87
- package/dist/http/lifecycle-handlers.js.map +0 -1
- package/dist/http/loopback.d.ts +0 -10
- package/dist/http/loopback.d.ts.map +0 -1
- package/dist/http/loopback.js +0 -34
- package/dist/http/loopback.js.map +0 -1
- package/dist/http/project-registry.d.ts +0 -48
- package/dist/http/project-registry.d.ts.map +0 -1
- package/dist/http/project-registry.js +0 -119
- package/dist/http/project-registry.js.map +0 -1
- package/dist/http/session-router.d.ts +0 -33
- package/dist/http/session-router.d.ts.map +0 -1
- package/dist/http/session-router.js +0 -62
- package/dist/http/session-router.js.map +0 -1
- package/dist/http/status-endpoint.d.ts +0 -20
- package/dist/http/status-endpoint.d.ts.map +0 -1
- package/dist/http/status-endpoint.js +0 -85
- package/dist/http/status-endpoint.js.map +0 -1
- package/dist/http/transport.d.ts +0 -14
- package/dist/http/transport.d.ts.map +0 -1
- package/dist/http/transport.js +0 -209
- package/dist/http/transport.js.map +0 -1
- package/dist/index.d.ts +0 -4
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/dist/routing/render-provider-routing-matrix.d.ts +0 -7
- package/dist/routing/render-provider-routing-matrix.d.ts.map +0 -1
- package/dist/routing/render-provider-routing-matrix.js +0 -153
- package/dist/routing/render-provider-routing-matrix.js.map +0 -1
- package/dist/status-cli.d.ts +0 -2
- package/dist/status-cli.d.ts.map +0 -1
- package/dist/status-cli.js +0 -68
- package/dist/status-cli.js.map +0 -1
- package/dist/tools/audit-document.d.ts +0 -23
- package/dist/tools/audit-document.d.ts.map +0 -1
- package/dist/tools/audit-document.js +0 -123
- package/dist/tools/audit-document.js.map +0 -1
- package/dist/tools/batch-response.d.ts +0 -14
- package/dist/tools/batch-response.d.ts.map +0 -1
- package/dist/tools/batch-response.js +0 -42
- package/dist/tools/batch-response.js.map +0 -1
- package/dist/tools/confirm-clarifications.d.ts +0 -15
- package/dist/tools/confirm-clarifications.d.ts.map +0 -1
- package/dist/tools/confirm-clarifications.js +0 -95
- package/dist/tools/confirm-clarifications.js.map +0 -1
- package/dist/tools/debug-task.d.ts +0 -13
- package/dist/tools/debug-task.d.ts.map +0 -1
- package/dist/tools/debug-task.js +0 -63
- package/dist/tools/debug-task.js.map +0 -1
- package/dist/tools/execute-plan.d.ts +0 -12
- package/dist/tools/execute-plan.d.ts.map +0 -1
- package/dist/tools/execute-plan.js +0 -123
- package/dist/tools/execute-plan.js.map +0 -1
- package/dist/tools/review-code.d.ts +0 -17
- package/dist/tools/review-code.d.ts.map +0 -1
- package/dist/tools/review-code.js +0 -108
- package/dist/tools/review-code.js.map +0 -1
- package/dist/tools/shared.d.ts +0 -72
- package/dist/tools/shared.d.ts.map +0 -1
- package/dist/tools/shared.js +0 -160
- package/dist/tools/shared.js.map +0 -1
- package/dist/tools/truncation.d.ts +0 -18
- package/dist/tools/truncation.d.ts.map +0 -1
- package/dist/tools/truncation.js +0 -62
- package/dist/tools/truncation.js.map +0 -1
- package/dist/tools/verify-work.d.ts +0 -12
- package/dist/tools/verify-work.d.ts.map +0 -1
- package/dist/tools/verify-work.js +0 -85
- package/dist/tools/verify-work.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,600 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
9
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
10
|
-
import { z } from 'zod';
|
|
11
|
-
import { loadConfigFromFile } from '@zhixuan92/multi-model-agent-core/config/load';
|
|
12
|
-
import { parseConfig } from '@zhixuan92/multi-model-agent-core/config/schema';
|
|
13
|
-
import { runTasks } from '@zhixuan92/multi-model-agent-core/run-tasks';
|
|
14
|
-
import { createDiagnosticLogger, createProjectContext } from '@zhixuan92/multi-model-agent-core';
|
|
15
|
-
import { renderProviderRoutingMatrix } from './routing/render-provider-routing-matrix.js';
|
|
16
|
-
import { computeTimings, computeBatchProgress, computeAggregateCost, } from './tools/batch-response.js';
|
|
17
|
-
import { buildUnifiedResponse, withDiagnostics } from './tools/shared.js';
|
|
18
|
-
import { truncateResults } from './tools/truncation.js';
|
|
19
|
-
import { registerAuditDocument } from './tools/audit-document.js';
|
|
20
|
-
import { registerDebugTask } from './tools/debug-task.js';
|
|
21
|
-
import { registerExecutePlan } from './tools/execute-plan.js';
|
|
22
|
-
import { registerReviewCode } from './tools/review-code.js';
|
|
23
|
-
import { registerVerifyWork } from './tools/verify-work.js';
|
|
24
|
-
import { compileDelegateTasks } from '@zhixuan92/multi-model-agent-core/intake/compilers/delegate';
|
|
25
|
-
import { runIntakePipeline } from '@zhixuan92/multi-model-agent-core/intake/pipeline';
|
|
26
|
-
import { registerConfirmClarifications } from './tools/confirm-clarifications.js';
|
|
27
|
-
export { computeTimings, computeBatchProgress, computeAggregateCost } from './tools/batch-response.js';
|
|
28
|
-
export const SERVER_NAME = 'multi-model-agent';
|
|
29
|
-
export const ASSISTANT_MODEL_NAME = 'GPT-5';
|
|
30
|
-
const DEFAULT_LARGE_RESPONSE_THRESHOLD_CHARS = 65_536;
|
|
31
|
-
export function buildCliGreeting() {
|
|
32
|
-
return `Hi! I'm ${ASSISTANT_MODEL_NAME}, your friendly multi-model agent assistant.`;
|
|
33
|
-
}
|
|
34
|
-
function parsePositiveInt(s) {
|
|
35
|
-
if (!s)
|
|
36
|
-
return undefined;
|
|
37
|
-
const n = Number.parseInt(s, 10);
|
|
38
|
-
if (Number.isFinite(n) && n > 0 && String(n) === s.trim())
|
|
39
|
-
return n;
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
// Read the version from package.json at module load so the MCP server
|
|
43
|
-
// metadata (and tests that assert against it) stays in lockstep with the
|
|
44
|
-
// published npm package version. `createRequire` keeps the JSON read
|
|
45
|
-
// outside tsc's `rootDir: src` constraint and avoids the `with { type:
|
|
46
|
-
// 'json' }` import attribute (which would force us to commit to a
|
|
47
|
-
// specific TS/Node module-resolution combination). The relative path is
|
|
48
|
-
// resolved from the compiled `dist/cli.js` — that sits one level below
|
|
49
|
-
// `packages/mcp/package.json`.
|
|
50
|
-
const packageRequire = createRequire(import.meta.url);
|
|
51
|
-
const pkg = packageRequire('../package.json');
|
|
52
|
-
export const SERVER_VERSION = pkg.version;
|
|
53
|
-
export function buildTaskSchema(availableAgents) {
|
|
54
|
-
return z.object({
|
|
55
|
-
prompt: z.string().describe('The task instruction. Required.'),
|
|
56
|
-
agentType: z.enum(availableAgents).optional().describe('How hard the task is. Default: standard (cost-effective). Set to complex for harder reasoning or ambiguous scope.'),
|
|
57
|
-
filePaths: z.array(z.string()).optional().describe('Files the sub-agent should focus on. Existing files are pre-verified. Non-existent paths are treated as output targets.'),
|
|
58
|
-
done: z.string().optional().describe('Acceptance criteria in plain language. The worker works toward this goal. The reviewer verifies it.'),
|
|
59
|
-
contextBlockIds: z.array(z.string()).optional().describe('IDs from register_context_block to prepend to prompt.'),
|
|
60
|
-
}).strict();
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Batch cache for `retry_tasks`. Every `delegate_tasks` call stashes the
|
|
64
|
-
* original `TaskSpec[]` under a UUID so the caller can later ask us to
|
|
65
|
-
* re-dispatch specific indices without re-transmitting the briefs. Two
|
|
66
|
-
* bounds (enforced by `BatchCache` in core):
|
|
67
|
-
*
|
|
68
|
-
* - TTL (30 min from creation): keeps stale batches from lingering
|
|
69
|
-
* through a long session. TTL is from-creation (not from-last-access).
|
|
70
|
-
* - LRU cap (100 entries): prevents unbounded growth from a chatty
|
|
71
|
-
* caller that never retries.
|
|
72
|
-
*
|
|
73
|
-
* The `BatchCache` instance lives on `projectContext.batchCache` so it is
|
|
74
|
-
* shared across all sessions attached to the same project root.
|
|
75
|
-
*/
|
|
76
|
-
export function buildMcpServer(config, logger, options) {
|
|
77
|
-
const { projectContext } = options;
|
|
78
|
-
const agentKeys = config.agents ? Object.keys(config.agents) : [];
|
|
79
|
-
if (agentKeys.length === 0) {
|
|
80
|
-
throw new Error('buildMcpServer requires at least one configured agent.');
|
|
81
|
-
}
|
|
82
|
-
// Resolve the threshold once at server startup
|
|
83
|
-
const envThreshold = parsePositiveInt(process.env.MULTI_MODEL_LARGE_RESPONSE_THRESHOLD_CHARS);
|
|
84
|
-
if (process.env.MULTI_MODEL_LARGE_RESPONSE_THRESHOLD_CHARS !== undefined && envThreshold === undefined) {
|
|
85
|
-
process.stderr.write(`[multi-model-agent] warning: MULTI_MODEL_LARGE_RESPONSE_THRESHOLD_CHARS=${process.env.MULTI_MODEL_LARGE_RESPONSE_THRESHOLD_CHARS} is not a positive integer, ignoring\n`);
|
|
86
|
-
}
|
|
87
|
-
const resolvedThreshold = envThreshold
|
|
88
|
-
?? config.defaults.largeResponseThresholdChars
|
|
89
|
-
?? options.largeResponseThresholdChars
|
|
90
|
-
?? DEFAULT_LARGE_RESPONSE_THRESHOLD_CHARS;
|
|
91
|
-
const runTasksImpl = options._testRunTasksOverride ?? runTasks;
|
|
92
|
-
// Resolve parentModel once: env var > config > undefined
|
|
93
|
-
const resolvedParentModel = process.env.PARENT_MODEL_NAME || config.defaults.parentModel || undefined;
|
|
94
|
-
function injectDefaults(tasks) {
|
|
95
|
-
return tasks.map(t => ({
|
|
96
|
-
...t,
|
|
97
|
-
agentType: t.agentType,
|
|
98
|
-
tools: config.defaults.tools,
|
|
99
|
-
timeoutMs: config.defaults.timeoutMs,
|
|
100
|
-
maxCostUSD: config.defaults.maxCostUSD,
|
|
101
|
-
sandboxPolicy: config.defaults.sandboxPolicy,
|
|
102
|
-
cwd: projectContext.cwd,
|
|
103
|
-
reviewPolicy: 'full',
|
|
104
|
-
effort: undefined,
|
|
105
|
-
parentModel: resolvedParentModel,
|
|
106
|
-
autoCommit: true,
|
|
107
|
-
}));
|
|
108
|
-
}
|
|
109
|
-
const server = new McpServer({
|
|
110
|
-
name: SERVER_NAME,
|
|
111
|
-
version: SERVER_VERSION,
|
|
112
|
-
});
|
|
113
|
-
// Stores sourced from projectContext — shared across all sessions for this project.
|
|
114
|
-
const contextBlockStore = projectContext.contextBlocks;
|
|
115
|
-
const clarificationStore = projectContext.clarifications;
|
|
116
|
-
const batchCache = projectContext.batchCache;
|
|
117
|
-
const availableAgents = agentKeys;
|
|
118
|
-
server.tool('delegate_tasks', 'General-purpose task dispatch — use only when no specialized route fits. ' +
|
|
119
|
-
'Try specialized tools first: audit_document (auditing), review_code (reviewing), verify_work (verifying), debug_task (debugging), execute_plan (implementing from a written plan/spec file on disk). ' +
|
|
120
|
-
'Use delegate_tasks for ad-hoc implementation, research, or any work that has no plan file and no specialized route.\n\n' +
|
|
121
|
-
'Minimum: { prompt }. Everything else has good defaults. ' +
|
|
122
|
-
'Set filePaths whenever the task targets specific files. Set done whenever you have explicit acceptance criteria (required). ' +
|
|
123
|
-
'Do not invent extra fields such as inputs or done_condition; put extra context in prompt and use only the public schema fields.\n\n' +
|
|
124
|
-
renderProviderRoutingMatrix(config), {
|
|
125
|
-
tasks: z.array(buildTaskSchema(availableAgents)).describe('Array of tasks to execute in parallel'),
|
|
126
|
-
}, withDiagnostics('delegate_tasks', logger, async ({ tasks }, extra) => {
|
|
127
|
-
const rawToken = extra._meta?.progressToken;
|
|
128
|
-
const progressToken = typeof rawToken === 'string' || typeof rawToken === 'number'
|
|
129
|
-
? rawToken
|
|
130
|
-
: undefined;
|
|
131
|
-
let progressCounter = 0;
|
|
132
|
-
const sendProgress = progressToken !== undefined
|
|
133
|
-
? (taskIndex, event) => {
|
|
134
|
-
progressCounter += 1;
|
|
135
|
-
const headline = `[task ${taskIndex}] ${event.headline}`;
|
|
136
|
-
void extra.sendNotification({
|
|
137
|
-
method: 'notifications/progress',
|
|
138
|
-
params: {
|
|
139
|
-
progressToken,
|
|
140
|
-
progress: progressCounter,
|
|
141
|
-
message: headline,
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
: undefined;
|
|
146
|
-
// Intake pipeline: compile → infer → classify → resolve
|
|
147
|
-
const requestId = randomUUID();
|
|
148
|
-
const drafts = compileDelegateTasks(tasks, requestId);
|
|
149
|
-
const intakeResult = runIntakePipeline(drafts, config, contextBlockStore);
|
|
150
|
-
// Execute ready tasks through normal dispatch
|
|
151
|
-
let results = [];
|
|
152
|
-
const readySpecs = intakeResult.ready.map(r => r.task);
|
|
153
|
-
const batchId = batchCache.remember(readySpecs.length > 0 ? readySpecs : tasks);
|
|
154
|
-
const batchStartMs = Date.now();
|
|
155
|
-
let batchAborted = false;
|
|
156
|
-
try {
|
|
157
|
-
if (readySpecs.length > 0) {
|
|
158
|
-
const resolvedTasks = injectDefaults(readySpecs);
|
|
159
|
-
results = await runTasksImpl(resolvedTasks, config, {
|
|
160
|
-
onProgress: sendProgress,
|
|
161
|
-
runtime: { contextBlockStore },
|
|
162
|
-
});
|
|
163
|
-
intakeResult.intakeProgress.executedDrafts = results.length;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
catch (err) {
|
|
167
|
-
batchAborted = true;
|
|
168
|
-
throw err;
|
|
169
|
-
}
|
|
170
|
-
finally {
|
|
171
|
-
if (batchAborted) {
|
|
172
|
-
try {
|
|
173
|
-
batchCache.abort(batchId);
|
|
174
|
-
}
|
|
175
|
-
catch { /* already terminal */ }
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
try {
|
|
179
|
-
batchCache.complete(batchId, results);
|
|
180
|
-
}
|
|
181
|
-
catch { /* already terminal */ }
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const wallClockMs = Date.now() - batchStartMs;
|
|
185
|
-
// Create clarification set if needed
|
|
186
|
-
let clarificationId;
|
|
187
|
-
if (intakeResult.clarifications.length > 0) {
|
|
188
|
-
const storedDrafts = intakeResult.clarifications.map(c => ({
|
|
189
|
-
draft: drafts.find(d => d.draftId === c.draftId),
|
|
190
|
-
taskIndex: c.taskIndex,
|
|
191
|
-
roundCount: 0,
|
|
192
|
-
}));
|
|
193
|
-
clarificationId = clarificationStore.create(storedDrafts, batchId);
|
|
194
|
-
}
|
|
195
|
-
// Apply auto-escape truncation
|
|
196
|
-
const truncatedResults = truncateResults(results.map(r => ({ status: r.status, output: r.output, filesWritten: r.filesWritten, error: r.error })), batchId, resolvedThreshold);
|
|
197
|
-
return buildUnifiedResponse({
|
|
198
|
-
batchId,
|
|
199
|
-
results: results.map((r, i) => ({ ...r, output: truncatedResults[i].output })),
|
|
200
|
-
tasks: readySpecs,
|
|
201
|
-
wallClockMs,
|
|
202
|
-
parentModel: resolvedParentModel,
|
|
203
|
-
clarificationId,
|
|
204
|
-
clarifications: intakeResult.clarifications.length > 0 ? intakeResult.clarifications : undefined,
|
|
205
|
-
});
|
|
206
|
-
}));
|
|
207
|
-
server.tool('register_context_block', 'Store a reusable content block for later delegate_tasks calls. Returns a block id.\n\n' +
|
|
208
|
-
'When this saves money:\n' +
|
|
209
|
-
'- You\'re dispatching 3+ tasks that all need the same file or spec as context\n' +
|
|
210
|
-
'- You\'re doing multiple rounds of review/audit on the same document\n' +
|
|
211
|
-
'- Your shared context is >2K tokens (below that, duplication cost is negligible)\n\n' +
|
|
212
|
-
'Common patterns:\n' +
|
|
213
|
-
' Delta audit — Register round 1\'s audit report, then dispatch round 2 via\n' +
|
|
214
|
-
' delegate_tasks with contextBlockIds + a prompt like "Only report new findings\n' +
|
|
215
|
-
' not in the prior report, findings not fixed, and confirm which were fixed."\n' +
|
|
216
|
-
' This cuts audit cost roughly in half on subsequent rounds.\n\n' +
|
|
217
|
-
' Diff-scoped review — Register the git diff output, then dispatch review via\n' +
|
|
218
|
-
' delegate_tasks with contextBlockIds + a prompt like "Review only the changes\n' +
|
|
219
|
-
' in the diff, not the entire file." Focuses the reviewer on what changed.\n\n' +
|
|
220
|
-
' Shared spec — Register a spec/plan once, reference it from multiple parallel\n' +
|
|
221
|
-
' tasks. 3 tasks × 25K tokens = 75K transmitted; with a context block, ~25K total.\n\n' +
|
|
222
|
-
'Example workflow:\n' +
|
|
223
|
-
' 1. register_context_block({ content: <spec file contents> }) -> { id: "abc123" }\n' +
|
|
224
|
-
' 2. delegate_tasks({ tasks: [\n' +
|
|
225
|
-
' { prompt: "Review section 1", contextBlockIds: ["abc123"] },\n' +
|
|
226
|
-
' { prompt: "Review section 2", contextBlockIds: ["abc123"] },\n' +
|
|
227
|
-
' { prompt: "Review section 3", contextBlockIds: ["abc123"] }\n' +
|
|
228
|
-
' ]})\n' +
|
|
229
|
-
' -> The spec is transmitted once to the server, not three times.\n\n' +
|
|
230
|
-
'Blocks live in an in-memory store with a 30-minute TTL and 100-entry LRU cap.\n' +
|
|
231
|
-
'If a block expires before use, delegate_tasks returns an error identifying the missing id.', {
|
|
232
|
-
id: z.string().optional().describe('Optional id; auto-generated UUID if omitted'),
|
|
233
|
-
content: z.string().describe('The content to store'),
|
|
234
|
-
}, async ({ id, content }) => {
|
|
235
|
-
const result = contextBlockStore.register(content, { id });
|
|
236
|
-
return {
|
|
237
|
-
content: [{ type: 'text', text: JSON.stringify({ contextBlockId: result.id }, null, 2) }],
|
|
238
|
-
};
|
|
239
|
-
});
|
|
240
|
-
server.tool('retry_tasks', 'Re-run specific tasks from a previous delegate_tasks batch.\n\n' +
|
|
241
|
-
'When to use:\n' +
|
|
242
|
-
'- A task returned \'incomplete\' but you believe a retry will succeed\n' +
|
|
243
|
-
' (e.g., after fixing a file the task depends on, or after a parallel conflict is resolved)\n' +
|
|
244
|
-
'- You want to re-run a subset of a batch without re-transmitting prompts and context blocks\n\n' +
|
|
245
|
-
'When NOT to use (re-dispatch via delegate_tasks instead):\n' +
|
|
246
|
-
'- You need to change the task prompt, tools, effort, or limits\n' +
|
|
247
|
-
'- The original batch is older than 30 minutes (cache TTL)\n' +
|
|
248
|
-
'- You want to try a different provider or agent type\n\n' +
|
|
249
|
-
'Pass the batchId returned by delegate_tasks and an array of 0-based task indices.\n' +
|
|
250
|
-
'Batches live in an in-memory cache with a 30-minute TTL and 100-entry LRU cap.', {
|
|
251
|
-
batchId: z.string().describe('Batch id returned from a previous delegate_tasks call'),
|
|
252
|
-
taskIndices: z
|
|
253
|
-
.array(z.number().int().nonnegative())
|
|
254
|
-
.describe('Zero-based indices (into the original batch) of the tasks to re-run'),
|
|
255
|
-
}, async ({ batchId, taskIndices }) => {
|
|
256
|
-
const batch = batchCache.get(batchId);
|
|
257
|
-
if (!batch) {
|
|
258
|
-
throw new Error(`batch "${batchId}" is unknown or expired — re-dispatch with full task specs via delegate_tasks`);
|
|
259
|
-
}
|
|
260
|
-
// Mark this batch as recently used so the LRU eviction does not
|
|
261
|
-
// drop a hot entry when newer batches arrive. Does NOT refresh TTL.
|
|
262
|
-
batchCache.touch(batchId);
|
|
263
|
-
for (const i of taskIndices) {
|
|
264
|
-
if (i < 0 || i >= batch.tasks.length) {
|
|
265
|
-
throw new Error(`index ${i} is out of range for batch ${batchId} (size ${batch.tasks.length})`);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const subset = taskIndices.map((i) => batch.tasks[i]);
|
|
269
|
-
// Create a fresh batch for the retried tasks so the original batch
|
|
270
|
-
// entry is preserved and get_batch_slice can still retrieve it.
|
|
271
|
-
const retryBatchId = batchCache.remember(subset);
|
|
272
|
-
const batchStartMs = Date.now();
|
|
273
|
-
let results = [];
|
|
274
|
-
let retryAborted = false;
|
|
275
|
-
try {
|
|
276
|
-
results = await runTasksImpl(injectDefaults(subset), config, {
|
|
277
|
-
runtime: { contextBlockStore },
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
catch (err) {
|
|
281
|
-
retryAborted = true;
|
|
282
|
-
throw err;
|
|
283
|
-
}
|
|
284
|
-
finally {
|
|
285
|
-
if (retryAborted) {
|
|
286
|
-
try {
|
|
287
|
-
batchCache.abort(retryBatchId);
|
|
288
|
-
}
|
|
289
|
-
catch { /* already terminal */ }
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
try {
|
|
293
|
-
batchCache.complete(retryBatchId, results);
|
|
294
|
-
}
|
|
295
|
-
catch { /* already terminal */ }
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
const wallClockMs = Date.now() - batchStartMs;
|
|
299
|
-
// Apply auto-escape truncation
|
|
300
|
-
const truncatedResults = truncateResults(results.map(r => ({ status: r.status, output: r.output, filesWritten: r.filesWritten, error: r.error })), retryBatchId, resolvedThreshold);
|
|
301
|
-
return buildUnifiedResponse({
|
|
302
|
-
batchId: retryBatchId,
|
|
303
|
-
results: results.map((r, i) => ({ ...r, output: truncatedResults[i].output })),
|
|
304
|
-
tasks: subset,
|
|
305
|
-
wallClockMs,
|
|
306
|
-
parentModel: resolvedParentModel,
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
server.tool('get_batch_slice', `Retrieve full telemetry and output data from a previous delegate_tasks batch.
|
|
310
|
-
|
|
311
|
-
Returns the complete batch with timings, progress, cost breakdown, and all task results.
|
|
312
|
-
Optionally filter to a single task via taskIndex.
|
|
313
|
-
|
|
314
|
-
Batches are cached in memory per MCP server instance with a 30-minute TTL from creation
|
|
315
|
-
and a 100-entry LRU cap. Access touches the LRU order but does not refresh TTL. If the
|
|
316
|
-
batch is expired or evicted, re-dispatch via delegate_tasks with the full specs.`, {
|
|
317
|
-
batchId: z.string().describe('Batch ID from a prior delegate_tasks or retry_tasks response'),
|
|
318
|
-
taskIndex: z.number().int().min(0).optional().describe('0-based task index. Omit for all tasks.'),
|
|
319
|
-
}, async ({ batchId, taskIndex }) => {
|
|
320
|
-
const entry = batchCache.get(batchId);
|
|
321
|
-
if (!entry) {
|
|
322
|
-
return {
|
|
323
|
-
content: [{
|
|
324
|
-
type: 'text',
|
|
325
|
-
text: `Batch "${batchId}" is unknown or expired. Batch results are cached for 30 minutes after completion. Re-dispatch the original task to get fresh results.`,
|
|
326
|
-
}],
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
batchCache.touch(batchId);
|
|
330
|
-
if (!entry.results) {
|
|
331
|
-
return {
|
|
332
|
-
content: [{
|
|
333
|
-
type: 'text',
|
|
334
|
-
text: `Batch "${batchId}" has no results yet — the original dispatch may still be running.`,
|
|
335
|
-
}],
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
if (taskIndex !== undefined && (taskIndex < 0 || taskIndex >= entry.results.length)) {
|
|
339
|
-
return {
|
|
340
|
-
content: [{
|
|
341
|
-
type: 'text',
|
|
342
|
-
text: `taskIndex ${taskIndex} is out of range. Batch "${batchId}" has ${entry.results.length} tasks (0-based index: 0 to ${entry.results.length - 1}).`,
|
|
343
|
-
}],
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
const results = taskIndex !== undefined
|
|
347
|
-
? [entry.results[taskIndex]]
|
|
348
|
-
: entry.results;
|
|
349
|
-
const wallClockMs = Math.max(0, ...entry.results.map((r) => r.durationMs ?? 0));
|
|
350
|
-
const timings = computeTimings(wallClockMs, entry.results);
|
|
351
|
-
const batchProgress = computeBatchProgress(entry.results);
|
|
352
|
-
const aggregateCost = computeAggregateCost(entry.results);
|
|
353
|
-
return {
|
|
354
|
-
content: [{
|
|
355
|
-
type: 'text',
|
|
356
|
-
text: JSON.stringify({
|
|
357
|
-
batchId,
|
|
358
|
-
timings,
|
|
359
|
-
batchProgress,
|
|
360
|
-
aggregateCost,
|
|
361
|
-
results,
|
|
362
|
-
}, null, 2),
|
|
363
|
-
}],
|
|
364
|
-
};
|
|
365
|
-
});
|
|
366
|
-
registerAuditDocument(server, config, logger, contextBlockStore);
|
|
367
|
-
registerDebugTask(server, config, logger, contextBlockStore);
|
|
368
|
-
registerExecutePlan(server, config, logger, contextBlockStore);
|
|
369
|
-
registerReviewCode(server, config, logger, contextBlockStore);
|
|
370
|
-
registerVerifyWork(server, config, logger, contextBlockStore);
|
|
371
|
-
registerConfirmClarifications(server, config, logger, clarificationStore, runTasksImpl, batchCache.remember.bind(batchCache));
|
|
372
|
-
return server;
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* MCP CLI config discovery (owned by MCP, not core):
|
|
376
|
-
* 1. --config <path> argument (explicit)
|
|
377
|
-
* 2. MULTI_MODEL_CONFIG environment variable
|
|
378
|
-
* 3. ~/.multi-model/config.json (default home-directory location)
|
|
379
|
-
*/
|
|
380
|
-
export async function discoverConfig() {
|
|
381
|
-
const args = process.argv.slice(2);
|
|
382
|
-
// 1. Explicit --config
|
|
383
|
-
const configFlagIdx = args.indexOf('--config');
|
|
384
|
-
if (configFlagIdx >= 0 && args[configFlagIdx + 1]) {
|
|
385
|
-
return loadConfigFromFile(args[configFlagIdx + 1]);
|
|
386
|
-
}
|
|
387
|
-
// 2. MULTI_MODEL_CONFIG env var (file path)
|
|
388
|
-
const envPath = process.env.MULTI_MODEL_CONFIG;
|
|
389
|
-
if (envPath) {
|
|
390
|
-
return loadConfigFromFile(envPath);
|
|
391
|
-
}
|
|
392
|
-
// 3. ~/.multi-model/config.json
|
|
393
|
-
const defaultPath = path.join(os.homedir(), '.multi-model', 'config.json');
|
|
394
|
-
if (fs.existsSync(defaultPath)) {
|
|
395
|
-
return loadConfigFromFile(defaultPath);
|
|
396
|
-
}
|
|
397
|
-
// Fallback: empty config with required agents
|
|
398
|
-
return parseConfig({
|
|
399
|
-
agents: {
|
|
400
|
-
standard: { type: 'claude', model: 'claude-sonnet-4-6' },
|
|
401
|
-
complex: { type: 'claude', model: 'claude-sonnet-4-6' },
|
|
402
|
-
},
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
let installedLifecycleHandlers = null;
|
|
406
|
-
/**
|
|
407
|
-
* Install safety nets for the stdio transport lifecycle. The MCP SDK's
|
|
408
|
-
* StdioServerTransport writes every JSON-RPC frame to `process.stdout`
|
|
409
|
-
* but never attaches an error handler to it, so when the Claude Code
|
|
410
|
-
* client closes the read end of our stdout (reconnect, /mcp restart,
|
|
411
|
-
* extension reload, client crash, long-running-call abort) the next
|
|
412
|
-
* write emits an `EPIPE` error with no listener, which Node turns into
|
|
413
|
-
* `uncaughtException` and — absent a handler — terminates the process.
|
|
414
|
-
* That is the observed "MCP dies every ~2 calls" failure mode.
|
|
415
|
-
*
|
|
416
|
-
* Single-install contract: calling this more than once in one process is a
|
|
417
|
-
* programmer error. The healthy-server contract ("one stderr line at startup")
|
|
418
|
-
* covers only the first install. A second call writes a warning to stderr
|
|
419
|
-
* and returns — it does not register duplicate handlers. This warning path is
|
|
420
|
-
* outside the healthy-server contract; in normal operation `main()` is the only
|
|
421
|
-
* caller and is invoked exactly once per process.
|
|
422
|
-
*/
|
|
423
|
-
export function installStdioLifecycleHandlers(logger) {
|
|
424
|
-
if (installedLifecycleHandlers !== null) {
|
|
425
|
-
process.stderr.write('[multi-model-agent] lifecycle handlers already installed; skipping second install\n');
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
const stdoutError = (err) => {
|
|
429
|
-
if (err.code === 'EPIPE') {
|
|
430
|
-
logger.shutdown('stdout_epipe');
|
|
431
|
-
process.exit(0);
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
logger.shutdown('stdout_other_error');
|
|
435
|
-
process.stderr.write(`[multi-model-agent] stdout error: ${err.message}\n`);
|
|
436
|
-
process.exit(1);
|
|
437
|
-
};
|
|
438
|
-
const stdinEnd = () => {
|
|
439
|
-
logger.shutdown('stdin_end');
|
|
440
|
-
process.exit(0);
|
|
441
|
-
};
|
|
442
|
-
const uncaught = (err) => {
|
|
443
|
-
logger.error('uncaughtException', err);
|
|
444
|
-
logger.shutdown('uncaughtException');
|
|
445
|
-
process.stderr.write(`[multi-model-agent] uncaughtException: ${err.stack ?? String(err)}\n`);
|
|
446
|
-
process.exit(1);
|
|
447
|
-
};
|
|
448
|
-
const unhandled = (reason) => {
|
|
449
|
-
logger.error('unhandledRejection', reason);
|
|
450
|
-
const stack = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);
|
|
451
|
-
process.stderr.write(`[multi-model-agent] unhandledRejection: ${stack}\n`);
|
|
452
|
-
logger.shutdown('unhandledRejection');
|
|
453
|
-
process.exit(1);
|
|
454
|
-
};
|
|
455
|
-
const beforeExit = () => {
|
|
456
|
-
logger.shutdown('event_loop_empty');
|
|
457
|
-
};
|
|
458
|
-
const signals = {
|
|
459
|
-
SIGTERM: () => {
|
|
460
|
-
logger.shutdown('SIGTERM');
|
|
461
|
-
process.exit(0);
|
|
462
|
-
},
|
|
463
|
-
SIGINT: () => {
|
|
464
|
-
logger.shutdown('SIGINT');
|
|
465
|
-
process.exit(0);
|
|
466
|
-
},
|
|
467
|
-
SIGPIPE: () => {
|
|
468
|
-
logger.shutdown('SIGPIPE');
|
|
469
|
-
process.exit(1);
|
|
470
|
-
},
|
|
471
|
-
SIGHUP: () => {
|
|
472
|
-
logger.shutdown('SIGHUP');
|
|
473
|
-
process.exit(0);
|
|
474
|
-
},
|
|
475
|
-
SIGABRT: () => {
|
|
476
|
-
logger.shutdown('SIGABRT');
|
|
477
|
-
process.exit(1);
|
|
478
|
-
},
|
|
479
|
-
};
|
|
480
|
-
process.stdout.on('error', stdoutError);
|
|
481
|
-
process.stdin.on('end', stdinEnd);
|
|
482
|
-
process.on('uncaughtException', uncaught);
|
|
483
|
-
process.on('unhandledRejection', unhandled);
|
|
484
|
-
process.on('beforeExit', beforeExit);
|
|
485
|
-
process.on('SIGTERM', signals.SIGTERM);
|
|
486
|
-
process.on('SIGINT', signals.SIGINT);
|
|
487
|
-
process.on('SIGPIPE', signals.SIGPIPE);
|
|
488
|
-
process.on('SIGHUP', signals.SIGHUP);
|
|
489
|
-
process.on('SIGABRT', signals.SIGABRT);
|
|
490
|
-
installedLifecycleHandlers = { stdoutError, stdinEnd, uncaught, unhandled, beforeExit, signals };
|
|
491
|
-
}
|
|
492
|
-
/** Test-only. Not exported from the package public surface. */
|
|
493
|
-
export function __resetStdioLifecycleHandlersForTests() {
|
|
494
|
-
if (installedLifecycleHandlers === null)
|
|
495
|
-
return;
|
|
496
|
-
process.stdout.off('error', installedLifecycleHandlers.stdoutError);
|
|
497
|
-
process.stdin.off('end', installedLifecycleHandlers.stdinEnd);
|
|
498
|
-
process.off('uncaughtException', installedLifecycleHandlers.uncaught);
|
|
499
|
-
process.off('unhandledRejection', installedLifecycleHandlers.unhandled);
|
|
500
|
-
process.off('beforeExit', installedLifecycleHandlers.beforeExit);
|
|
501
|
-
process.off('SIGTERM', installedLifecycleHandlers.signals.SIGTERM);
|
|
502
|
-
process.off('SIGINT', installedLifecycleHandlers.signals.SIGINT);
|
|
503
|
-
process.off('SIGPIPE', installedLifecycleHandlers.signals.SIGPIPE);
|
|
504
|
-
process.off('SIGHUP', installedLifecycleHandlers.signals.SIGHUP);
|
|
505
|
-
process.off('SIGABRT', installedLifecycleHandlers.signals.SIGABRT);
|
|
506
|
-
installedLifecycleHandlers = null;
|
|
507
|
-
}
|
|
508
|
-
export function parseHttpFlags(args) {
|
|
509
|
-
if (!args.includes('--http'))
|
|
510
|
-
return undefined;
|
|
511
|
-
const portIdx = args.indexOf('--port');
|
|
512
|
-
const bindIdx = args.indexOf('--bind');
|
|
513
|
-
let port;
|
|
514
|
-
if (portIdx >= 0 && args[portIdx + 1]) {
|
|
515
|
-
const n = Number.parseInt(args[portIdx + 1], 10);
|
|
516
|
-
if (!Number.isFinite(n) || n <= 0)
|
|
517
|
-
throw new Error(`--port requires a positive integer, got: ${args[portIdx + 1]}`);
|
|
518
|
-
port = n;
|
|
519
|
-
}
|
|
520
|
-
const bind = bindIdx >= 0 ? args[bindIdx + 1] : undefined;
|
|
521
|
-
return { mode: 'http', port, bind };
|
|
522
|
-
}
|
|
523
|
-
async function main() {
|
|
524
|
-
const args = process.argv.slice(2);
|
|
525
|
-
if (args[0] === 'status') {
|
|
526
|
-
const { runStatusCli } = await import('./status-cli.js');
|
|
527
|
-
await runStatusCli(args.slice(1));
|
|
528
|
-
return;
|
|
529
|
-
}
|
|
530
|
-
if (args[0] === '--help' || args[0] === '-h') {
|
|
531
|
-
console.log('Usage: multi-model-agent serve [--http [--port N] [--bind ADDR]] [--config <path>]');
|
|
532
|
-
process.exit(0);
|
|
533
|
-
}
|
|
534
|
-
if (args[0] !== 'serve') {
|
|
535
|
-
console.error('Usage: multi-model-agent serve [--http [--port N] [--bind ADDR]] [--config <path>]');
|
|
536
|
-
process.exit(1);
|
|
537
|
-
}
|
|
538
|
-
const config = await discoverConfig();
|
|
539
|
-
const agentNames = config.agents ? Object.keys(config.agents) : [];
|
|
540
|
-
if (agentNames.length === 0) {
|
|
541
|
-
console.error('No agents configured. Create ~/.multi-model/config.json or pass --config <path>.');
|
|
542
|
-
process.exit(1);
|
|
543
|
-
}
|
|
544
|
-
const httpFlags = parseHttpFlags(args);
|
|
545
|
-
if (httpFlags) {
|
|
546
|
-
const effectiveConfig = {
|
|
547
|
-
...config,
|
|
548
|
-
transport: {
|
|
549
|
-
mode: 'http',
|
|
550
|
-
http: {
|
|
551
|
-
...config.transport.http,
|
|
552
|
-
...(httpFlags.port !== undefined ? { port: httpFlags.port } : {}),
|
|
553
|
-
...(httpFlags.bind !== undefined ? { bind: httpFlags.bind } : {}),
|
|
554
|
-
},
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
const { startHttpDaemon } = await import('./http/transport.js');
|
|
558
|
-
await startHttpDaemon(effectiveConfig);
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (config.transport.mode === 'http') {
|
|
562
|
-
// config says http mode even though CLI flag was absent
|
|
563
|
-
const { startHttpDaemon } = await import('./http/transport.js');
|
|
564
|
-
await startHttpDaemon(config);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
// stdio path — unchanged from today
|
|
568
|
-
const enabled = config.diagnostics?.log ?? false;
|
|
569
|
-
const logDir = config.diagnostics?.logDir;
|
|
570
|
-
const logger = createDiagnosticLogger({ enabled, logDir });
|
|
571
|
-
logger.startup(SERVER_VERSION);
|
|
572
|
-
const diagnosticLogPath = logger.expectedPath();
|
|
573
|
-
if (diagnosticLogPath !== undefined) {
|
|
574
|
-
process.stderr.write(`[multi-model-agent] diagnostic log: ${diagnosticLogPath}\n`);
|
|
575
|
-
}
|
|
576
|
-
installStdioLifecycleHandlers(logger);
|
|
577
|
-
const projectContext = createProjectContext(process.cwd());
|
|
578
|
-
const server = buildMcpServer(config, logger, { projectContext });
|
|
579
|
-
const transport = new StdioServerTransport();
|
|
580
|
-
await server.connect(transport);
|
|
581
|
-
}
|
|
582
|
-
// Only run main when executed directly
|
|
583
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
584
|
-
const isDirectRun = (() => {
|
|
585
|
-
if (!process.argv[1])
|
|
586
|
-
return false;
|
|
587
|
-
try {
|
|
588
|
-
return fs.realpathSync(process.argv[1]) === fs.realpathSync(thisFile);
|
|
589
|
-
}
|
|
590
|
-
catch {
|
|
591
|
-
return false;
|
|
592
|
-
}
|
|
593
|
-
})();
|
|
594
|
-
if (isDirectRun) {
|
|
595
|
-
main().catch((err) => {
|
|
596
|
-
console.error('Fatal:', err);
|
|
597
|
-
process.exit(1);
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
//# sourceMappingURL=cli.js.map
|
|
2
|
+
console.error(`
|
|
3
|
+
@zhixuan92/multi-model-agent-mcp has been replaced by @zhixuan92/multi-model-agent in 3.0.0.
|
|
4
|
+
Install the new package: npm i -g @zhixuan92/multi-model-agent
|
|
5
|
+
See: https://github.com/zhixuan312/multi-model-agent/blob/master/CHANGELOG.md#300
|
|
6
|
+
`);
|
|
7
|
+
process.exit(1);
|