lv24-opencode-consilium 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +499 -3
  3. package/README.ru.md +500 -0
  4. package/README.zh.md +188 -0
  5. package/bin/cli.js +19 -0
  6. package/bin/mcp.js +24 -0
  7. package/consilium.config.example.json +84 -0
  8. package/dist/agents.d.ts +79 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +1051 -0
  11. package/dist/agents.js.map +1 -0
  12. package/dist/artifacts.d.ts +75 -0
  13. package/dist/artifacts.d.ts.map +1 -0
  14. package/dist/artifacts.js +464 -0
  15. package/dist/artifacts.js.map +1 -0
  16. package/dist/cli.d.ts +12 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +1392 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/compat.d.ts +160 -0
  21. package/dist/compat.d.ts.map +1 -0
  22. package/dist/compat.js +419 -0
  23. package/dist/compat.js.map +1 -0
  24. package/dist/config.d.ts +112 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +728 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consensus.d.ts +73 -0
  29. package/dist/consensus.d.ts.map +1 -0
  30. package/dist/consensus.js +273 -0
  31. package/dist/consensus.js.map +1 -0
  32. package/dist/defaults.d.ts +34 -0
  33. package/dist/defaults.d.ts.map +1 -0
  34. package/dist/defaults.js +102 -0
  35. package/dist/defaults.js.map +1 -0
  36. package/dist/doctor.d.ts +27 -0
  37. package/dist/doctor.d.ts.map +1 -0
  38. package/dist/doctor.js +798 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/fallback.d.ts +50 -0
  41. package/dist/fallback.d.ts.map +1 -0
  42. package/dist/fallback.js +134 -0
  43. package/dist/fallback.js.map +1 -0
  44. package/dist/git.d.ts +28 -0
  45. package/dist/git.d.ts.map +1 -0
  46. package/dist/git.js +71 -0
  47. package/dist/git.js.map +1 -0
  48. package/dist/index.d.ts +100 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +653 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/mcp.d.ts +20 -0
  53. package/dist/mcp.d.ts.map +1 -0
  54. package/dist/mcp.js +1215 -0
  55. package/dist/mcp.js.map +1 -0
  56. package/dist/models.d.ts +97 -0
  57. package/dist/models.d.ts.map +1 -0
  58. package/dist/models.js +873 -0
  59. package/dist/models.js.map +1 -0
  60. package/dist/plugin.d.ts +59 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +223 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/presets.d.ts +27 -0
  65. package/dist/presets.d.ts.map +1 -0
  66. package/dist/presets.js +86 -0
  67. package/dist/presets.js.map +1 -0
  68. package/dist/replay.d.ts +37 -0
  69. package/dist/replay.d.ts.map +1 -0
  70. package/dist/replay.js +103 -0
  71. package/dist/replay.js.map +1 -0
  72. package/dist/report.d.ts +28 -0
  73. package/dist/report.d.ts.map +1 -0
  74. package/dist/report.js +300 -0
  75. package/dist/report.js.map +1 -0
  76. package/dist/smart-experts.d.ts +44 -0
  77. package/dist/smart-experts.d.ts.map +1 -0
  78. package/dist/smart-experts.js +242 -0
  79. package/dist/smart-experts.js.map +1 -0
  80. package/dist/stream.d.ts +50 -0
  81. package/dist/stream.d.ts.map +1 -0
  82. package/dist/stream.js +219 -0
  83. package/dist/stream.js.map +1 -0
  84. package/dist/tool-run.d.ts +49 -0
  85. package/dist/tool-run.d.ts.map +1 -0
  86. package/dist/tool-run.js +143 -0
  87. package/dist/tool-run.js.map +1 -0
  88. package/dist/types.d.ts +352 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +6 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/variants.d.ts +30 -0
  93. package/dist/variants.d.ts.map +1 -0
  94. package/dist/variants.js +111 -0
  95. package/dist/variants.js.map +1 -0
  96. package/dist/verify.d.ts +33 -0
  97. package/dist/verify.d.ts.map +1 -0
  98. package/dist/verify.js +80 -0
  99. package/dist/verify.js.map +1 -0
  100. package/dist/watch.d.ts +73 -0
  101. package/dist/watch.d.ts.map +1 -0
  102. package/dist/watch.js +350 -0
  103. package/dist/watch.js.map +1 -0
  104. package/docs/session-and-plugin.md +642 -0
  105. package/package.json +90 -6
  106. package/schema/config.schema.json +100 -0
