@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.
Files changed (54) hide show
  1. package/README.md +11 -133
  2. package/dist/cli.js +6 -584
  3. package/package.json +8 -48
  4. package/dist/cli.d.ts +0 -58
  5. package/dist/cli.d.ts.map +0 -1
  6. package/dist/cli.js.map +0 -1
  7. package/dist/headline.d.ts +0 -25
  8. package/dist/headline.d.ts.map +0 -1
  9. package/dist/headline.js +0 -58
  10. package/dist/headline.js.map +0 -1
  11. package/dist/index.d.ts +0 -4
  12. package/dist/index.d.ts.map +0 -1
  13. package/dist/index.js +0 -3
  14. package/dist/index.js.map +0 -1
  15. package/dist/routing/render-provider-routing-matrix.d.ts +0 -7
  16. package/dist/routing/render-provider-routing-matrix.d.ts.map +0 -1
  17. package/dist/routing/render-provider-routing-matrix.js +0 -153
  18. package/dist/routing/render-provider-routing-matrix.js.map +0 -1
  19. package/dist/tools/audit-document.d.ts +0 -23
  20. package/dist/tools/audit-document.d.ts.map +0 -1
  21. package/dist/tools/audit-document.js +0 -123
  22. package/dist/tools/audit-document.js.map +0 -1
  23. package/dist/tools/batch-response.d.ts +0 -14
  24. package/dist/tools/batch-response.d.ts.map +0 -1
  25. package/dist/tools/batch-response.js +0 -42
  26. package/dist/tools/batch-response.js.map +0 -1
  27. package/dist/tools/confirm-clarifications.d.ts +0 -15
  28. package/dist/tools/confirm-clarifications.d.ts.map +0 -1
  29. package/dist/tools/confirm-clarifications.js +0 -95
  30. package/dist/tools/confirm-clarifications.js.map +0 -1
  31. package/dist/tools/debug-task.d.ts +0 -13
  32. package/dist/tools/debug-task.d.ts.map +0 -1
  33. package/dist/tools/debug-task.js +0 -63
  34. package/dist/tools/debug-task.js.map +0 -1
  35. package/dist/tools/execute-plan.d.ts +0 -12
  36. package/dist/tools/execute-plan.d.ts.map +0 -1
  37. package/dist/tools/execute-plan.js +0 -123
  38. package/dist/tools/execute-plan.js.map +0 -1
  39. package/dist/tools/review-code.d.ts +0 -17
  40. package/dist/tools/review-code.d.ts.map +0 -1
  41. package/dist/tools/review-code.js +0 -108
  42. package/dist/tools/review-code.js.map +0 -1
  43. package/dist/tools/shared.d.ts +0 -72
  44. package/dist/tools/shared.d.ts.map +0 -1
  45. package/dist/tools/shared.js +0 -160
  46. package/dist/tools/shared.js.map +0 -1
  47. package/dist/tools/truncation.d.ts +0 -18
  48. package/dist/tools/truncation.d.ts.map +0 -1
  49. package/dist/tools/truncation.js +0 -62
  50. package/dist/tools/truncation.js.map +0 -1
  51. package/dist/tools/verify-work.d.ts +0 -12
  52. package/dist/tools/verify-work.d.ts.map +0 -1
  53. package/dist/tools/verify-work.js +0 -85
  54. 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
- import fs from 'fs';
3
- import path from 'path';
4
- import os from 'os';
5
- import { randomUUID } from 'node:crypto';
6
- import { createRequire } from 'node:module';
7
- import { fileURLToPath } from 'url';
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.7.5",
3
+ "version": "2.8.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "description": "MCP server for multi-model-agent. Exposes a delegate_tasks tool that routes work to Claude, Codex, or OpenAI-compatible sub-agents based on capability, quality tier, and cost.",
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
- "mcp",
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": "./dist/cli.js"
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": "tsc && chmod +x dist/cli.js",
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": ">=22.0.0"
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
+ }