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