package/dist/mcp.js ADDED
@@ -0,0 +1,1215 @@
1
+ import { existsSync, mkdirSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, resolve, sep } from 'node:path';
3
+ import { StringDecoder } from 'node:string_decoder';
4
+ import { Consilium, loadConfig } from './index.js';
5
+ import { findOpenCodeConfigPath, readJsonFile } from './config.js';
6
+ import { readAgentDefinition, readAgentSection, upsertAgents } from './compat.js';
7
+ import { buildConsiliumConfigFromModels, buildCouncilAgents, fetchFreeModels, selectCouncilModels } from './models.js';
8
+ import { formatDoctorReport, runDoctor } from './doctor.js';
9
+ import { verifyModels } from './verify.js';
10
+ import { applyPreset, listPresets } from './presets.js';
11
+ import { DEFAULT_CONFIG } from './defaults.js';
12
+ const DEFAULT_VERSION = '1.4.0';
13
+ const SERVER_NAME = 'lv24-opencode-consilium';
14
+ const MAX_LINE_BYTES = 4 * 1024 * 1024;
15
+ const FALLBACK_PROTOCOL_VERSION = '2025-06-18';
16
+ const SUPPORTED_PROTOCOL_VERSIONS = [
17
+ '2024-11-05',
18
+ '2025-03-26',
19
+ '2025-06-18'
20
+ ];
21
+ const OUTPUT_FORMATS = ['text', 'markdown', 'json'];
22
+ const PRESET_NAMES = ['fast', 'deep', 'security', 'review', 'free'];
23
+ let notificationSink = null;
24
+ let activeConsilium = null;
25
+ let activeRunRequestId;
26
+ const MCP_TOOLS = [
27
+ {
28
+ name: 'consilium_run',
29
+ description: 'Run a council of expert AI agents through the OpenCode CLI. Experts analyze the task in parallel and the chair synthesizes the final answer.',
30
+ inputSchema: {
31
+ type: 'object',
32
+ properties: {
33
+ task: {
34
+ type: 'string',
35
+ minLength: 1,
36
+ description: 'Task for the expert council.'
37
+ },
38
+ experts: {
39
+ type: 'array',
40
+ items: { type: 'string' },
41
+ description: 'Expert agent names to use (default: all enabled experts).'
42
+ },
43
+ chair: {
44
+ type: 'string',
45
+ description: 'Chair agent name (default: from configuration).'
46
+ },
47
+ timeout_ms: {
48
+ type: 'number',
49
+ exclusiveMinimum: 0,
50
+ description: 'Timeout per expert in milliseconds.'
51
+ },
52
+ output_file: {
53
+ type: 'string',
54
+ description: 'Optional path for saving the result (must stay inside the workspace).'
55
+ },
56
+ format: {
57
+ type: 'string',
58
+ enum: [...OUTPUT_FORMATS],
59
+ description: 'Format used when the result is saved.'
60
+ },
61
+ preset: {
62
+ type: 'string',
63
+ enum: [...PRESET_NAMES],
64
+ description: 'Configuration preset applied before the run.'
65
+ },
66
+ consensus: {
67
+ type: 'boolean',
68
+ description: 'Include expert agreement analysis in the chair prompt and the report.'
69
+ },
70
+ smart_experts: {
71
+ type: 'boolean',
72
+ description: 'Pick 2-3 relevant experts for the task instead of running all enabled experts.'
73
+ },
74
+ max_cost: {
75
+ type: 'number',
76
+ minimum: 0,
77
+ description: 'Maximum run cost in USD. When exceeded, the chair is not started.'
78
+ },
79
+ fallback_models: {
80
+ type: 'array',
81
+ items: { type: 'string' },
82
+ description: 'Models tried in order when an expert hits RATE_LIMITED or MODEL_UNAVAILABLE.'
83
+ },
84
+ session: {
85
+ type: 'string',
86
+ description: 'OpenCode session base id; each agent continues its own session.'
87
+ },
88
+ artifacts: {
89
+ type: ['boolean', 'string'],
90
+ description: 'Save the run to .consilium/runs (true) or to the given directory.'
91
+ }
92
+ },
93
+ required: ['task']
94
+ }
95
+ },
96
+ {
97
+ name: 'consilium_list_agents',
98
+ description: 'List configured consilium experts, the chair agent, the active config path and available presets.',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {},
102
+ required: []
103
+ }
104
+ },
105
+ {
106
+ name: 'consilium_free_models',
107
+ description: 'List free OpenCode models discovered from provider catalogs with context length, tool support, reasoning, score and availability.',
108
+ inputSchema: {
109
+ type: 'object',
110
+ properties: {
111
+ limit: {
112
+ type: 'integer',
113
+ minimum: 1,
114
+ maximum: 50,
115
+ default: 15,
116
+ description: 'Maximum number of models to show.'
117
+ },
118
+ available_only: {
119
+ type: 'boolean',
120
+ default: false,
121
+ description: 'Show only models marked as available.'
122
+ },
123
+ refresh: {
124
+ type: 'boolean',
125
+ default: false,
126
+ description: 'Ignore the cache and refresh the model catalog.'
127
+ }
128
+ },
129
+ required: []
130
+ }
131
+ },
132
+ {
133
+ name: 'consilium_doctor',
134
+ description: 'Run environment and configuration diagnostics for lv24-opencode-consilium.',
135
+ inputSchema: {
136
+ type: 'object',
137
+ properties: {},
138
+ required: []
139
+ }
140
+ },
141
+ {
142
+ name: 'consilium_init_free',
143
+ description: 'Propose (dry run) or apply a free-model council setup. Apply merges agents into opencode.json and writes consilium.config.json.',
144
+ inputSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ apply: {
148
+ type: 'boolean',
149
+ default: false,
150
+ description: 'Write the proposal to opencode.json and consilium.config.json.'
151
+ },
152
+ force: {
153
+ type: 'boolean',
154
+ default: false,
155
+ description: 'Overwrite existing agent models and consilium.config.json.'
156
+ },
157
+ verify: {
158
+ type: 'boolean',
159
+ default: false,
160
+ description: 'Probe candidate models with a tiny real request before selecting.'
161
+ }
162
+ },
163
+ required: []
164
+ }
165
+ }
166
+ ];
167
+ const MCP_TOOL_NAMES = new Set(MCP_TOOLS.map((tool) => tool.name));
168
+ export function getMcpTools() {
169
+ return MCP_TOOLS.map((tool) => ({
170
+ name: tool.name,
171
+ description: tool.description,
172
+ inputSchema: {
173
+ type: tool.inputSchema.type,
174
+ properties: { ...tool.inputSchema.properties },
175
+ required: [...tool.inputSchema.required]
176
+ }
177
+ }));
178
+ }
179
+ class ToolInputError extends Error {
180
+ constructor(message) {
181
+ super(message);
182
+ this.name = 'ToolInputError';
183
+ }
184
+ }
185
+ function isRecord(value) {
186
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
187
+ }
188
+ function describeError(error) {
189
+ return error instanceof Error ? error.message : String(error);
190
+ }
191
+ function safeJson(value) {
192
+ try {
193
+ return JSON.stringify(value) ?? String(value);
194
+ }
195
+ catch {
196
+ return String(value);
197
+ }
198
+ }
199
+ function cell(value) {
200
+ return value.replace(/\r?\n/g, ' ').replace(/\|/g, '\\|');
201
+ }
202
+ function truncateText(text, maxLength) {
203
+ return text.length > maxLength ? `${text.slice(0, maxLength)}…` : text;
204
+ }
205
+ function formatTimestamp(milliseconds) {
206
+ const date = new Date(milliseconds);
207
+ return Number.isNaN(date.getTime()) ? String(milliseconds) : date.toISOString();
208
+ }
209
+ function optionalString(args, key) {
210
+ const value = args[key];
211
+ if (value === undefined || value === null)
212
+ return undefined;
213
+ if (typeof value !== 'string') {
214
+ throw new ToolInputError(`"${key}" must be a string`);
215
+ }
216
+ return value;
217
+ }
218
+ function requireNonEmptyString(args, key) {
219
+ const value = optionalString(args, key);
220
+ if (value === undefined || value.trim().length === 0) {
221
+ throw new ToolInputError(`"${key}" is required and must be a non-empty string`);
222
+ }
223
+ return value;
224
+ }
225
+ function optionalStringArray(args, key) {
226
+ const value = args[key];
227
+ if (value === undefined || value === null)
228
+ return undefined;
229
+ if (!Array.isArray(value)) {
230
+ throw new ToolInputError(`"${key}" must be an array of strings`);
231
+ }
232
+ const items = [];
233
+ for (const item of value) {
234
+ if (typeof item !== 'string') {
235
+ throw new ToolInputError(`"${key}" must be an array of strings`);
236
+ }
237
+ items.push(item);
238
+ }
239
+ return items;
240
+ }
241
+ function optionalPositiveNumber(args, key) {
242
+ const value = args[key];
243
+ if (value === undefined || value === null)
244
+ return undefined;
245
+ if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
246
+ throw new ToolInputError(`"${key}" must be a positive number`);
247
+ }
248
+ return value;
249
+ }
250
+ function optionalInteger(args, key, minimum, maximum) {
251
+ const value = args[key];
252
+ if (value === undefined || value === null)
253
+ return undefined;
254
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < minimum || value > maximum) {
255
+ throw new ToolInputError(`"${key}" must be an integer between ${minimum} and ${maximum}`);
256
+ }
257
+ return value;
258
+ }
259
+ function optionalBoolean(args, key) {
260
+ const value = args[key];
261
+ if (value === undefined || value === null)
262
+ return undefined;
263
+ if (typeof value !== 'boolean') {
264
+ throw new ToolInputError(`"${key}" must be a boolean`);
265
+ }
266
+ return value;
267
+ }
268
+ function optionalEnum(args, key, allowed) {
269
+ const value = optionalString(args, key);
270
+ if (value === undefined)
271
+ return undefined;
272
+ if (!allowed.includes(value)) {
273
+ throw new ToolInputError(`"${key}" must be one of: ${allowed.join(', ')}`);
274
+ }
275
+ return value;
276
+ }
277
+ function toolText(text) {
278
+ return { content: [{ type: 'text', text }] };
279
+ }
280
+ function toolError(text) {
281
+ return { content: [{ type: 'text', text }], isError: true };
282
+ }
283
+ function jsonRpcSuccess(id, result) {
284
+ return { jsonrpc: '2.0', id, result };
285
+ }
286
+ function jsonRpcError(id, code, message, data) {
287
+ return data === undefined
288
+ ? { jsonrpc: '2.0', id, error: { code, message } }
289
+ : { jsonrpc: '2.0', id, error: { code, message, data } };
290
+ }
291
+ function normalizeRequestId(value) {
292
+ if (typeof value === 'string' || typeof value === 'number')
293
+ return value;
294
+ return null;
295
+ }
296
+ function extractProgressToken(params) {
297
+ const meta = params['_meta'];
298
+ if (!isRecord(meta))
299
+ return undefined;
300
+ const token = meta['progressToken'];
301
+ if (typeof token === 'string' || typeof token === 'number')
302
+ return token;
303
+ return undefined;
304
+ }
305
+ function emitNotification(message) {
306
+ const sink = notificationSink;
307
+ if (sink === null)
308
+ return;
309
+ try {
310
+ sink(message);
311
+ }
312
+ catch {
313
+ // Progress notifications are best-effort.
314
+ }
315
+ }
316
+ function formatConsiliumResult(result) {
317
+ const sections = [result.text, '---', '## Мнения экспертов'];
318
+ for (const expert of result.experts) {
319
+ sections.push(`### ${expert.agent} (${expert.model ?? 'default'})`);
320
+ sections.push(truncateText(expert.text, 4000));
321
+ }
322
+ sections.push('---');
323
+ const summary = {
324
+ success: result.success,
325
+ totalTime: result.totalTime,
326
+ parallelTime: result.parallelTime,
327
+ usage: result.usage,
328
+ cost: result.metadata?.cost,
329
+ consensus: result.metadata?.consensus !== null && typeof result.metadata?.consensus === 'object'
330
+ ? (() => {
331
+ const report = result.metadata.consensus;
332
+ return {
333
+ agreement: report.agreement,
334
+ averageSimilarity: report.averageSimilarity
335
+ };
336
+ })()
337
+ : undefined,
338
+ experts: result.experts.map((expert) => ({
339
+ agent: expert.agent,
340
+ model: expert.model,
341
+ success: expert.success,
342
+ duration: expert.duration,
343
+ errorCode: expert.errorCode,
344
+ usage: expert.usage
345
+ }))
346
+ };
347
+ sections.push('```json');
348
+ sections.push(JSON.stringify(summary, null, 2));
349
+ sections.push('```');
350
+ const savedFile = result.metadata?.savedFile;
351
+ const saveError = result.metadata?.saveError;
352
+ if (typeof savedFile === 'string') {
353
+ sections.push(`**Saved to:** \`${savedFile}\``);
354
+ }
355
+ else if (typeof saveError === 'string') {
356
+ sections.push(`**Save failed:** ${saveError}`);
357
+ }
358
+ const artifacts = result.metadata?.artifacts;
359
+ if (artifacts !== null && typeof artifacts === 'object' && !Array.isArray(artifacts)) {
360
+ const dir = artifacts.dir;
361
+ if (typeof dir === 'string') {
362
+ sections.push(`**Artifacts:** \`${dir}\``);
363
+ }
364
+ }
365
+ return sections.join('\n\n');
366
+ }
367
+ function resolveOutputFile(outputFile) {
368
+ if (outputFile === undefined || outputFile.trim().length === 0) {
369
+ return {};
370
+ }
371
+ const cwd = process.cwd();
372
+ const resolved = resolve(cwd, outputFile);
373
+ const prefix = cwd.endsWith(sep) ? cwd : `${cwd}${sep}`;
374
+ if (!resolved.startsWith(prefix)) {
375
+ return {
376
+ error: `output_file must stay inside the workspace (${cwd}); got: ${outputFile}`
377
+ };
378
+ }
379
+ return { value: resolved };
380
+ }
381
+ function applyPresetFor(config, name) {
382
+ return applyPreset(config, name);
383
+ }
384
+ async function toolConsiliumRun(args, progressToken, requestId) {
385
+ const task = requireNonEmptyString(args, 'task');
386
+ const experts = optionalStringArray(args, 'experts');
387
+ const chair = optionalString(args, 'chair');
388
+ const timeoutMs = optionalPositiveNumber(args, 'timeout_ms');
389
+ const outputFile = optionalString(args, 'output_file');
390
+ const format = optionalEnum(args, 'format', OUTPUT_FORMATS);
391
+ const preset = optionalEnum(args, 'preset', PRESET_NAMES);
392
+ const consensus = optionalBoolean(args, 'consensus');
393
+ const smartExperts = optionalBoolean(args, 'smart_experts');
394
+ const maxCostRaw = args['max_cost'];
395
+ if (maxCostRaw !== undefined &&
396
+ (typeof maxCostRaw !== 'number' || !Number.isFinite(maxCostRaw) || maxCostRaw < 0)) {
397
+ throw new ToolInputError('"max_cost" must be a number >= 0');
398
+ }
399
+ const maxCost = maxCostRaw;
400
+ const fallbackModels = optionalStringArray(args, 'fallback_models');
401
+ const session = optionalString(args, 'session');
402
+ const artifactsArg = args['artifacts'];
403
+ let artifacts;
404
+ if (typeof artifactsArg === 'boolean') {
405
+ artifacts = artifactsArg;
406
+ }
407
+ else if (typeof artifactsArg === 'string') {
408
+ const cwd = process.cwd();
409
+ const resolved = resolve(cwd, artifactsArg);
410
+ const prefix = cwd.endsWith(sep) ? cwd : `${cwd}${sep}`;
411
+ if (resolved !== cwd && !resolved.startsWith(prefix)) {
412
+ throw new ToolInputError('"artifacts" must stay inside the workspace');
413
+ }
414
+ artifacts = resolved;
415
+ }
416
+ else if (artifactsArg !== undefined) {
417
+ throw new ToolInputError('"artifacts" must be a boolean or a string path');
418
+ }
419
+ const outputPath = resolveOutputFile(outputFile);
420
+ if (outputPath.error !== undefined) {
421
+ return toolError(outputPath.error);
422
+ }
423
+ const loaded = loadConfig();
424
+ let config = loaded.config;
425
+ if (preset !== undefined) {
426
+ config = applyPresetFor(config, preset);
427
+ }
428
+ if (format !== undefined) {
429
+ config.output.format = format;
430
+ }
431
+ if (consensus !== undefined)
432
+ config.consensus = consensus;
433
+ if (smartExperts !== undefined)
434
+ config.smartExperts = smartExperts;
435
+ if (maxCost !== undefined)
436
+ config.maxCost = maxCost;
437
+ if (fallbackModels !== undefined)
438
+ config.fallbackModels = fallbackModels;
439
+ if (session !== undefined)
440
+ config.session = session;
441
+ if (artifacts !== undefined)
442
+ config.artifacts = artifacts;
443
+ if (outputPath.value !== undefined) {
444
+ config.output.file = outputPath.value;
445
+ }
446
+ const runOptions = {};
447
+ if (experts !== undefined)
448
+ runOptions.experts = experts;
449
+ if (chair !== undefined)
450
+ runOptions.chair = chair;
451
+ if (timeoutMs !== undefined)
452
+ runOptions.timeout = timeoutMs;
453
+ if (outputPath.value !== undefined)
454
+ runOptions.outputFile = outputPath.value;
455
+ const enabledCount = config.experts.filter((expert) => expert.enabled !== false).length;
456
+ const expertCount = experts !== undefined && experts.length > 0 ? experts.length : enabledCount;
457
+ const total = Math.max(1, expertCount) + 1;
458
+ const consilium = new Consilium(config);
459
+ let unsubscribe;
460
+ let startedExperts = 0;
461
+ if (progressToken !== undefined) {
462
+ unsubscribe = consilium.onProgress((event) => {
463
+ let progress;
464
+ let message;
465
+ if (event.type === 'expert_start') {
466
+ startedExperts += 1;
467
+ progress = Math.min(startedExperts, total - 1);
468
+ message = `Expert ${event.agent ?? ''} started`.trim();
469
+ }
470
+ else if (event.type === 'chair_start') {
471
+ progress = Math.max(1, total - 1);
472
+ message = 'Chair is synthesizing the final answer';
473
+ }
474
+ else if (event.type === 'chair_end') {
475
+ progress = total;
476
+ message = 'Consilium completed';
477
+ }
478
+ else {
479
+ return;
480
+ }
481
+ emitNotification({
482
+ jsonrpc: '2.0',
483
+ method: 'notifications/progress',
484
+ params: {
485
+ progressToken,
486
+ progress,
487
+ total,
488
+ message
489
+ }
490
+ });
491
+ });
492
+ }
493
+ activeConsilium = consilium;
494
+ activeRunRequestId = requestId;
495
+ try {
496
+ const result = await consilium.run(task, runOptions);
497
+ let text = formatConsiliumResult(result);
498
+ if (loaded.warnings.length > 0) {
499
+ text += `\n\n---\n\n**Config warnings**\n${loaded.warnings.map(w => `- ${w}`).join('\n')}`;
500
+ }
501
+ return {
502
+ content: [{ type: 'text', text }],
503
+ isError: result.success === false
504
+ };
505
+ }
506
+ catch (error) {
507
+ return toolError(`Consilium run failed: ${describeError(error)}`);
508
+ }
509
+ finally {
510
+ if (unsubscribe !== undefined)
511
+ unsubscribe();
512
+ if (activeConsilium === consilium)
513
+ activeConsilium = null;
514
+ if (activeConsilium === null)
515
+ activeRunRequestId = undefined;
516
+ }
517
+ }
518
+ function formatPresets(presets) {
519
+ if (!Array.isArray(presets)) {
520
+ return typeof presets === 'string' && presets.length > 0 ? presets : 'none';
521
+ }
522
+ const entries = [];
523
+ for (const preset of presets) {
524
+ if (typeof preset === 'string') {
525
+ entries.push(preset);
526
+ continue;
527
+ }
528
+ if (isRecord(preset)) {
529
+ const name = preset['name'];
530
+ if (typeof name === 'string') {
531
+ const description = preset['description'];
532
+ entries.push(typeof description === 'string' ? `${name} — ${description}` : name);
533
+ continue;
534
+ }
535
+ }
536
+ entries.push(safeJson(preset));
537
+ }
538
+ return entries.length > 0 ? entries.join(', ') : 'none';
539
+ }
540
+ function formatAgentsReport(config, configPath, presets) {
541
+ const lines = [];
542
+ lines.push('## Consilium agents');
543
+ lines.push('');
544
+ lines.push(`Config file: ${configPath ?? '(none, using defaults)'}`);
545
+ lines.push('');
546
+ lines.push(`### Experts (${config.experts.length})`);
547
+ lines.push('');
548
+ lines.push('| Name | Model | Enabled | Prompt length |');
549
+ lines.push('| --- | --- | --- | --- |');
550
+ for (const expert of config.experts) {
551
+ const promptLength = typeof expert.prompt === 'function'
552
+ ? `function (${expert.prompt.toString().length} chars)`
553
+ : expert.prompt !== undefined
554
+ ? `${expert.prompt.length} chars`
555
+ : 'default';
556
+ lines.push(`| ${cell(expert.name)} | ${cell(expert.model ?? '(default)')} | ${expert.enabled === false ? 'no' : 'yes'} | ${cell(promptLength)} |`);
557
+ }
558
+ lines.push('');
559
+ lines.push('### Chair');
560
+ lines.push('');
561
+ lines.push(`- Agent: ${config.chair.agent}`);
562
+ lines.push(`- Model: ${config.chair.model ?? '(default)'}`);
563
+ lines.push(`- Max expert text length: ${config.chair.maxExpertTextLength ?? '(default)'}`);
564
+ lines.push('');
565
+ lines.push('### Presets');
566
+ lines.push('');
567
+ lines.push(formatPresets(presets));
568
+ return lines.join('\n');
569
+ }
570
+ async function toolListAgents() {
571
+ const loaded = loadConfig();
572
+ let presets = [];
573
+ try {
574
+ presets = await listPresets();
575
+ }
576
+ catch {
577
+ presets = [];
578
+ }
579
+ return toolText(formatAgentsReport(loaded.config, loaded.configPath, presets));
580
+ }
581
+ const MODEL_ID_KEYS = ['id', 'model', 'modelId', 'model_id', 'name', 'slug'];
582
+ const MODEL_CONTEXT_KEYS = [
583
+ 'contextLength',
584
+ 'context_length',
585
+ 'contextWindow',
586
+ 'context_window',
587
+ 'context',
588
+ 'maxContext',
589
+ 'max_context',
590
+ 'limit'
591
+ ];
592
+ const MODEL_TOOLS_KEYS = [
593
+ 'supportsTools',
594
+ 'supports_tools',
595
+ 'tools',
596
+ 'toolCall',
597
+ 'tool_call',
598
+ 'functionCalling',
599
+ 'function_calling'
600
+ ];
601
+ const MODEL_REASONING_KEYS = [
602
+ 'reasoning',
603
+ 'supportsReasoning',
604
+ 'supports_reasoning',
605
+ 'thinking'
606
+ ];
607
+ const MODEL_SCORE_KEYS = [
608
+ 'score',
609
+ 'quality',
610
+ 'rank',
611
+ 'rating',
612
+ 'benchmarkScore',
613
+ 'benchmark_score'
614
+ ];
615
+ const MODEL_AVAILABLE_KEYS = [
616
+ 'available',
617
+ 'isAvailable',
618
+ 'is_available',
619
+ 'hasKey',
620
+ 'has_key'
621
+ ];
622
+ const MODEL_SOURCE_KEYS = ['source', 'provider', 'origin'];
623
+ function readModelValue(model, keys) {
624
+ const record = model;
625
+ for (const key of keys) {
626
+ const value = record[key];
627
+ if (value !== undefined && value !== null)
628
+ return value;
629
+ }
630
+ return undefined;
631
+ }
632
+ function displayValue(value) {
633
+ if (value === undefined || value === null)
634
+ return '?';
635
+ if (typeof value === 'string')
636
+ return value;
637
+ if (typeof value === 'number' || typeof value === 'boolean')
638
+ return String(value);
639
+ return safeJson(value);
640
+ }
641
+ function flagValue(value) {
642
+ if (typeof value === 'boolean')
643
+ return value;
644
+ if (typeof value === 'number')
645
+ return value !== 0;
646
+ if (typeof value === 'string') {
647
+ const normalized = value.trim().toLowerCase();
648
+ if (['true', 'yes', '1', 'available', 'supported'].includes(normalized))
649
+ return true;
650
+ if (['false', 'no', '0', 'unavailable', 'unsupported'].includes(normalized))
651
+ return false;
652
+ }
653
+ return undefined;
654
+ }
655
+ function readModelFlag(model, keys) {
656
+ return flagValue(readModelValue(model, keys));
657
+ }
658
+ function renderFlag(model, keys) {
659
+ const flag = readModelFlag(model, keys);
660
+ if (flag === undefined)
661
+ return '?';
662
+ return flag ? 'yes' : 'no';
663
+ }
664
+ function renderContext(model) {
665
+ const value = readModelValue(model, MODEL_CONTEXT_KEYS);
666
+ if (typeof value === 'number' && Number.isFinite(value))
667
+ return String(value);
668
+ return displayValue(value);
669
+ }
670
+ async function toolFreeModels(args) {
671
+ const limit = optionalInteger(args, 'limit', 1, 50) ?? 15;
672
+ const availableOnly = optionalBoolean(args, 'available_only') ?? false;
673
+ const refresh = optionalBoolean(args, 'refresh') ?? false;
674
+ const fetched = await fetchFreeModels({
675
+ forceRefresh: refresh,
676
+ includeUnavailable: !availableOnly
677
+ });
678
+ const filtered = availableOnly
679
+ ? fetched.models.filter((model) => readModelFlag(model, MODEL_AVAILABLE_KEYS) === true)
680
+ : fetched.models;
681
+ const visible = filtered.slice(0, limit);
682
+ const lines = [];
683
+ lines.push('## Free OpenCode models');
684
+ lines.push('');
685
+ lines.push(`Cache: ${fetched.fromCache ? 'hit' : 'miss'}; fetched at ${formatTimestamp(fetched.fetchedAt)}`);
686
+ lines.push('');
687
+ lines.push('### Sources');
688
+ if (fetched.sources.length === 0) {
689
+ lines.push('- (no sources reported)');
690
+ }
691
+ else {
692
+ for (const source of fetched.sources) {
693
+ const status = source.ok
694
+ ? `ok (${source.count} models)`
695
+ : `failed${source.error !== undefined ? ` — ${source.error}` : ''}`;
696
+ lines.push(`- ${source.source}: ${status}`);
697
+ }
698
+ }
699
+ lines.push('');
700
+ lines.push(`### Models (${visible.length} of ${filtered.length})`);
701
+ lines.push('');
702
+ if (visible.length === 0) {
703
+ lines.push('No models matched the request.');
704
+ }
705
+ else {
706
+ lines.push('| rank | model id | ctx | tools | reasoning | score | available | source |');
707
+ lines.push('| --- | --- | --- | --- | --- | --- | --- | --- |');
708
+ visible.forEach((model, index) => {
709
+ const id = displayValue(readModelValue(model, MODEL_ID_KEYS));
710
+ const score = displayValue(readModelValue(model, MODEL_SCORE_KEYS));
711
+ const source = displayValue(readModelValue(model, MODEL_SOURCE_KEYS));
712
+ lines.push(`| ${index + 1} | ${cell(id)} | ${cell(renderContext(model))} | ${cell(renderFlag(model, MODEL_TOOLS_KEYS))} | ${cell(renderFlag(model, MODEL_REASONING_KEYS))} | ${cell(score)} | ${cell(renderFlag(model, MODEL_AVAILABLE_KEYS))} | ${cell(source)} |`);
713
+ });
714
+ }
715
+ return toolText(lines.join('\n'));
716
+ }
717
+ async function toolDoctor() {
718
+ const loaded = loadConfig();
719
+ const checks = await runDoctor(loaded.config, { configPath: loaded.configPath });
720
+ const report = formatDoctorReport(checks);
721
+ if (loaded.warnings.length === 0) {
722
+ return toolText(report);
723
+ }
724
+ const warnings = loaded.warnings.map(warning => `- ${warning}`).join('\n');
725
+ return toolText(`${report}\n\n### Config warnings\n\n${warnings}`);
726
+ }
727
+ function writeFileAtomic(filePath, content) {
728
+ const directory = dirname(filePath);
729
+ mkdirSync(directory, { recursive: true });
730
+ const temporaryPath = resolve(directory, `.${basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
731
+ writeFileSync(temporaryPath, content, 'utf-8');
732
+ try {
733
+ renameSync(temporaryPath, filePath);
734
+ }
735
+ catch (error) {
736
+ try {
737
+ unlinkSync(temporaryPath);
738
+ }
739
+ catch {
740
+ // Ignore cleanup failures and surface the original error.
741
+ }
742
+ throw error;
743
+ }
744
+ }
745
+ function applyFreeSetup(agents, consiliumConfig, force, extraWarnings = []) {
746
+ const workingDirectory = consiliumConfig.cwd ?? process.cwd();
747
+ const opencodePath = findOpenCodeConfigPath(consiliumConfig.opencodeConfigPath, workingDirectory) ??
748
+ resolve(workingDirectory, 'opencode.json');
749
+ const consiliumPath = resolve(workingDirectory, 'consilium.config.json');
750
+ let baseConfig = {};
751
+ if (existsSync(opencodePath)) {
752
+ let parsed;
753
+ try {
754
+ parsed = readJsonFile(opencodePath);
755
+ }
756
+ catch (error) {
757
+ return toolError(`Cannot apply: opencode.json is not valid JSON (${describeError(error)}). No files were written.`);
758
+ }
759
+ if (!isRecord(parsed)) {
760
+ return toolError('Cannot apply: opencode.json must contain a JSON object. No files were written.');
761
+ }
762
+ baseConfig = parsed;
763
+ }
764
+ const section = readAgentSection(baseConfig);
765
+ const definitions = {};
766
+ for (const [agentName, proposed] of Object.entries(agents)) {
767
+ const current = section.entries[agentName];
768
+ const currentView = current === undefined ? undefined : readAgentDefinition(current);
769
+ const currentModel = currentView?.model;
770
+ const kept = currentModel !== undefined && currentModel.length > 0 && !force;
771
+ definitions[agentName] = {
772
+ model: kept && currentModel !== undefined ? currentModel : proposed.model,
773
+ prompt: currentView?.prompt ?? proposed.prompt,
774
+ mode: kept ? currentView?.mode ?? proposed.mode : proposed.mode
775
+ };
776
+ }
777
+ const upserted = upsertAgents(baseConfig, definitions, {});
778
+ const nextConfig = typeof upserted.config['$schema'] === 'string'
779
+ ? upserted.config
780
+ : { $schema: 'https://opencode.ai/config.json', ...upserted.config };
781
+ writeFileAtomic(opencodePath, `${JSON.stringify(nextConfig, null, 2)}\n`);
782
+ let consiliumWritten = false;
783
+ const warnings = [...extraWarnings, ...upserted.warnings];
784
+ if (opencodePath.endsWith('.jsonc')) {
785
+ warnings.push(`Файл ${opencodePath} перезаписан без комментариев: JSONC-комментарии не сохраняются.`);
786
+ }
787
+ if (!existsSync(consiliumPath) || force) {
788
+ writeFileAtomic(consiliumPath, `${JSON.stringify(consiliumConfig, null, 2)}\n`);
789
+ consiliumWritten = true;
790
+ }
791
+ else {
792
+ warnings.push(`Skipped ${consiliumPath}: file already exists (pass "force": true to overwrite).`);
793
+ }
794
+ const lines = [];
795
+ lines.push('## Free-model setup applied');
796
+ lines.push('');
797
+ lines.push('### Written files');
798
+ lines.push(`- ${opencodePath}`);
799
+ if (consiliumWritten) {
800
+ lines.push(`- ${consiliumPath}`);
801
+ }
802
+ lines.push('');
803
+ lines.push('### Assigned models');
804
+ lines.push('');
805
+ lines.push('| Agent | Model |');
806
+ lines.push('| --- | --- |');
807
+ for (const agentName of Object.keys(agents).sort()) {
808
+ const model = definitions[agentName]?.model ?? '(unchanged)';
809
+ lines.push(`| ${cell(agentName)} | ${cell(model)} |`);
810
+ }
811
+ if (warnings.length > 0) {
812
+ lines.push('');
813
+ lines.push('### Warnings');
814
+ for (const warning of warnings)
815
+ lines.push(`- ${warning}`);
816
+ }
817
+ return toolText(lines.join('\n'));
818
+ }
819
+ async function toolInitFree(args) {
820
+ const apply = optionalBoolean(args, 'apply') ?? false;
821
+ const force = optionalBoolean(args, 'force') ?? false;
822
+ const verify = optionalBoolean(args, 'verify') ?? false;
823
+ const fetched = await fetchFreeModels({ includeUnavailable: false });
824
+ let pool = fetched.models;
825
+ const verificationWarnings = [];
826
+ let verification = [];
827
+ if (verify) {
828
+ const candidates = pool.slice(0, Math.min(12, pool.length));
829
+ const checks = await verifyModels(candidates, { timeoutMs: 60000, concurrency: 4 });
830
+ verification = checks.map(check => ({
831
+ model: check.model.id,
832
+ ok: check.ok,
833
+ error: check.error
834
+ }));
835
+ const working = checks.filter(check => check.ok).map(check => check.model);
836
+ if (working.length >= 3) {
837
+ pool = working;
838
+ }
839
+ else {
840
+ verificationWarnings.push(`Only ${working.length} model(s) passed verification; using the unverified ranking.`);
841
+ }
842
+ }
843
+ const selected = selectCouncilModels(pool, 5);
844
+ if (selected.length === 0) {
845
+ return toolError('No free models are available; cannot build a council configuration.');
846
+ }
847
+ const agents = buildCouncilAgents(selected);
848
+ const council = buildConsiliumConfigFromModels(selected);
849
+ const consiliumConfig = {
850
+ version: DEFAULT_CONFIG.version ?? '1.0.0',
851
+ experts: council.experts,
852
+ chair: council.chair,
853
+ retry: { ...DEFAULT_CONFIG.retry },
854
+ timeouts: { ...DEFAULT_CONFIG.timeouts },
855
+ output: {
856
+ file: false,
857
+ format: 'markdown',
858
+ progress: true,
859
+ logLevel: 'info'
860
+ },
861
+ autoDiscoverAgents: false
862
+ };
863
+ if (!apply) {
864
+ return toolText(JSON.stringify({
865
+ opencodeConfig: { agent: agents },
866
+ consiliumConfig,
867
+ verification,
868
+ warnings: verificationWarnings
869
+ }, null, 2));
870
+ }
871
+ return applyFreeSetup(agents, consiliumConfig, force, verificationWarnings);
872
+ }
873
+ async function executeTool(name, args, progressToken, requestId) {
874
+ try {
875
+ switch (name) {
876
+ case 'consilium_run':
877
+ return await toolConsiliumRun(args, progressToken, requestId);
878
+ case 'consilium_list_agents':
879
+ return await toolListAgents();
880
+ case 'consilium_free_models':
881
+ return await toolFreeModels(args);
882
+ case 'consilium_doctor':
883
+ return await toolDoctor();
884
+ case 'consilium_init_free':
885
+ return await toolInitFree(args);
886
+ default:
887
+ return toolError(`Unknown tool: ${name}`);
888
+ }
889
+ }
890
+ catch (error) {
891
+ return toolError(describeError(error));
892
+ }
893
+ }
894
+ function buildInitializeResult(params, version) {
895
+ const requested = params !== undefined && typeof params['protocolVersion'] === 'string'
896
+ ? params['protocolVersion']
897
+ : '';
898
+ const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(requested)
899
+ ? requested
900
+ : FALLBACK_PROTOCOL_VERSION;
901
+ return {
902
+ protocolVersion,
903
+ capabilities: {
904
+ tools: { listChanged: false },
905
+ logging: {}
906
+ },
907
+ serverInfo: { name: SERVER_NAME, version },
908
+ instructions: 'lv24-opencode-consilium runs a council of expert AI agents through the OpenCode CLI. Use consilium_run to ask the council a task, consilium_list_agents to inspect configured experts, consilium_free_models to discover free models, consilium_doctor to diagnose setup issues, and consilium_init_free to propose or apply a free-model configuration.'
909
+ };
910
+ }
911
+ async function dispatchMcpMethod(method, params, id, hasId, ctx) {
912
+ const respond = (result) => hasId ? jsonRpcSuccess(id, result) : null;
913
+ switch (method) {
914
+ case 'initialize':
915
+ return respond(buildInitializeResult(params, ctx.version ?? DEFAULT_VERSION));
916
+ case 'notifications/initialized':
917
+ return hasId ? jsonRpcSuccess(id, {}) : null;
918
+ case 'ping':
919
+ return respond({});
920
+ case 'tools/list':
921
+ return respond({ tools: getMcpTools() });
922
+ case 'tools/call': {
923
+ if (params === undefined) {
924
+ return hasId ? jsonRpcError(id, -32602, 'Invalid params: expected an object') : null;
925
+ }
926
+ const name = params['name'];
927
+ if (typeof name !== 'string' || !MCP_TOOL_NAMES.has(name)) {
928
+ return hasId
929
+ ? jsonRpcError(id, -32602, `Unknown tool: ${typeof name === 'string' ? name : '(missing)'}`)
930
+ : null;
931
+ }
932
+ const rawArguments = params['arguments'];
933
+ if (rawArguments !== undefined && !isRecord(rawArguments)) {
934
+ return hasId
935
+ ? jsonRpcError(id, -32602, 'Invalid params: "arguments" must be an object')
936
+ : null;
937
+ }
938
+ const args = isRecord(rawArguments) ? rawArguments : {};
939
+ const result = await executeTool(name, args, extractProgressToken(params), id);
940
+ return respond(result);
941
+ }
942
+ case 'resources/list':
943
+ return respond({ resources: [] });
944
+ case 'prompts/list':
945
+ return respond({ prompts: [] });
946
+ case 'logging/setLevel':
947
+ return respond({});
948
+ default:
949
+ return hasId ? jsonRpcError(id, -32601, `Method not found: ${method}`) : null;
950
+ }
951
+ }
952
+ export async function handleMcpRequest(request, ctx = {}) {
953
+ if (!isRecord(request)) {
954
+ return jsonRpcError(null, -32600, 'Invalid Request');
955
+ }
956
+ const hasId = Object.prototype.hasOwnProperty.call(request, 'id');
957
+ const id = normalizeRequestId(request['id']);
958
+ const method = request['method'];
959
+ if (typeof method !== 'string') {
960
+ return jsonRpcError(id, -32600, 'Invalid Request: method must be a string');
961
+ }
962
+ const rawParams = request['params'];
963
+ const params = isRecord(rawParams) ? rawParams : undefined;
964
+ try {
965
+ return await dispatchMcpMethod(method, params, id, hasId, ctx);
966
+ }
967
+ catch (error) {
968
+ if (!hasId)
969
+ return null;
970
+ return jsonRpcError(id, -32603, 'Internal error', describeError(error));
971
+ }
972
+ }
973
+ function writeJsonLine(stream, message) {
974
+ try {
975
+ stream.write(`${JSON.stringify(message)}\n`);
976
+ }
977
+ catch (error) {
978
+ process.stderr.write(`MCP write failed: ${describeError(error)}\n`);
979
+ }
980
+ }
981
+ async function processInputLine(line, stdout, version) {
982
+ let parsed;
983
+ try {
984
+ parsed = JSON.parse(line);
985
+ }
986
+ catch {
987
+ writeJsonLine(stdout, jsonRpcError(null, -32700, 'Parse error'));
988
+ return;
989
+ }
990
+ const response = await handleMcpRequest(parsed, { version });
991
+ if (response !== null) {
992
+ writeJsonLine(stdout, response);
993
+ }
994
+ }
995
+ function readLines(stream, onLine, onOversizedLine) {
996
+ return new Promise((resolvePromise) => {
997
+ let buffer = '';
998
+ let discarding = false;
999
+ let finished = false;
1000
+ const decoder = new StringDecoder('utf8');
1001
+ function cleanup() {
1002
+ stream.off('data', onData);
1003
+ stream.off('end', onEnd);
1004
+ stream.off('close', onClose);
1005
+ stream.off('error', onError);
1006
+ }
1007
+ function finish() {
1008
+ if (finished)
1009
+ return;
1010
+ finished = true;
1011
+ cleanup();
1012
+ resolvePromise();
1013
+ }
1014
+ function reportOversized() {
1015
+ if (discarding)
1016
+ return;
1017
+ discarding = true;
1018
+ onOversizedLine();
1019
+ }
1020
+ function handleLine(rawLine) {
1021
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
1022
+ if (line.trim().length === 0)
1023
+ return;
1024
+ if (Buffer.byteLength(line, 'utf8') > MAX_LINE_BYTES) {
1025
+ onOversizedLine();
1026
+ return;
1027
+ }
1028
+ onLine(line);
1029
+ }
1030
+ function onData(chunk) {
1031
+ buffer += typeof chunk === 'string' ? chunk : decoder.write(chunk);
1032
+ for (;;) {
1033
+ const newlineIndex = buffer.indexOf('\n');
1034
+ if (newlineIndex === -1) {
1035
+ if (Buffer.byteLength(buffer, 'utf8') > MAX_LINE_BYTES) {
1036
+ reportOversized();
1037
+ buffer = '';
1038
+ }
1039
+ return;
1040
+ }
1041
+ const rawLine = buffer.slice(0, newlineIndex);
1042
+ buffer = buffer.slice(newlineIndex + 1);
1043
+ if (discarding) {
1044
+ discarding = false;
1045
+ continue;
1046
+ }
1047
+ handleLine(rawLine);
1048
+ }
1049
+ }
1050
+ function onEnd() {
1051
+ buffer += decoder.end();
1052
+ if (buffer.length > 0 && !discarding) {
1053
+ handleLine(buffer);
1054
+ }
1055
+ finish();
1056
+ }
1057
+ function onClose() {
1058
+ finish();
1059
+ }
1060
+ function onError(error) {
1061
+ process.stderr.write(`MCP stdin error: ${describeError(error)}\n`);
1062
+ finish();
1063
+ }
1064
+ const state = stream;
1065
+ if (state.readableEnded === true || state.destroyed === true) {
1066
+ resolvePromise();
1067
+ return;
1068
+ }
1069
+ stream.on('data', onData);
1070
+ stream.on('end', onEnd);
1071
+ stream.on('close', onClose);
1072
+ stream.on('error', onError);
1073
+ });
1074
+ }
1075
+ function stringifyConsoleValue(value) {
1076
+ if (typeof value === 'string')
1077
+ return value;
1078
+ if (value instanceof Error)
1079
+ return value.stack ?? value.message;
1080
+ return safeJson(value);
1081
+ }
1082
+ function redirectConsoleLogs() {
1083
+ const originalLog = console.log;
1084
+ const originalInfo = console.info;
1085
+ const originalDebug = console.debug;
1086
+ const writeToStderr = (...data) => {
1087
+ try {
1088
+ const text = data.map(stringifyConsoleValue).join(' ');
1089
+ process.stderr.write(text.endsWith('\n') ? text : `${text}\n`);
1090
+ }
1091
+ catch {
1092
+ // Logging must never break the protocol.
1093
+ }
1094
+ };
1095
+ console.log = writeToStderr;
1096
+ console.info = writeToStderr;
1097
+ console.debug = writeToStderr;
1098
+ return () => {
1099
+ console.log = originalLog;
1100
+ console.info = originalInfo;
1101
+ console.debug = originalDebug;
1102
+ };
1103
+ }
1104
+ export async function startMcpServer(opts = {}) {
1105
+ const version = opts.version ?? DEFAULT_VERSION;
1106
+ const stdin = opts.stdin ?? process.stdin;
1107
+ const stdout = opts.stdout ?? process.stdout;
1108
+ const previousSink = notificationSink;
1109
+ notificationSink = (message) => {
1110
+ writeJsonLine(stdout, message);
1111
+ };
1112
+ const restoreConsole = redirectConsoleLogs();
1113
+ const onOutputError = (error) => {
1114
+ process.stderr.write(`MCP stdout error: ${describeError(error)}\n`);
1115
+ };
1116
+ try {
1117
+ stdout.on('error', onOutputError);
1118
+ }
1119
+ catch {
1120
+ // A custom stream may not support event listeners.
1121
+ }
1122
+ let queue = Promise.resolve();
1123
+ let transportClosed = false;
1124
+ const enqueueToolCall = (line) => {
1125
+ queue = queue
1126
+ .then(async () => {
1127
+ if (transportClosed)
1128
+ return;
1129
+ await processInputLine(line, stdout, version);
1130
+ })
1131
+ .catch((error) => {
1132
+ process.stderr.write(`MCP handler error: ${describeError(error)}\n`);
1133
+ });
1134
+ };
1135
+ const cancelActiveRun = () => {
1136
+ try {
1137
+ activeConsilium?.cancel();
1138
+ }
1139
+ catch {
1140
+ // Cancellation is best-effort.
1141
+ }
1142
+ };
1143
+ const handleLine = (line) => {
1144
+ let parsed;
1145
+ try {
1146
+ parsed = JSON.parse(line);
1147
+ }
1148
+ catch {
1149
+ writeJsonLine(stdout, jsonRpcError(null, -32700, 'Parse error'));
1150
+ return;
1151
+ }
1152
+ if (isRecord(parsed)) {
1153
+ const method = parsed['method'];
1154
+ if (method === 'notifications/cancelled') {
1155
+ const cancelParams = isRecord(parsed['params']) ? parsed['params'] : undefined;
1156
+ const target = cancelParams?.['requestId'];
1157
+ if (target === undefined || target === activeRunRequestId) {
1158
+ cancelActiveRun();
1159
+ }
1160
+ return;
1161
+ }
1162
+ const hasId = Object.prototype.hasOwnProperty.call(parsed, 'id');
1163
+ if (hasId && method === 'tools/call') {
1164
+ enqueueToolCall(line);
1165
+ return;
1166
+ }
1167
+ }
1168
+ // Control methods and notifications never wait behind a long council run.
1169
+ void processInputLine(line, stdout, version);
1170
+ };
1171
+ function cleanup() {
1172
+ notificationSink = previousSink;
1173
+ restoreConsole();
1174
+ process.off('SIGINT', onSignal);
1175
+ process.off('SIGTERM', onSignal);
1176
+ try {
1177
+ stdout.off('error', onOutputError);
1178
+ }
1179
+ catch {
1180
+ // Ignore streams without removeListener support.
1181
+ }
1182
+ }
1183
+ function onSignal() {
1184
+ transportClosed = true;
1185
+ try {
1186
+ activeConsilium?.cancel();
1187
+ }
1188
+ catch {
1189
+ // Cancellation is best-effort during shutdown.
1190
+ }
1191
+ cleanup();
1192
+ process.exit(0);
1193
+ }
1194
+ process.on('SIGINT', onSignal);
1195
+ process.on('SIGTERM', onSignal);
1196
+ try {
1197
+ await readLines(stdin, handleLine, () => {
1198
+ writeJsonLine(stdout, jsonRpcError(null, -32700, 'Parse error: input line exceeds 4 MiB'));
1199
+ });
1200
+ // Транспорт закрыт — отменяем активный запуск, не начинаем новые и не висим дольше таймаута.
1201
+ transportClosed = true;
1202
+ cancelActiveRun();
1203
+ await Promise.race([
1204
+ queue,
1205
+ new Promise(resolveTimeout => {
1206
+ const timer = setTimeout(resolveTimeout, 5000);
1207
+ timer.unref();
1208
+ })
1209
+ ]);
1210
+ }
1211
+ finally {
1212
+ cleanup();
1213
+ }
1214
+ }
1215
+ //# sourceMappingURL=mcp.js.map