@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.
Files changed (90) hide show
  1. package/README.md +11 -207
  2. package/dist/cli.js +6 -599
  3. package/package.json +8 -49
  4. package/dist/cli.d.ts +0 -79
  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/http/auth.d.ts +0 -8
  12. package/dist/http/auth.d.ts.map +0 -1
  13. package/dist/http/auth.js +0 -43
  14. package/dist/http/auth.js.map +0 -1
  15. package/dist/http/cwd-validator.d.ts +0 -11
  16. package/dist/http/cwd-validator.d.ts.map +0 -1
  17. package/dist/http/cwd-validator.js +0 -31
  18. package/dist/http/cwd-validator.js.map +0 -1
  19. package/dist/http/lifecycle-handlers.d.ts +0 -12
  20. package/dist/http/lifecycle-handlers.d.ts.map +0 -1
  21. package/dist/http/lifecycle-handlers.js +0 -87
  22. package/dist/http/lifecycle-handlers.js.map +0 -1
  23. package/dist/http/loopback.d.ts +0 -10
  24. package/dist/http/loopback.d.ts.map +0 -1
  25. package/dist/http/loopback.js +0 -34
  26. package/dist/http/loopback.js.map +0 -1
  27. package/dist/http/project-registry.d.ts +0 -48
  28. package/dist/http/project-registry.d.ts.map +0 -1
  29. package/dist/http/project-registry.js +0 -119
  30. package/dist/http/project-registry.js.map +0 -1
  31. package/dist/http/session-router.d.ts +0 -33
  32. package/dist/http/session-router.d.ts.map +0 -1
  33. package/dist/http/session-router.js +0 -62
  34. package/dist/http/session-router.js.map +0 -1
  35. package/dist/http/status-endpoint.d.ts +0 -20
  36. package/dist/http/status-endpoint.d.ts.map +0 -1
  37. package/dist/http/status-endpoint.js +0 -85
  38. package/dist/http/status-endpoint.js.map +0 -1
  39. package/dist/http/transport.d.ts +0 -14
  40. package/dist/http/transport.d.ts.map +0 -1
  41. package/dist/http/transport.js +0 -209
  42. package/dist/http/transport.js.map +0 -1
  43. package/dist/index.d.ts +0 -4
  44. package/dist/index.d.ts.map +0 -1
  45. package/dist/index.js +0 -3
  46. package/dist/index.js.map +0 -1
  47. package/dist/routing/render-provider-routing-matrix.d.ts +0 -7
  48. package/dist/routing/render-provider-routing-matrix.d.ts.map +0 -1
  49. package/dist/routing/render-provider-routing-matrix.js +0 -153
  50. package/dist/routing/render-provider-routing-matrix.js.map +0 -1
  51. package/dist/status-cli.d.ts +0 -2
  52. package/dist/status-cli.d.ts.map +0 -1
  53. package/dist/status-cli.js +0 -68
  54. package/dist/status-cli.js.map +0 -1
  55. package/dist/tools/audit-document.d.ts +0 -23
  56. package/dist/tools/audit-document.d.ts.map +0 -1
  57. package/dist/tools/audit-document.js +0 -123
  58. package/dist/tools/audit-document.js.map +0 -1
  59. package/dist/tools/batch-response.d.ts +0 -14
  60. package/dist/tools/batch-response.d.ts.map +0 -1
  61. package/dist/tools/batch-response.js +0 -42
  62. package/dist/tools/batch-response.js.map +0 -1
  63. package/dist/tools/confirm-clarifications.d.ts +0 -15
  64. package/dist/tools/confirm-clarifications.d.ts.map +0 -1
  65. package/dist/tools/confirm-clarifications.js +0 -95
  66. package/dist/tools/confirm-clarifications.js.map +0 -1
  67. package/dist/tools/debug-task.d.ts +0 -13
  68. package/dist/tools/debug-task.d.ts.map +0 -1
  69. package/dist/tools/debug-task.js +0 -63
  70. package/dist/tools/debug-task.js.map +0 -1
  71. package/dist/tools/execute-plan.d.ts +0 -12
  72. package/dist/tools/execute-plan.d.ts.map +0 -1
  73. package/dist/tools/execute-plan.js +0 -123
  74. package/dist/tools/execute-plan.js.map +0 -1
  75. package/dist/tools/review-code.d.ts +0 -17
  76. package/dist/tools/review-code.d.ts.map +0 -1
  77. package/dist/tools/review-code.js +0 -108
  78. package/dist/tools/review-code.js.map +0 -1
  79. package/dist/tools/shared.d.ts +0 -72
  80. package/dist/tools/shared.d.ts.map +0 -1
  81. package/dist/tools/shared.js +0 -160
  82. package/dist/tools/shared.js.map +0 -1
  83. package/dist/tools/truncation.d.ts +0 -18
  84. package/dist/tools/truncation.d.ts.map +0 -1
  85. package/dist/tools/truncation.js +0 -62
  86. package/dist/tools/truncation.js.map +0 -1
  87. package/dist/tools/verify-work.d.ts +0 -12
  88. package/dist/tools/verify-work.d.ts.map +0 -1
  89. package/dist/tools/verify-work.js +0 -85
  90. 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
- 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 { 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);