@corbat-tech/coco 2.25.7 → 2.25.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +841 -605
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +2731 -220
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs16__default, { readFile, access, readdir } from 'fs/promises';
|
|
3
|
-
import * as path17 from 'path';
|
|
4
|
-
import path17__default, { dirname, join, basename, resolve } from 'path';
|
|
5
|
-
import { randomUUID } from 'crypto';
|
|
6
|
-
import 'http';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
1
|
+
import { Logger } from 'tslog';
|
|
8
2
|
import * as fs4 from 'fs';
|
|
9
3
|
import fs4__default, { readFileSync, constants } from 'fs';
|
|
4
|
+
import * as path17 from 'path';
|
|
5
|
+
import path17__default, { dirname, join, basename, resolve } from 'path';
|
|
6
|
+
import * as fs16 from 'fs/promises';
|
|
7
|
+
import fs16__default, { access, readFile, writeFile, mkdir, readdir } from 'fs/promises';
|
|
8
|
+
import { randomUUID, randomBytes, createHash } from 'crypto';
|
|
9
|
+
import * as http from 'http';
|
|
10
|
+
import { fileURLToPath, URL as URL$1 } from 'url';
|
|
10
11
|
import { z } from 'zod';
|
|
11
12
|
import * as p4 from '@clack/prompts';
|
|
12
13
|
import chalk5 from 'chalk';
|
|
13
|
-
import { exec, execSync,
|
|
14
|
+
import { exec, execFile, execSync, spawn } from 'child_process';
|
|
14
15
|
import { promisify } from 'util';
|
|
15
16
|
import { homedir } from 'os';
|
|
16
17
|
import JSON5 from 'json5';
|
|
17
18
|
import { execa } from 'execa';
|
|
18
19
|
import { parse } from '@typescript-eslint/typescript-estree';
|
|
19
20
|
import { glob } from 'glob';
|
|
20
|
-
import { Logger } from 'tslog';
|
|
21
21
|
import Anthropic from '@anthropic-ai/sdk';
|
|
22
22
|
import { jsonrepair } from 'jsonrepair';
|
|
23
23
|
import OpenAI from 'openai';
|
|
@@ -198,6 +198,60 @@ var init_errors = __esm({
|
|
|
198
198
|
};
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
|
+
function levelToNumber(level) {
|
|
202
|
+
const levels = {
|
|
203
|
+
silly: 0,
|
|
204
|
+
trace: 1,
|
|
205
|
+
debug: 2,
|
|
206
|
+
info: 3,
|
|
207
|
+
warn: 4,
|
|
208
|
+
error: 5,
|
|
209
|
+
fatal: 6
|
|
210
|
+
};
|
|
211
|
+
return levels[level];
|
|
212
|
+
}
|
|
213
|
+
function createLogger(config = {}) {
|
|
214
|
+
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
215
|
+
const logger2 = new Logger({
|
|
216
|
+
name: finalConfig.name,
|
|
217
|
+
minLevel: levelToNumber(finalConfig.level),
|
|
218
|
+
prettyLogTemplate: finalConfig.prettyPrint ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}} {{logLevelName}} [{{name}}] " : void 0,
|
|
219
|
+
prettyLogTimeZone: "local",
|
|
220
|
+
stylePrettyLogs: finalConfig.prettyPrint
|
|
221
|
+
});
|
|
222
|
+
if (finalConfig.logToFile && finalConfig.logDir) {
|
|
223
|
+
setupFileLogging(logger2, finalConfig.logDir, finalConfig.name);
|
|
224
|
+
}
|
|
225
|
+
return logger2;
|
|
226
|
+
}
|
|
227
|
+
function setupFileLogging(logger2, logDir, name) {
|
|
228
|
+
if (!fs4__default.existsSync(logDir)) {
|
|
229
|
+
fs4__default.mkdirSync(logDir, { recursive: true });
|
|
230
|
+
}
|
|
231
|
+
const logFile = path17__default.join(logDir, `${name}.log`);
|
|
232
|
+
logger2.attachTransport((logObj) => {
|
|
233
|
+
const line = JSON.stringify(logObj) + "\n";
|
|
234
|
+
fs4__default.appendFileSync(logFile, line);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function getLogger() {
|
|
238
|
+
if (!globalLogger) {
|
|
239
|
+
globalLogger = createLogger();
|
|
240
|
+
}
|
|
241
|
+
return globalLogger;
|
|
242
|
+
}
|
|
243
|
+
var DEFAULT_CONFIG, globalLogger;
|
|
244
|
+
var init_logger = __esm({
|
|
245
|
+
"src/utils/logger.ts"() {
|
|
246
|
+
DEFAULT_CONFIG = {
|
|
247
|
+
name: "coco",
|
|
248
|
+
level: "info",
|
|
249
|
+
prettyPrint: true,
|
|
250
|
+
logToFile: false
|
|
251
|
+
};
|
|
252
|
+
globalLogger = null;
|
|
253
|
+
}
|
|
254
|
+
});
|
|
201
255
|
async function refreshAccessToken(provider, refreshToken) {
|
|
202
256
|
const config = OAUTH_CONFIGS[provider];
|
|
203
257
|
if (!config) {
|
|
@@ -309,8 +363,276 @@ var init_pkce = __esm({
|
|
|
309
363
|
"src/auth/pkce.ts"() {
|
|
310
364
|
}
|
|
311
365
|
});
|
|
366
|
+
function escapeHtml(unsafe) {
|
|
367
|
+
return unsafe.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
368
|
+
}
|
|
369
|
+
async function createCallbackServer(expectedState, timeout = 5 * 60 * 1e3, port = OAUTH_CALLBACK_PORT) {
|
|
370
|
+
let resolveResult;
|
|
371
|
+
let rejectResult;
|
|
372
|
+
const resultPromise = new Promise((resolve3, reject) => {
|
|
373
|
+
resolveResult = resolve3;
|
|
374
|
+
rejectResult = reject;
|
|
375
|
+
});
|
|
376
|
+
const server = http.createServer((req, res) => {
|
|
377
|
+
console.log(` [OAuth] ${req.method} ${req.url?.split("?")[0]}`);
|
|
378
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
379
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
380
|
+
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
381
|
+
if (req.method === "OPTIONS") {
|
|
382
|
+
res.writeHead(204);
|
|
383
|
+
res.end();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
if (!req.url?.startsWith("/auth/callback")) {
|
|
387
|
+
res.writeHead(404);
|
|
388
|
+
res.end("Not Found");
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const url = new URL$1(req.url, `http://localhost`);
|
|
393
|
+
const code = url.searchParams.get("code");
|
|
394
|
+
const state = url.searchParams.get("state");
|
|
395
|
+
const error = url.searchParams.get("error");
|
|
396
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
397
|
+
if (error) {
|
|
398
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
399
|
+
res.end(ERROR_HTML(errorDescription || error));
|
|
400
|
+
server.close();
|
|
401
|
+
rejectResult(new Error(errorDescription || error));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!code || !state) {
|
|
405
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
406
|
+
res.end(ERROR_HTML("Missing authorization code or state"));
|
|
407
|
+
server.close();
|
|
408
|
+
rejectResult(new Error("Missing authorization code or state"));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (state !== expectedState) {
|
|
412
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
413
|
+
res.end(ERROR_HTML("State mismatch - possible CSRF attack"));
|
|
414
|
+
server.close();
|
|
415
|
+
rejectResult(new Error("State mismatch - possible CSRF attack"));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
419
|
+
res.end(SUCCESS_HTML);
|
|
420
|
+
server.close();
|
|
421
|
+
resolveResult({ code, state });
|
|
422
|
+
} catch (err) {
|
|
423
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
424
|
+
res.end(ERROR_HTML(String(err)));
|
|
425
|
+
server.close();
|
|
426
|
+
rejectResult(err instanceof Error ? err : new Error(String(err)));
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
const actualPort = await new Promise((resolve3, reject) => {
|
|
430
|
+
const errorHandler = (err) => {
|
|
431
|
+
if (err.code === "EADDRINUSE") {
|
|
432
|
+
console.log(` Port ${port} is in use, trying alternative port...`);
|
|
433
|
+
server.removeListener("error", errorHandler);
|
|
434
|
+
server.listen(0, () => {
|
|
435
|
+
const address = server.address();
|
|
436
|
+
if (typeof address === "object" && address) {
|
|
437
|
+
resolve3(address.port);
|
|
438
|
+
} else {
|
|
439
|
+
reject(new Error("Failed to get server port"));
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
reject(err);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
server.on("error", errorHandler);
|
|
447
|
+
server.listen(port, () => {
|
|
448
|
+
server.removeListener("error", errorHandler);
|
|
449
|
+
const address = server.address();
|
|
450
|
+
if (typeof address === "object" && address) {
|
|
451
|
+
resolve3(address.port);
|
|
452
|
+
} else {
|
|
453
|
+
reject(new Error("Failed to get server port"));
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
const timeoutId = setTimeout(() => {
|
|
458
|
+
server.close();
|
|
459
|
+
rejectResult(new Error("Authentication timed out. Please try again."));
|
|
460
|
+
}, timeout);
|
|
461
|
+
server.on("close", () => {
|
|
462
|
+
clearTimeout(timeoutId);
|
|
463
|
+
});
|
|
464
|
+
return { port: actualPort, resultPromise };
|
|
465
|
+
}
|
|
466
|
+
var OAUTH_CALLBACK_PORT, SUCCESS_HTML, ERROR_HTML;
|
|
312
467
|
var init_callback_server = __esm({
|
|
313
468
|
"src/auth/callback-server.ts"() {
|
|
469
|
+
OAUTH_CALLBACK_PORT = 1455;
|
|
470
|
+
SUCCESS_HTML = `
|
|
471
|
+
<!DOCTYPE html>
|
|
472
|
+
<html>
|
|
473
|
+
<head>
|
|
474
|
+
<meta charset="utf-8">
|
|
475
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
476
|
+
<title>Authentication Successful</title>
|
|
477
|
+
<style>
|
|
478
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
479
|
+
body {
|
|
480
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
481
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
482
|
+
min-height: 100vh;
|
|
483
|
+
display: flex;
|
|
484
|
+
align-items: center;
|
|
485
|
+
justify-content: center;
|
|
486
|
+
}
|
|
487
|
+
.container {
|
|
488
|
+
background: white;
|
|
489
|
+
border-radius: 16px;
|
|
490
|
+
padding: 48px;
|
|
491
|
+
text-align: center;
|
|
492
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
493
|
+
max-width: 400px;
|
|
494
|
+
}
|
|
495
|
+
.checkmark {
|
|
496
|
+
width: 80px;
|
|
497
|
+
height: 80px;
|
|
498
|
+
margin: 0 auto 24px;
|
|
499
|
+
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
500
|
+
border-radius: 50%;
|
|
501
|
+
display: flex;
|
|
502
|
+
align-items: center;
|
|
503
|
+
justify-content: center;
|
|
504
|
+
}
|
|
505
|
+
.checkmark svg {
|
|
506
|
+
width: 40px;
|
|
507
|
+
height: 40px;
|
|
508
|
+
stroke: white;
|
|
509
|
+
stroke-width: 3;
|
|
510
|
+
fill: none;
|
|
511
|
+
}
|
|
512
|
+
h1 {
|
|
513
|
+
font-size: 24px;
|
|
514
|
+
font-weight: 600;
|
|
515
|
+
color: #1f2937;
|
|
516
|
+
margin-bottom: 12px;
|
|
517
|
+
}
|
|
518
|
+
p {
|
|
519
|
+
color: #6b7280;
|
|
520
|
+
font-size: 16px;
|
|
521
|
+
line-height: 1.5;
|
|
522
|
+
}
|
|
523
|
+
.brand {
|
|
524
|
+
margin-top: 24px;
|
|
525
|
+
padding-top: 24px;
|
|
526
|
+
border-top: 1px solid #e5e7eb;
|
|
527
|
+
color: #9ca3af;
|
|
528
|
+
font-size: 14px;
|
|
529
|
+
}
|
|
530
|
+
.brand strong {
|
|
531
|
+
color: #667eea;
|
|
532
|
+
}
|
|
533
|
+
</style>
|
|
534
|
+
</head>
|
|
535
|
+
<body>
|
|
536
|
+
<div class="container">
|
|
537
|
+
<div class="checkmark">
|
|
538
|
+
<svg viewBox="0 0 24 24">
|
|
539
|
+
<path d="M20 6L9 17l-5-5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
540
|
+
</svg>
|
|
541
|
+
</div>
|
|
542
|
+
<h1>Authentication Successful!</h1>
|
|
543
|
+
<p>You can close this window and return to your terminal.</p>
|
|
544
|
+
<div class="brand">
|
|
545
|
+
Powered by <strong>Corbat-Coco</strong>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
<script>
|
|
549
|
+
// Auto-close after 3 seconds
|
|
550
|
+
setTimeout(() => window.close(), 3000);
|
|
551
|
+
</script>
|
|
552
|
+
</body>
|
|
553
|
+
</html>
|
|
554
|
+
`;
|
|
555
|
+
ERROR_HTML = (error) => {
|
|
556
|
+
const safeError = escapeHtml(error);
|
|
557
|
+
return `
|
|
558
|
+
<!DOCTYPE html>
|
|
559
|
+
<html>
|
|
560
|
+
<head>
|
|
561
|
+
<meta charset="utf-8">
|
|
562
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
563
|
+
<title>Authentication Failed</title>
|
|
564
|
+
<style>
|
|
565
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
566
|
+
body {
|
|
567
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
568
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
569
|
+
min-height: 100vh;
|
|
570
|
+
display: flex;
|
|
571
|
+
align-items: center;
|
|
572
|
+
justify-content: center;
|
|
573
|
+
}
|
|
574
|
+
.container {
|
|
575
|
+
background: white;
|
|
576
|
+
border-radius: 16px;
|
|
577
|
+
padding: 48px;
|
|
578
|
+
text-align: center;
|
|
579
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
|
580
|
+
max-width: 400px;
|
|
581
|
+
}
|
|
582
|
+
.icon {
|
|
583
|
+
width: 80px;
|
|
584
|
+
height: 80px;
|
|
585
|
+
margin: 0 auto 24px;
|
|
586
|
+
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
587
|
+
border-radius: 50%;
|
|
588
|
+
display: flex;
|
|
589
|
+
align-items: center;
|
|
590
|
+
justify-content: center;
|
|
591
|
+
}
|
|
592
|
+
.icon svg {
|
|
593
|
+
width: 40px;
|
|
594
|
+
height: 40px;
|
|
595
|
+
stroke: white;
|
|
596
|
+
stroke-width: 3;
|
|
597
|
+
fill: none;
|
|
598
|
+
}
|
|
599
|
+
h1 {
|
|
600
|
+
font-size: 24px;
|
|
601
|
+
font-weight: 600;
|
|
602
|
+
color: #1f2937;
|
|
603
|
+
margin-bottom: 12px;
|
|
604
|
+
}
|
|
605
|
+
p {
|
|
606
|
+
color: #6b7280;
|
|
607
|
+
font-size: 16px;
|
|
608
|
+
line-height: 1.5;
|
|
609
|
+
}
|
|
610
|
+
.error {
|
|
611
|
+
margin-top: 16px;
|
|
612
|
+
padding: 12px;
|
|
613
|
+
background: #fef2f2;
|
|
614
|
+
border-radius: 8px;
|
|
615
|
+
color: #dc2626;
|
|
616
|
+
font-family: monospace;
|
|
617
|
+
font-size: 14px;
|
|
618
|
+
}
|
|
619
|
+
</style>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div class="container">
|
|
623
|
+
<div class="icon">
|
|
624
|
+
<svg viewBox="0 0 24 24">
|
|
625
|
+
<path d="M18 6L6 18M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
626
|
+
</svg>
|
|
627
|
+
</div>
|
|
628
|
+
<h1>Authentication Failed</h1>
|
|
629
|
+
<p>Something went wrong. Please try again.</p>
|
|
630
|
+
<div class="error">${safeError}</div>
|
|
631
|
+
</div>
|
|
632
|
+
</body>
|
|
633
|
+
</html>
|
|
634
|
+
`;
|
|
635
|
+
};
|
|
314
636
|
}
|
|
315
637
|
});
|
|
316
638
|
function detectWSL() {
|
|
@@ -506,6 +828,33 @@ var init_auth = __esm({
|
|
|
506
828
|
init_gcloud();
|
|
507
829
|
}
|
|
508
830
|
});
|
|
831
|
+
|
|
832
|
+
// src/config/schema.ts
|
|
833
|
+
var schema_exports = {};
|
|
834
|
+
__export(schema_exports, {
|
|
835
|
+
CocoConfigSchema: () => CocoConfigSchema,
|
|
836
|
+
GitHubConfigSchema: () => GitHubConfigSchema,
|
|
837
|
+
IntegrationsConfigSchema: () => IntegrationsConfigSchema,
|
|
838
|
+
MCPConfigSchema: () => MCPConfigSchema,
|
|
839
|
+
MCPServerConfigEntrySchema: () => MCPServerConfigEntrySchema,
|
|
840
|
+
PersistenceConfigSchema: () => PersistenceConfigSchema,
|
|
841
|
+
ProjectConfigSchema: () => ProjectConfigSchema2,
|
|
842
|
+
ProviderConfigSchema: () => ProviderConfigSchema,
|
|
843
|
+
QualityConfigSchema: () => QualityConfigSchema,
|
|
844
|
+
ShipConfigSchema: () => ShipConfigSchema,
|
|
845
|
+
SkillsConfigSchema: () => SkillsConfigSchema,
|
|
846
|
+
StackConfigSchema: () => StackConfigSchema,
|
|
847
|
+
ToolsConfigSchema: () => ToolsConfigSchema,
|
|
848
|
+
createDefaultConfigObject: () => createDefaultConfigObject,
|
|
849
|
+
validateConfig: () => validateConfig
|
|
850
|
+
});
|
|
851
|
+
function validateConfig(config) {
|
|
852
|
+
const result = CocoConfigSchema.safeParse(config);
|
|
853
|
+
if (result.success) {
|
|
854
|
+
return { success: true, data: result.data };
|
|
855
|
+
}
|
|
856
|
+
return { success: false, error: result.error };
|
|
857
|
+
}
|
|
509
858
|
function createDefaultConfigObject(projectName, language = "typescript") {
|
|
510
859
|
return {
|
|
511
860
|
project: {
|
|
@@ -721,7 +1070,7 @@ var init_schema = __esm({
|
|
|
721
1070
|
});
|
|
722
1071
|
}
|
|
723
1072
|
});
|
|
724
|
-
var COCO_HOME, CONFIG_PATHS;
|
|
1073
|
+
var COCO_HOME, CONFIG_PATHS, LEGACY_PATHS;
|
|
725
1074
|
var init_paths = __esm({
|
|
726
1075
|
"src/config/paths.ts"() {
|
|
727
1076
|
COCO_HOME = join(homedir(), ".coco");
|
|
@@ -755,14 +1104,28 @@ var init_paths = __esm({
|
|
|
755
1104
|
/** MCP server registry: ~/.coco/mcp.json */
|
|
756
1105
|
mcpRegistry: join(COCO_HOME, "mcp.json")
|
|
757
1106
|
};
|
|
758
|
-
|
|
1107
|
+
LEGACY_PATHS = {
|
|
759
1108
|
/** Old config location */
|
|
760
1109
|
oldConfig: join(homedir(), ".config", "corbat-coco"),
|
|
761
1110
|
/** Old MCP config directory (pre-unification) */
|
|
762
1111
|
oldMcpDir: join(homedir(), ".config", "coco", "mcp")
|
|
763
|
-
}
|
|
1112
|
+
};
|
|
764
1113
|
}
|
|
765
1114
|
});
|
|
1115
|
+
|
|
1116
|
+
// src/config/loader.ts
|
|
1117
|
+
var loader_exports = {};
|
|
1118
|
+
__export(loader_exports, {
|
|
1119
|
+
configExists: () => configExists,
|
|
1120
|
+
createDefaultConfig: () => createDefaultConfig,
|
|
1121
|
+
findAllConfigPaths: () => findAllConfigPaths,
|
|
1122
|
+
findConfigPath: () => findConfigPath,
|
|
1123
|
+
getConfigValue: () => getConfigValue,
|
|
1124
|
+
loadConfig: () => loadConfig,
|
|
1125
|
+
mergeWithDefaults: () => mergeWithDefaults,
|
|
1126
|
+
saveConfig: () => saveConfig,
|
|
1127
|
+
setConfigValue: () => setConfigValue
|
|
1128
|
+
});
|
|
766
1129
|
async function loadConfig(configPath) {
|
|
767
1130
|
let config = createDefaultConfig("my-project");
|
|
768
1131
|
const globalConfig = await loadConfigFile(CONFIG_PATHS.config, { strict: false });
|
|
@@ -858,6 +1221,45 @@ async function saveConfig(config, configPath, global = false) {
|
|
|
858
1221
|
function createDefaultConfig(projectName, language = "typescript") {
|
|
859
1222
|
return createDefaultConfigObject(projectName, language);
|
|
860
1223
|
}
|
|
1224
|
+
async function findConfigPath(cwd) {
|
|
1225
|
+
const envPath = process.env["COCO_CONFIG_PATH"];
|
|
1226
|
+
if (envPath) {
|
|
1227
|
+
try {
|
|
1228
|
+
await fs16__default.access(envPath);
|
|
1229
|
+
return envPath;
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const basePath = cwd || process.cwd();
|
|
1234
|
+
const projectConfigPath = path17__default.join(basePath, ".coco", "config.json");
|
|
1235
|
+
try {
|
|
1236
|
+
await fs16__default.access(projectConfigPath);
|
|
1237
|
+
return projectConfigPath;
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
try {
|
|
1241
|
+
await fs16__default.access(CONFIG_PATHS.config);
|
|
1242
|
+
return CONFIG_PATHS.config;
|
|
1243
|
+
} catch {
|
|
1244
|
+
return void 0;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
async function findAllConfigPaths(cwd) {
|
|
1248
|
+
const result = {};
|
|
1249
|
+
try {
|
|
1250
|
+
await fs16__default.access(CONFIG_PATHS.config);
|
|
1251
|
+
result.global = CONFIG_PATHS.config;
|
|
1252
|
+
} catch {
|
|
1253
|
+
}
|
|
1254
|
+
const basePath = cwd || process.cwd();
|
|
1255
|
+
const projectConfigPath = path17__default.join(basePath, ".coco", "config.json");
|
|
1256
|
+
try {
|
|
1257
|
+
await fs16__default.access(projectConfigPath);
|
|
1258
|
+
result.project = projectConfigPath;
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
return result;
|
|
1262
|
+
}
|
|
861
1263
|
async function configExists(configPath, scope = "any") {
|
|
862
1264
|
if (configPath) {
|
|
863
1265
|
try {
|
|
@@ -885,6 +1287,45 @@ async function configExists(configPath, scope = "any") {
|
|
|
885
1287
|
}
|
|
886
1288
|
return false;
|
|
887
1289
|
}
|
|
1290
|
+
function getConfigValue(config, path42) {
|
|
1291
|
+
const keys = path42.split(".");
|
|
1292
|
+
let current = config;
|
|
1293
|
+
for (const key of keys) {
|
|
1294
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
1295
|
+
return void 0;
|
|
1296
|
+
}
|
|
1297
|
+
current = current[key];
|
|
1298
|
+
}
|
|
1299
|
+
return current;
|
|
1300
|
+
}
|
|
1301
|
+
function setConfigValue(config, configPath, value) {
|
|
1302
|
+
const keys = configPath.split(".");
|
|
1303
|
+
const result = structuredClone(config);
|
|
1304
|
+
let current = result;
|
|
1305
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1306
|
+
const key = keys[i];
|
|
1307
|
+
if (!key) continue;
|
|
1308
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") {
|
|
1309
|
+
throw new Error(`Invalid config path: cannot set ${key}`);
|
|
1310
|
+
}
|
|
1311
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
1312
|
+
current[key] = {};
|
|
1313
|
+
}
|
|
1314
|
+
current = current[key];
|
|
1315
|
+
}
|
|
1316
|
+
const lastKey = keys[keys.length - 1];
|
|
1317
|
+
if (lastKey) {
|
|
1318
|
+
if (lastKey === "__proto__" || lastKey === "constructor" || lastKey === "prototype") {
|
|
1319
|
+
throw new Error(`Invalid config path: cannot set ${lastKey}`);
|
|
1320
|
+
}
|
|
1321
|
+
current[lastKey] = value;
|
|
1322
|
+
}
|
|
1323
|
+
return result;
|
|
1324
|
+
}
|
|
1325
|
+
function mergeWithDefaults(partial, projectName) {
|
|
1326
|
+
const defaults = createDefaultConfig(projectName);
|
|
1327
|
+
return deepMergeConfig(defaults, partial);
|
|
1328
|
+
}
|
|
888
1329
|
var init_loader = __esm({
|
|
889
1330
|
"src/config/loader.ts"() {
|
|
890
1331
|
init_schema();
|
|
@@ -1007,7 +1448,7 @@ function getDefaultModel(provider) {
|
|
|
1007
1448
|
case "codex":
|
|
1008
1449
|
return process.env["CODEX_MODEL"] ?? "codex-mini-latest";
|
|
1009
1450
|
case "copilot":
|
|
1010
|
-
return process.env["COPILOT_MODEL"] ?? "
|
|
1451
|
+
return process.env["COPILOT_MODEL"] ?? "claude-sonnet-4.6";
|
|
1011
1452
|
case "groq":
|
|
1012
1453
|
return process.env["GROQ_MODEL"] ?? "llama-3.3-70b-versatile";
|
|
1013
1454
|
case "openrouter":
|
|
@@ -1203,123 +1644,553 @@ var init_heartbeat = __esm({
|
|
|
1203
1644
|
}
|
|
1204
1645
|
});
|
|
1205
1646
|
|
|
1206
|
-
// src/
|
|
1207
|
-
var
|
|
1208
|
-
|
|
1209
|
-
promptAllowPath: () => promptAllowPath
|
|
1210
|
-
});
|
|
1211
|
-
async function promptAllowPath(dirPath) {
|
|
1212
|
-
const absolute = path17__default.resolve(dirPath);
|
|
1213
|
-
console.log();
|
|
1214
|
-
console.log(chalk5.yellow(" \u26A0 Access denied \u2014 path is outside the project directory"));
|
|
1215
|
-
console.log(chalk5.dim(` \u{1F4C1} ${absolute}`));
|
|
1216
|
-
console.log();
|
|
1217
|
-
const action = await p4.select({
|
|
1218
|
-
message: "Grant access to this directory?",
|
|
1219
|
-
options: [
|
|
1220
|
-
{ value: "session-write", label: "\u2713 Allow write (this session)" },
|
|
1221
|
-
{ value: "session-read", label: "\u25D0 Allow read-only (this session)" },
|
|
1222
|
-
{ value: "persist-write", label: "\u26A1 Allow write (remember for this project)" },
|
|
1223
|
-
{ value: "persist-read", label: "\u{1F4BE} Allow read-only (remember for this project)" },
|
|
1224
|
-
{ value: "no", label: "\u2717 Deny" }
|
|
1225
|
-
]
|
|
1226
|
-
});
|
|
1227
|
-
if (p4.isCancel(action) || action === "no") {
|
|
1228
|
-
return false;
|
|
1647
|
+
// src/mcp/types.ts
|
|
1648
|
+
var init_types = __esm({
|
|
1649
|
+
"src/mcp/types.ts"() {
|
|
1229
1650
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1651
|
+
});
|
|
1652
|
+
|
|
1653
|
+
// src/mcp/errors.ts
|
|
1654
|
+
var MCPError, MCPTransportError, MCPConnectionError, MCPTimeoutError;
|
|
1655
|
+
var init_errors2 = __esm({
|
|
1656
|
+
"src/mcp/errors.ts"() {
|
|
1657
|
+
init_types();
|
|
1658
|
+
MCPError = class extends Error {
|
|
1659
|
+
code;
|
|
1660
|
+
constructor(code, message) {
|
|
1661
|
+
super(message);
|
|
1662
|
+
this.name = "MCPError";
|
|
1663
|
+
this.code = code;
|
|
1664
|
+
}
|
|
1665
|
+
};
|
|
1666
|
+
MCPTransportError = class extends MCPError {
|
|
1667
|
+
constructor(message) {
|
|
1668
|
+
super(-32001 /* TRANSPORT_ERROR */, message);
|
|
1669
|
+
this.name = "MCPTransportError";
|
|
1670
|
+
}
|
|
1671
|
+
};
|
|
1672
|
+
MCPConnectionError = class extends MCPError {
|
|
1673
|
+
constructor(message) {
|
|
1674
|
+
super(-32003 /* CONNECTION_ERROR */, message);
|
|
1675
|
+
this.name = "MCPConnectionError";
|
|
1676
|
+
}
|
|
1677
|
+
};
|
|
1678
|
+
MCPTimeoutError = class extends MCPError {
|
|
1679
|
+
constructor(message = "Request timed out") {
|
|
1680
|
+
super(-32002 /* TIMEOUT_ERROR */, message);
|
|
1681
|
+
this.name = "MCPTimeoutError";
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1235
1684
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
return true;
|
|
1685
|
+
});
|
|
1686
|
+
function getDefaultRegistryPath() {
|
|
1687
|
+
return CONFIG_PATHS.mcpRegistry;
|
|
1240
1688
|
}
|
|
1241
|
-
|
|
1242
|
-
"
|
|
1243
|
-
|
|
1689
|
+
function validateServerConfig(config) {
|
|
1690
|
+
if (!config || typeof config !== "object") {
|
|
1691
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "Server config must be an object");
|
|
1244
1692
|
}
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1693
|
+
const cfg = config;
|
|
1694
|
+
if (!cfg.name || typeof cfg.name !== "string") {
|
|
1695
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "Server name is required and must be a string");
|
|
1696
|
+
}
|
|
1697
|
+
if (cfg.name.length < 1 || cfg.name.length > 64) {
|
|
1698
|
+
throw new MCPError(
|
|
1699
|
+
-32602 /* INVALID_PARAMS */,
|
|
1700
|
+
"Server name must be between 1 and 64 characters"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(cfg.name)) {
|
|
1704
|
+
throw new MCPError(
|
|
1705
|
+
-32602 /* INVALID_PARAMS */,
|
|
1706
|
+
"Server name must contain only letters, numbers, underscores, and hyphens"
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
if (!cfg.transport || cfg.transport !== "stdio" && cfg.transport !== "http") {
|
|
1710
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, 'Transport must be "stdio" or "http"');
|
|
1711
|
+
}
|
|
1712
|
+
if (cfg.transport === "stdio") {
|
|
1713
|
+
if (!cfg.stdio || typeof cfg.stdio !== "object") {
|
|
1714
|
+
throw new MCPError(
|
|
1715
|
+
-32602 /* INVALID_PARAMS */,
|
|
1716
|
+
"stdio transport requires stdio configuration"
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
const stdio = cfg.stdio;
|
|
1720
|
+
if (!stdio.command || typeof stdio.command !== "string") {
|
|
1721
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "stdio.command is required");
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
if (cfg.transport === "http") {
|
|
1725
|
+
if (!cfg.http || typeof cfg.http !== "object") {
|
|
1726
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "http transport requires http configuration");
|
|
1727
|
+
}
|
|
1728
|
+
const http2 = cfg.http;
|
|
1729
|
+
if (!http2.url || typeof http2.url !== "string") {
|
|
1730
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "http.url is required");
|
|
1731
|
+
}
|
|
1249
1732
|
try {
|
|
1250
|
-
|
|
1251
|
-
const pkg = JSON.parse(content);
|
|
1252
|
-
if (pkg.name === "@corbat-tech/coco") return pkg;
|
|
1733
|
+
new URL(http2.url);
|
|
1253
1734
|
} catch {
|
|
1735
|
+
throw new MCPError(-32602 /* INVALID_PARAMS */, "http.url must be a valid URL");
|
|
1254
1736
|
}
|
|
1255
|
-
dir = dirname(dir);
|
|
1256
1737
|
}
|
|
1257
|
-
return { version: "0.0.0" };
|
|
1258
1738
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
Your goals:
|
|
1265
|
-
1. Understand what the user wants to build
|
|
1266
|
-
2. Extract clear, actionable requirements
|
|
1267
|
-
3. Identify ambiguities and ask clarifying questions
|
|
1268
|
-
4. Make reasonable assumptions when appropriate
|
|
1269
|
-
5. Recommend technology choices when needed
|
|
1270
|
-
|
|
1271
|
-
Guidelines:
|
|
1272
|
-
- Be thorough but not overwhelming
|
|
1273
|
-
- Ask focused, specific questions
|
|
1274
|
-
- Group related questions together
|
|
1275
|
-
- Prioritize questions by importance
|
|
1276
|
-
- Make assumptions for minor details
|
|
1277
|
-
- Always explain your reasoning
|
|
1278
|
-
|
|
1279
|
-
You communicate in a professional but friendly manner. You use concrete examples to clarify abstract requirements.`;
|
|
1280
|
-
var INITIAL_ANALYSIS_PROMPT = `Analyze the following project description and extract:
|
|
1281
|
-
|
|
1282
|
-
1. **Project Type**: What kind of software is this? (CLI, API, web app, library, service, etc.)
|
|
1283
|
-
2. **Complexity**: How complex is this project? (simple, moderate, complex, enterprise)
|
|
1284
|
-
3. **Completeness**: How complete is the description? (0-100%)
|
|
1285
|
-
4. **Functional Requirements**: What should the system do?
|
|
1286
|
-
5. **Non-Functional Requirements**: Performance, security, scalability needs
|
|
1287
|
-
6. **Technical Constraints**: Any specified technologies or limitations
|
|
1288
|
-
7. **Assumptions**: What must we assume to proceed?
|
|
1289
|
-
8. **Critical Questions**: What must be clarified before proceeding?
|
|
1290
|
-
9. **Technology Recommendations**: What tech stack would you recommend?
|
|
1291
|
-
|
|
1292
|
-
User's project description:
|
|
1293
|
-
---
|
|
1294
|
-
{{userInput}}
|
|
1295
|
-
---
|
|
1296
|
-
|
|
1297
|
-
Respond in JSON format:
|
|
1298
|
-
{
|
|
1299
|
-
"projectType": "string",
|
|
1300
|
-
"complexity": "simple|moderate|complex|enterprise",
|
|
1301
|
-
"completeness": number,
|
|
1302
|
-
"requirements": [
|
|
1303
|
-
{
|
|
1304
|
-
"category": "functional|non_functional|technical|constraint",
|
|
1305
|
-
"priority": "must_have|should_have|could_have|wont_have",
|
|
1306
|
-
"title": "string",
|
|
1307
|
-
"description": "string",
|
|
1308
|
-
"explicit": boolean,
|
|
1309
|
-
"acceptanceCriteria": ["string"]
|
|
1310
|
-
}
|
|
1311
|
-
],
|
|
1312
|
-
"assumptions": [
|
|
1313
|
-
{
|
|
1314
|
-
"category": "string",
|
|
1315
|
-
"statement": "string",
|
|
1316
|
-
"confidence": "high|medium|low",
|
|
1317
|
-
"impactIfWrong": "string"
|
|
1739
|
+
function parseRegistry(json2) {
|
|
1740
|
+
try {
|
|
1741
|
+
const parsed = JSON.parse(json2);
|
|
1742
|
+
if (!parsed.servers || !Array.isArray(parsed.servers)) {
|
|
1743
|
+
return [];
|
|
1318
1744
|
}
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1745
|
+
return parsed.servers;
|
|
1746
|
+
} catch {
|
|
1747
|
+
return [];
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function serializeRegistry(servers) {
|
|
1751
|
+
return JSON.stringify({ servers, version: "1.0" }, null, 2);
|
|
1752
|
+
}
|
|
1753
|
+
async function migrateMCPData(opts) {
|
|
1754
|
+
const oldDir = LEGACY_PATHS.oldMcpDir;
|
|
1755
|
+
const newRegistry = CONFIG_PATHS.mcpRegistry;
|
|
1756
|
+
const newConfig = CONFIG_PATHS.config;
|
|
1757
|
+
try {
|
|
1758
|
+
await migrateRegistry(oldDir, newRegistry);
|
|
1759
|
+
await migrateGlobalConfig(oldDir, newConfig);
|
|
1760
|
+
} catch (error) {
|
|
1761
|
+
getLogger().warn(
|
|
1762
|
+
`[MCP] Migration failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
async function migrateRegistry(oldDir, newRegistry) {
|
|
1767
|
+
const oldFile = join(oldDir, "registry.json");
|
|
1768
|
+
if (await fileExists3(newRegistry)) return;
|
|
1769
|
+
if (!await fileExists3(oldFile)) return;
|
|
1770
|
+
try {
|
|
1771
|
+
const content = await readFile(oldFile, "utf-8");
|
|
1772
|
+
const servers = parseRegistry(content);
|
|
1773
|
+
await mkdir(dirname(newRegistry), { recursive: true });
|
|
1774
|
+
await writeFile(newRegistry, serializeRegistry(servers), "utf-8");
|
|
1775
|
+
getLogger().info(
|
|
1776
|
+
`[MCP] Migrated registry from ${oldFile} to ${newRegistry}. The old file can be safely deleted.`
|
|
1777
|
+
);
|
|
1778
|
+
} catch (error) {
|
|
1779
|
+
getLogger().warn(
|
|
1780
|
+
`[MCP] Could not migrate registry: ${error instanceof Error ? error.message : String(error)}`
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
async function migrateGlobalConfig(oldDir, newConfigPath) {
|
|
1785
|
+
const oldFile = join(oldDir, "config.json");
|
|
1786
|
+
if (!await fileExists3(oldFile)) return;
|
|
1787
|
+
try {
|
|
1788
|
+
const oldContent = await readFile(oldFile, "utf-8");
|
|
1789
|
+
const oldMcpConfig = JSON.parse(oldContent);
|
|
1790
|
+
let cocoConfig = {};
|
|
1791
|
+
if (await fileExists3(newConfigPath)) {
|
|
1792
|
+
const existing = await readFile(newConfigPath, "utf-8");
|
|
1793
|
+
cocoConfig = JSON.parse(existing);
|
|
1794
|
+
}
|
|
1795
|
+
const existingMcp = cocoConfig.mcp ?? {};
|
|
1796
|
+
const fieldsToMigrate = ["defaultTimeout", "autoDiscover", "logLevel", "customServersPath"];
|
|
1797
|
+
let didMerge = false;
|
|
1798
|
+
for (const field of fieldsToMigrate) {
|
|
1799
|
+
if (oldMcpConfig[field] !== void 0 && existingMcp[field] === void 0) {
|
|
1800
|
+
existingMcp[field] = oldMcpConfig[field];
|
|
1801
|
+
didMerge = true;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
if (!didMerge) return;
|
|
1805
|
+
cocoConfig.mcp = existingMcp;
|
|
1806
|
+
await mkdir(dirname(newConfigPath), { recursive: true });
|
|
1807
|
+
await writeFile(newConfigPath, JSON.stringify(cocoConfig, null, 2), "utf-8");
|
|
1808
|
+
getLogger().info(`[MCP] Migrated global MCP settings from ${oldFile} into ${newConfigPath}.`);
|
|
1809
|
+
} catch (error) {
|
|
1810
|
+
getLogger().warn(
|
|
1811
|
+
`[MCP] Could not migrate global MCP config: ${error instanceof Error ? error.message : String(error)}`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
async function fileExists3(path42) {
|
|
1816
|
+
try {
|
|
1817
|
+
await access(path42);
|
|
1818
|
+
return true;
|
|
1819
|
+
} catch {
|
|
1820
|
+
return false;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
var init_config = __esm({
|
|
1824
|
+
"src/mcp/config.ts"() {
|
|
1825
|
+
init_paths();
|
|
1826
|
+
init_types();
|
|
1827
|
+
init_errors2();
|
|
1828
|
+
init_logger();
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
// src/mcp/config-loader.ts
|
|
1833
|
+
var config_loader_exports = {};
|
|
1834
|
+
__export(config_loader_exports, {
|
|
1835
|
+
loadMCPConfigFile: () => loadMCPConfigFile,
|
|
1836
|
+
loadMCPServersFromCOCOConfig: () => loadMCPServersFromCOCOConfig,
|
|
1837
|
+
loadProjectMCPFile: () => loadProjectMCPFile,
|
|
1838
|
+
mergeMCPConfigs: () => mergeMCPConfigs
|
|
1839
|
+
});
|
|
1840
|
+
function expandEnvVar(value) {
|
|
1841
|
+
return value.replace(/\$\{([^}]+)\}/g, (match, name) => process.env[name] ?? match);
|
|
1842
|
+
}
|
|
1843
|
+
function expandEnvObject(env2) {
|
|
1844
|
+
const result = {};
|
|
1845
|
+
for (const [k, v] of Object.entries(env2)) {
|
|
1846
|
+
result[k] = expandEnvVar(v);
|
|
1847
|
+
}
|
|
1848
|
+
return result;
|
|
1849
|
+
}
|
|
1850
|
+
function expandHeaders(headers) {
|
|
1851
|
+
const result = {};
|
|
1852
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
1853
|
+
result[k] = expandEnvVar(v);
|
|
1854
|
+
}
|
|
1855
|
+
return result;
|
|
1856
|
+
}
|
|
1857
|
+
function convertStandardEntry(name, entry) {
|
|
1858
|
+
if (entry.command) {
|
|
1859
|
+
return {
|
|
1860
|
+
name,
|
|
1861
|
+
transport: "stdio",
|
|
1862
|
+
enabled: entry.enabled ?? true,
|
|
1863
|
+
stdio: {
|
|
1864
|
+
command: entry.command,
|
|
1865
|
+
args: entry.args,
|
|
1866
|
+
env: entry.env ? expandEnvObject(entry.env) : void 0
|
|
1867
|
+
}
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
if (entry.url) {
|
|
1871
|
+
const headers = entry.headers ? expandHeaders(entry.headers) : void 0;
|
|
1872
|
+
const authHeader = headers?.["Authorization"] ?? headers?.["authorization"];
|
|
1873
|
+
let auth;
|
|
1874
|
+
if (authHeader) {
|
|
1875
|
+
if (authHeader.startsWith("Bearer ")) {
|
|
1876
|
+
auth = { type: "bearer", token: authHeader.slice(7) };
|
|
1877
|
+
} else {
|
|
1878
|
+
auth = { type: "apikey", token: authHeader };
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
return {
|
|
1882
|
+
name,
|
|
1883
|
+
transport: "http",
|
|
1884
|
+
enabled: entry.enabled ?? true,
|
|
1885
|
+
http: {
|
|
1886
|
+
url: entry.url,
|
|
1887
|
+
...headers && Object.keys(headers).length > 0 ? { headers } : {},
|
|
1888
|
+
...auth ? { auth } : {}
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
}
|
|
1892
|
+
throw new Error(`Server "${name}" must have either "command" (stdio) or "url" (http) defined`);
|
|
1893
|
+
}
|
|
1894
|
+
async function loadMCPConfigFile(configPath) {
|
|
1895
|
+
try {
|
|
1896
|
+
await access(configPath);
|
|
1897
|
+
} catch {
|
|
1898
|
+
throw new MCPError(-32003 /* CONNECTION_ERROR */, `Config file not found: ${configPath}`);
|
|
1899
|
+
}
|
|
1900
|
+
let content;
|
|
1901
|
+
try {
|
|
1902
|
+
content = await readFile(configPath, "utf-8");
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
throw new MCPError(
|
|
1905
|
+
-32003 /* CONNECTION_ERROR */,
|
|
1906
|
+
`Failed to read config file: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
let parsed;
|
|
1910
|
+
try {
|
|
1911
|
+
parsed = JSON.parse(content);
|
|
1912
|
+
} catch {
|
|
1913
|
+
throw new MCPError(-32700 /* PARSE_ERROR */, "Invalid JSON in config file");
|
|
1914
|
+
}
|
|
1915
|
+
const obj = parsed;
|
|
1916
|
+
if (obj.mcpServers && typeof obj.mcpServers === "object" && !Array.isArray(obj.mcpServers)) {
|
|
1917
|
+
return loadStandardFormat(obj, configPath);
|
|
1918
|
+
}
|
|
1919
|
+
if (obj.servers && Array.isArray(obj.servers)) {
|
|
1920
|
+
return loadCocoFormat(obj, configPath);
|
|
1921
|
+
}
|
|
1922
|
+
throw new MCPError(
|
|
1923
|
+
-32602 /* INVALID_PARAMS */,
|
|
1924
|
+
'Config file must have either a "mcpServers" object (standard) or a "servers" array (Coco format)'
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
function loadStandardFormat(config, configPath) {
|
|
1928
|
+
const validServers = [];
|
|
1929
|
+
const errors = [];
|
|
1930
|
+
for (const [name, entry] of Object.entries(config.mcpServers)) {
|
|
1931
|
+
if (name.startsWith("_")) continue;
|
|
1932
|
+
try {
|
|
1933
|
+
const converted = convertStandardEntry(name, entry);
|
|
1934
|
+
validateServerConfig(converted);
|
|
1935
|
+
validServers.push(converted);
|
|
1936
|
+
} catch (error) {
|
|
1937
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1938
|
+
errors.push(`Server '${name}': ${message}`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
if (errors.length > 0) {
|
|
1942
|
+
getLogger().warn(`[MCP] Some servers in ${configPath} failed to load: ${errors.join("; ")}`);
|
|
1943
|
+
}
|
|
1944
|
+
return validServers;
|
|
1945
|
+
}
|
|
1946
|
+
async function loadProjectMCPFile(projectPath) {
|
|
1947
|
+
const mcpJsonPath = path17__default.join(projectPath, ".mcp.json");
|
|
1948
|
+
try {
|
|
1949
|
+
await access(mcpJsonPath);
|
|
1950
|
+
} catch {
|
|
1951
|
+
return [];
|
|
1952
|
+
}
|
|
1953
|
+
try {
|
|
1954
|
+
return await loadMCPConfigFile(mcpJsonPath);
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
getLogger().warn(
|
|
1957
|
+
`[MCP] Failed to load .mcp.json: ${error instanceof Error ? error.message : String(error)}`
|
|
1958
|
+
);
|
|
1959
|
+
return [];
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
function loadCocoFormat(config, configPath) {
|
|
1963
|
+
const validServers = [];
|
|
1964
|
+
const errors = [];
|
|
1965
|
+
for (const server of config.servers) {
|
|
1966
|
+
try {
|
|
1967
|
+
const converted = convertCocoServerEntry(server);
|
|
1968
|
+
validateServerConfig(converted);
|
|
1969
|
+
validServers.push(converted);
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
1972
|
+
errors.push(`Server '${server.name || "unknown"}': ${message}`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
if (errors.length > 0) {
|
|
1976
|
+
getLogger().warn(`[MCP] Some servers in ${configPath} failed to load: ${errors.join("; ")}`);
|
|
1977
|
+
}
|
|
1978
|
+
return validServers;
|
|
1979
|
+
}
|
|
1980
|
+
function convertCocoServerEntry(server) {
|
|
1981
|
+
const base = {
|
|
1982
|
+
name: server.name,
|
|
1983
|
+
description: server.description,
|
|
1984
|
+
transport: server.transport,
|
|
1985
|
+
enabled: server.enabled ?? true,
|
|
1986
|
+
metadata: server.metadata
|
|
1987
|
+
};
|
|
1988
|
+
if (server.transport === "stdio" && server.stdio) {
|
|
1989
|
+
return {
|
|
1990
|
+
...base,
|
|
1991
|
+
stdio: {
|
|
1992
|
+
command: server.stdio.command,
|
|
1993
|
+
args: server.stdio.args,
|
|
1994
|
+
env: server.stdio.env ? expandEnvObject(server.stdio.env) : void 0,
|
|
1995
|
+
cwd: server.stdio.cwd
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1999
|
+
if (server.transport === "http" && server.http) {
|
|
2000
|
+
return {
|
|
2001
|
+
...base,
|
|
2002
|
+
http: {
|
|
2003
|
+
url: server.http.url,
|
|
2004
|
+
...server.http.headers ? { headers: expandHeaders(server.http.headers) } : {},
|
|
2005
|
+
...server.http.auth ? { auth: server.http.auth } : {},
|
|
2006
|
+
...server.http.timeout !== void 0 ? { timeout: server.http.timeout } : {}
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
throw new Error(`Missing configuration for transport: ${server.transport}`);
|
|
2011
|
+
}
|
|
2012
|
+
function mergeMCPConfigs(base, ...overrides) {
|
|
2013
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2014
|
+
for (const server of base) {
|
|
2015
|
+
merged.set(server.name, server);
|
|
2016
|
+
}
|
|
2017
|
+
for (const override of overrides) {
|
|
2018
|
+
for (const server of override) {
|
|
2019
|
+
const existing = merged.get(server.name);
|
|
2020
|
+
if (existing) {
|
|
2021
|
+
merged.set(server.name, { ...existing, ...server });
|
|
2022
|
+
} else {
|
|
2023
|
+
merged.set(server.name, server);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return Array.from(merged.values());
|
|
2028
|
+
}
|
|
2029
|
+
async function loadMCPServersFromCOCOConfig(configPath) {
|
|
2030
|
+
const { loadConfig: loadConfig2 } = await Promise.resolve().then(() => (init_loader(), loader_exports));
|
|
2031
|
+
const { MCPServerConfigEntrySchema: MCPServerConfigEntrySchema2 } = await Promise.resolve().then(() => (init_schema(), schema_exports));
|
|
2032
|
+
const config = await loadConfig2(configPath);
|
|
2033
|
+
if (!config.mcp?.servers || config.mcp.servers.length === 0) {
|
|
2034
|
+
return [];
|
|
2035
|
+
}
|
|
2036
|
+
const servers = [];
|
|
2037
|
+
for (const entry of config.mcp.servers) {
|
|
2038
|
+
try {
|
|
2039
|
+
const parsed = MCPServerConfigEntrySchema2.parse(entry);
|
|
2040
|
+
const serverConfig = {
|
|
2041
|
+
name: parsed.name,
|
|
2042
|
+
description: parsed.description,
|
|
2043
|
+
transport: parsed.transport,
|
|
2044
|
+
enabled: parsed.enabled,
|
|
2045
|
+
...parsed.transport === "stdio" && parsed.command && {
|
|
2046
|
+
stdio: {
|
|
2047
|
+
command: parsed.command,
|
|
2048
|
+
args: parsed.args,
|
|
2049
|
+
env: parsed.env ? expandEnvObject(parsed.env) : void 0
|
|
2050
|
+
}
|
|
2051
|
+
},
|
|
2052
|
+
...parsed.transport === "http" && parsed.url && {
|
|
2053
|
+
http: {
|
|
2054
|
+
url: parsed.url,
|
|
2055
|
+
auth: parsed.auth
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
2059
|
+
validateServerConfig(serverConfig);
|
|
2060
|
+
servers.push(serverConfig);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
2063
|
+
getLogger().warn(`[MCP] Failed to load server '${entry.name}': ${message}`);
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return servers;
|
|
2067
|
+
}
|
|
2068
|
+
var init_config_loader = __esm({
|
|
2069
|
+
"src/mcp/config-loader.ts"() {
|
|
2070
|
+
init_config();
|
|
2071
|
+
init_types();
|
|
2072
|
+
init_errors2();
|
|
2073
|
+
init_logger();
|
|
2074
|
+
}
|
|
2075
|
+
});
|
|
2076
|
+
|
|
2077
|
+
// src/cli/repl/allow-path-prompt.ts
|
|
2078
|
+
var allow_path_prompt_exports = {};
|
|
2079
|
+
__export(allow_path_prompt_exports, {
|
|
2080
|
+
promptAllowPath: () => promptAllowPath
|
|
2081
|
+
});
|
|
2082
|
+
async function promptAllowPath(dirPath) {
|
|
2083
|
+
const absolute = path17__default.resolve(dirPath);
|
|
2084
|
+
console.log();
|
|
2085
|
+
console.log(chalk5.yellow(" \u26A0 Access denied \u2014 path is outside the project directory"));
|
|
2086
|
+
console.log(chalk5.dim(` \u{1F4C1} ${absolute}`));
|
|
2087
|
+
console.log();
|
|
2088
|
+
const action = await p4.select({
|
|
2089
|
+
message: "Grant access to this directory?",
|
|
2090
|
+
options: [
|
|
2091
|
+
{ value: "session-write", label: "\u2713 Allow write (this session)" },
|
|
2092
|
+
{ value: "session-read", label: "\u25D0 Allow read-only (this session)" },
|
|
2093
|
+
{ value: "persist-write", label: "\u26A1 Allow write (remember for this project)" },
|
|
2094
|
+
{ value: "persist-read", label: "\u{1F4BE} Allow read-only (remember for this project)" },
|
|
2095
|
+
{ value: "no", label: "\u2717 Deny" }
|
|
2096
|
+
]
|
|
2097
|
+
});
|
|
2098
|
+
if (p4.isCancel(action) || action === "no") {
|
|
2099
|
+
return false;
|
|
2100
|
+
}
|
|
2101
|
+
const level = action.includes("read") ? "read" : "write";
|
|
2102
|
+
const persist = action.startsWith("persist");
|
|
2103
|
+
addAllowedPathToSession(absolute, level);
|
|
2104
|
+
if (persist) {
|
|
2105
|
+
await persistAllowedPath(absolute, level);
|
|
2106
|
+
}
|
|
2107
|
+
const levelLabel = level === "write" ? "write" : "read-only";
|
|
2108
|
+
const persistLabel = persist ? " (remembered)" : "";
|
|
2109
|
+
console.log(chalk5.green(` \u2713 Access granted: ${levelLabel}${persistLabel}`));
|
|
2110
|
+
return true;
|
|
2111
|
+
}
|
|
2112
|
+
var init_allow_path_prompt = __esm({
|
|
2113
|
+
"src/cli/repl/allow-path-prompt.ts"() {
|
|
2114
|
+
init_allowed_paths();
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
function findPackageJson() {
|
|
2118
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
2119
|
+
for (let i = 0; i < 10; i++) {
|
|
2120
|
+
try {
|
|
2121
|
+
const content = readFileSync(join(dir, "package.json"), "utf-8");
|
|
2122
|
+
const pkg = JSON.parse(content);
|
|
2123
|
+
if (pkg.name === "@corbat-tech/coco") return pkg;
|
|
2124
|
+
} catch {
|
|
2125
|
+
}
|
|
2126
|
+
dir = dirname(dir);
|
|
2127
|
+
}
|
|
2128
|
+
return { version: "0.0.0" };
|
|
2129
|
+
}
|
|
2130
|
+
var VERSION = findPackageJson().version;
|
|
2131
|
+
|
|
2132
|
+
// src/phases/converge/prompts.ts
|
|
2133
|
+
var DISCOVERY_SYSTEM_PROMPT = `You are a senior software architect and requirements analyst. Your role is to help gather and clarify requirements for software projects.
|
|
2134
|
+
|
|
2135
|
+
Your goals:
|
|
2136
|
+
1. Understand what the user wants to build
|
|
2137
|
+
2. Extract clear, actionable requirements
|
|
2138
|
+
3. Identify ambiguities and ask clarifying questions
|
|
2139
|
+
4. Make reasonable assumptions when appropriate
|
|
2140
|
+
5. Recommend technology choices when needed
|
|
2141
|
+
|
|
2142
|
+
Guidelines:
|
|
2143
|
+
- Be thorough but not overwhelming
|
|
2144
|
+
- Ask focused, specific questions
|
|
2145
|
+
- Group related questions together
|
|
2146
|
+
- Prioritize questions by importance
|
|
2147
|
+
- Make assumptions for minor details
|
|
2148
|
+
- Always explain your reasoning
|
|
2149
|
+
|
|
2150
|
+
You communicate in a professional but friendly manner. You use concrete examples to clarify abstract requirements.`;
|
|
2151
|
+
var INITIAL_ANALYSIS_PROMPT = `Analyze the following project description and extract:
|
|
2152
|
+
|
|
2153
|
+
1. **Project Type**: What kind of software is this? (CLI, API, web app, library, service, etc.)
|
|
2154
|
+
2. **Complexity**: How complex is this project? (simple, moderate, complex, enterprise)
|
|
2155
|
+
3. **Completeness**: How complete is the description? (0-100%)
|
|
2156
|
+
4. **Functional Requirements**: What should the system do?
|
|
2157
|
+
5. **Non-Functional Requirements**: Performance, security, scalability needs
|
|
2158
|
+
6. **Technical Constraints**: Any specified technologies or limitations
|
|
2159
|
+
7. **Assumptions**: What must we assume to proceed?
|
|
2160
|
+
8. **Critical Questions**: What must be clarified before proceeding?
|
|
2161
|
+
9. **Technology Recommendations**: What tech stack would you recommend?
|
|
2162
|
+
|
|
2163
|
+
User's project description:
|
|
2164
|
+
---
|
|
2165
|
+
{{userInput}}
|
|
2166
|
+
---
|
|
2167
|
+
|
|
2168
|
+
Respond in JSON format:
|
|
2169
|
+
{
|
|
2170
|
+
"projectType": "string",
|
|
2171
|
+
"complexity": "simple|moderate|complex|enterprise",
|
|
2172
|
+
"completeness": number,
|
|
2173
|
+
"requirements": [
|
|
2174
|
+
{
|
|
2175
|
+
"category": "functional|non_functional|technical|constraint",
|
|
2176
|
+
"priority": "must_have|should_have|could_have|wont_have",
|
|
2177
|
+
"title": "string",
|
|
2178
|
+
"description": "string",
|
|
2179
|
+
"explicit": boolean,
|
|
2180
|
+
"acceptanceCriteria": ["string"]
|
|
2181
|
+
}
|
|
2182
|
+
],
|
|
2183
|
+
"assumptions": [
|
|
2184
|
+
{
|
|
2185
|
+
"category": "string",
|
|
2186
|
+
"statement": "string",
|
|
2187
|
+
"confidence": "high|medium|low",
|
|
2188
|
+
"impactIfWrong": "string"
|
|
2189
|
+
}
|
|
2190
|
+
],
|
|
2191
|
+
"questions": [
|
|
2192
|
+
{
|
|
2193
|
+
"category": "clarification|expansion|decision|confirmation|scope|priority",
|
|
1323
2194
|
"question": "string",
|
|
1324
2195
|
"context": "string",
|
|
1325
2196
|
"importance": "critical|important|helpful",
|
|
@@ -8666,16 +9537,16 @@ var QualityEvaluator = class {
|
|
|
8666
9537
|
* Find source files in project, adapting to the detected language stack.
|
|
8667
9538
|
*/
|
|
8668
9539
|
async findSourceFiles() {
|
|
8669
|
-
const { access:
|
|
8670
|
-
const { join:
|
|
9540
|
+
const { access: access13 } = await import('fs/promises');
|
|
9541
|
+
const { join: join19 } = await import('path');
|
|
8671
9542
|
let isJava = false;
|
|
8672
9543
|
try {
|
|
8673
|
-
await
|
|
9544
|
+
await access13(join19(this.projectPath, "pom.xml"));
|
|
8674
9545
|
isJava = true;
|
|
8675
9546
|
} catch {
|
|
8676
9547
|
for (const f of ["build.gradle", "build.gradle.kts"]) {
|
|
8677
9548
|
try {
|
|
8678
|
-
await
|
|
9549
|
+
await access13(join19(this.projectPath, f));
|
|
8679
9550
|
isJava = true;
|
|
8680
9551
|
break;
|
|
8681
9552
|
} catch {
|
|
@@ -8976,55 +9847,9 @@ function buildFeedbackSection(feedback, issues) {
|
|
|
8976
9847
|
|
|
8977
9848
|
// src/phases/complete/generator.ts
|
|
8978
9849
|
init_errors();
|
|
8979
|
-
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
prettyPrint: true,
|
|
8983
|
-
logToFile: false
|
|
8984
|
-
};
|
|
8985
|
-
function levelToNumber(level) {
|
|
8986
|
-
const levels = {
|
|
8987
|
-
silly: 0,
|
|
8988
|
-
trace: 1,
|
|
8989
|
-
debug: 2,
|
|
8990
|
-
info: 3,
|
|
8991
|
-
warn: 4,
|
|
8992
|
-
error: 5,
|
|
8993
|
-
fatal: 6
|
|
8994
|
-
};
|
|
8995
|
-
return levels[level];
|
|
8996
|
-
}
|
|
8997
|
-
function createLogger(config = {}) {
|
|
8998
|
-
const finalConfig = { ...DEFAULT_CONFIG, ...config };
|
|
8999
|
-
const logger = new Logger({
|
|
9000
|
-
name: finalConfig.name,
|
|
9001
|
-
minLevel: levelToNumber(finalConfig.level),
|
|
9002
|
-
prettyLogTemplate: finalConfig.prettyPrint ? "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}} {{logLevelName}} [{{name}}] " : void 0,
|
|
9003
|
-
prettyLogTimeZone: "local",
|
|
9004
|
-
stylePrettyLogs: finalConfig.prettyPrint
|
|
9005
|
-
});
|
|
9006
|
-
if (finalConfig.logToFile && finalConfig.logDir) {
|
|
9007
|
-
setupFileLogging(logger, finalConfig.logDir, finalConfig.name);
|
|
9008
|
-
}
|
|
9009
|
-
return logger;
|
|
9010
|
-
}
|
|
9011
|
-
function setupFileLogging(logger, logDir, name) {
|
|
9012
|
-
if (!fs4__default.existsSync(logDir)) {
|
|
9013
|
-
fs4__default.mkdirSync(logDir, { recursive: true });
|
|
9014
|
-
}
|
|
9015
|
-
const logFile = path17__default.join(logDir, `${name}.log`);
|
|
9016
|
-
logger.attachTransport((logObj) => {
|
|
9017
|
-
const line = JSON.stringify(logObj) + "\n";
|
|
9018
|
-
fs4__default.appendFileSync(logFile, line);
|
|
9019
|
-
});
|
|
9020
|
-
}
|
|
9021
|
-
var globalLogger = null;
|
|
9022
|
-
function getLogger() {
|
|
9023
|
-
if (!globalLogger) {
|
|
9024
|
-
globalLogger = createLogger();
|
|
9025
|
-
}
|
|
9026
|
-
return globalLogger;
|
|
9027
|
-
}
|
|
9850
|
+
|
|
9851
|
+
// src/tools/registry.ts
|
|
9852
|
+
init_logger();
|
|
9028
9853
|
|
|
9029
9854
|
// src/utils/error-humanizer.ts
|
|
9030
9855
|
function extractQuotedPath(msg) {
|
|
@@ -9075,12 +9900,12 @@ function humanizeError(message, toolName) {
|
|
|
9075
9900
|
return msg;
|
|
9076
9901
|
}
|
|
9077
9902
|
if (/ENOENT/i.test(msg)) {
|
|
9078
|
-
const
|
|
9079
|
-
return
|
|
9903
|
+
const path42 = extractQuotedPath(msg);
|
|
9904
|
+
return path42 ? `File or directory not found: ${path42}` : "File or directory not found";
|
|
9080
9905
|
}
|
|
9081
9906
|
if (/EACCES/i.test(msg)) {
|
|
9082
|
-
const
|
|
9083
|
-
return
|
|
9907
|
+
const path42 = extractQuotedPath(msg);
|
|
9908
|
+
return path42 ? `Permission denied: ${path42}` : "Permission denied \u2014 check file permissions";
|
|
9084
9909
|
}
|
|
9085
9910
|
if (/EISDIR/i.test(msg)) {
|
|
9086
9911
|
return "Expected a file but found a directory at the specified path";
|
|
@@ -12536,6 +13361,7 @@ async function withRetry(fn, config = {}) {
|
|
|
12536
13361
|
}
|
|
12537
13362
|
|
|
12538
13363
|
// src/providers/anthropic.ts
|
|
13364
|
+
init_logger();
|
|
12539
13365
|
var DEFAULT_MODEL = "claude-opus-4-6";
|
|
12540
13366
|
var CONTEXT_WINDOWS = {
|
|
12541
13367
|
// Kimi Code model (Anthropic-compatible endpoint)
|
|
@@ -15068,6 +15894,11 @@ var CONTEXT_WINDOWS4 = {
|
|
|
15068
15894
|
"gemini-2.5-pro": 1048576
|
|
15069
15895
|
};
|
|
15070
15896
|
var DEFAULT_MODEL4 = "claude-sonnet-4.6";
|
|
15897
|
+
function normalizeModel(model) {
|
|
15898
|
+
if (typeof model !== "string") return void 0;
|
|
15899
|
+
const trimmed = model.trim();
|
|
15900
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
15901
|
+
}
|
|
15071
15902
|
var COPILOT_HEADERS = {
|
|
15072
15903
|
"Copilot-Integration-Id": "vscode-chat",
|
|
15073
15904
|
"Editor-Version": "vscode/1.99.0",
|
|
@@ -15091,7 +15922,7 @@ var CopilotProvider = class extends OpenAIProvider {
|
|
|
15091
15922
|
async initialize(config) {
|
|
15092
15923
|
this.config = {
|
|
15093
15924
|
...config,
|
|
15094
|
-
model: config.model ?? DEFAULT_MODEL4
|
|
15925
|
+
model: normalizeModel(config.model) ?? DEFAULT_MODEL4
|
|
15095
15926
|
};
|
|
15096
15927
|
const tokenResult = await getValidCopilotToken();
|
|
15097
15928
|
if (tokenResult) {
|
|
@@ -15977,12 +16808,17 @@ function createResilientProvider(provider, config) {
|
|
|
15977
16808
|
init_copilot();
|
|
15978
16809
|
init_errors();
|
|
15979
16810
|
init_env();
|
|
16811
|
+
function normalizeProviderModel(model) {
|
|
16812
|
+
if (typeof model !== "string") return void 0;
|
|
16813
|
+
const trimmed = model.trim();
|
|
16814
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
16815
|
+
}
|
|
15980
16816
|
async function createProvider(type, config = {}) {
|
|
15981
16817
|
let provider;
|
|
15982
16818
|
const mergedConfig = {
|
|
15983
16819
|
apiKey: config.apiKey ?? getApiKey(type),
|
|
15984
16820
|
baseUrl: config.baseUrl ?? getBaseUrl(type),
|
|
15985
|
-
model: config.model ?? getDefaultModel(type),
|
|
16821
|
+
model: normalizeProviderModel(config.model) ?? getDefaultModel(type),
|
|
15986
16822
|
maxTokens: config.maxTokens,
|
|
15987
16823
|
temperature: config.temperature,
|
|
15988
16824
|
timeout: config.timeout
|
|
@@ -16171,9 +17007,9 @@ function createInitialState(config) {
|
|
|
16171
17007
|
}
|
|
16172
17008
|
async function loadExistingState(projectPath) {
|
|
16173
17009
|
try {
|
|
16174
|
-
const
|
|
17010
|
+
const fs39 = await import('fs/promises');
|
|
16175
17011
|
const statePath = `${projectPath}/.coco/state/project.json`;
|
|
16176
|
-
const content = await
|
|
17012
|
+
const content = await fs39.readFile(statePath, "utf-8");
|
|
16177
17013
|
const data = JSON.parse(content);
|
|
16178
17014
|
data.createdAt = new Date(data.createdAt);
|
|
16179
17015
|
data.updatedAt = new Date(data.updatedAt);
|
|
@@ -16183,13 +17019,13 @@ async function loadExistingState(projectPath) {
|
|
|
16183
17019
|
}
|
|
16184
17020
|
}
|
|
16185
17021
|
async function saveState(state) {
|
|
16186
|
-
const
|
|
17022
|
+
const fs39 = await import('fs/promises');
|
|
16187
17023
|
const statePath = `${state.path}/.coco/state`;
|
|
16188
|
-
await
|
|
17024
|
+
await fs39.mkdir(statePath, { recursive: true });
|
|
16189
17025
|
const filePath = `${statePath}/project.json`;
|
|
16190
17026
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
16191
|
-
await
|
|
16192
|
-
await
|
|
17027
|
+
await fs39.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
17028
|
+
await fs39.rename(tmpPath, filePath);
|
|
16193
17029
|
}
|
|
16194
17030
|
function getPhaseExecutor(phase) {
|
|
16195
17031
|
switch (phase) {
|
|
@@ -16248,20 +17084,20 @@ async function createPhaseContext(config, state) {
|
|
|
16248
17084
|
};
|
|
16249
17085
|
const tools = {
|
|
16250
17086
|
file: {
|
|
16251
|
-
async read(
|
|
16252
|
-
const
|
|
16253
|
-
return
|
|
17087
|
+
async read(path42) {
|
|
17088
|
+
const fs39 = await import('fs/promises');
|
|
17089
|
+
return fs39.readFile(path42, "utf-8");
|
|
16254
17090
|
},
|
|
16255
|
-
async write(
|
|
16256
|
-
const
|
|
17091
|
+
async write(path42, content) {
|
|
17092
|
+
const fs39 = await import('fs/promises');
|
|
16257
17093
|
const nodePath = await import('path');
|
|
16258
|
-
await
|
|
16259
|
-
await
|
|
17094
|
+
await fs39.mkdir(nodePath.dirname(path42), { recursive: true });
|
|
17095
|
+
await fs39.writeFile(path42, content, "utf-8");
|
|
16260
17096
|
},
|
|
16261
|
-
async exists(
|
|
16262
|
-
const
|
|
17097
|
+
async exists(path42) {
|
|
17098
|
+
const fs39 = await import('fs/promises');
|
|
16263
17099
|
try {
|
|
16264
|
-
await
|
|
17100
|
+
await fs39.access(path42);
|
|
16265
17101
|
return true;
|
|
16266
17102
|
} catch {
|
|
16267
17103
|
return false;
|
|
@@ -16410,9 +17246,9 @@ async function createSnapshot(state) {
|
|
|
16410
17246
|
var MAX_CHECKPOINT_VERSIONS = 5;
|
|
16411
17247
|
async function getCheckpointFiles(state, phase) {
|
|
16412
17248
|
try {
|
|
16413
|
-
const
|
|
17249
|
+
const fs39 = await import('fs/promises');
|
|
16414
17250
|
const checkpointDir = `${state.path}/.coco/checkpoints`;
|
|
16415
|
-
const files = await
|
|
17251
|
+
const files = await fs39.readdir(checkpointDir);
|
|
16416
17252
|
const phaseFiles = files.filter((f) => f.startsWith(`snapshot-pre-${phase}-`) && f.endsWith(".json")).sort((a, b) => {
|
|
16417
17253
|
const tsA = parseInt(a.split("-").pop()?.replace(".json", "") ?? "0", 10);
|
|
16418
17254
|
const tsB = parseInt(b.split("-").pop()?.replace(".json", "") ?? "0", 10);
|
|
@@ -16425,11 +17261,11 @@ async function getCheckpointFiles(state, phase) {
|
|
|
16425
17261
|
}
|
|
16426
17262
|
async function cleanupOldCheckpoints(state, phase) {
|
|
16427
17263
|
try {
|
|
16428
|
-
const
|
|
17264
|
+
const fs39 = await import('fs/promises');
|
|
16429
17265
|
const files = await getCheckpointFiles(state, phase);
|
|
16430
17266
|
if (files.length > MAX_CHECKPOINT_VERSIONS) {
|
|
16431
17267
|
const filesToDelete = files.slice(MAX_CHECKPOINT_VERSIONS);
|
|
16432
|
-
await Promise.all(filesToDelete.map((f) =>
|
|
17268
|
+
await Promise.all(filesToDelete.map((f) => fs39.unlink(f).catch(() => {
|
|
16433
17269
|
})));
|
|
16434
17270
|
}
|
|
16435
17271
|
} catch {
|
|
@@ -16437,13 +17273,13 @@ async function cleanupOldCheckpoints(state, phase) {
|
|
|
16437
17273
|
}
|
|
16438
17274
|
async function saveSnapshot(state, snapshotId) {
|
|
16439
17275
|
try {
|
|
16440
|
-
const
|
|
17276
|
+
const fs39 = await import('fs/promises');
|
|
16441
17277
|
const snapshotPath = `${state.path}/.coco/checkpoints/snapshot-${snapshotId}.json`;
|
|
16442
17278
|
const snapshotDir = `${state.path}/.coco/checkpoints`;
|
|
16443
|
-
await
|
|
17279
|
+
await fs39.mkdir(snapshotDir, { recursive: true });
|
|
16444
17280
|
const createdAt = state.createdAt instanceof Date ? state.createdAt.toISOString() : String(state.createdAt);
|
|
16445
17281
|
const updatedAt = state.updatedAt instanceof Date ? state.updatedAt.toISOString() : String(state.updatedAt);
|
|
16446
|
-
await
|
|
17282
|
+
await fs39.writeFile(
|
|
16447
17283
|
snapshotPath,
|
|
16448
17284
|
JSON.stringify(
|
|
16449
17285
|
{
|
|
@@ -16735,20 +17571,69 @@ var SENSITIVE_PATTERNS = [
|
|
|
16735
17571
|
// PyPI auth
|
|
16736
17572
|
];
|
|
16737
17573
|
var BLOCKED_PATHS = ["/etc", "/var", "/usr", "/root", "/sys", "/proc", "/boot"];
|
|
17574
|
+
var SAFE_COCO_HOME_READ_FILES = /* @__PURE__ */ new Set([
|
|
17575
|
+
"mcp.json",
|
|
17576
|
+
"config.json",
|
|
17577
|
+
"COCO.md",
|
|
17578
|
+
"AGENTS.md",
|
|
17579
|
+
"CLAUDE.md",
|
|
17580
|
+
"projects.json",
|
|
17581
|
+
"trusted-tools.json",
|
|
17582
|
+
"allowed-paths.json"
|
|
17583
|
+
]);
|
|
17584
|
+
var SAFE_COCO_HOME_READ_DIR_PREFIXES = ["skills", "memories", "logs", "checkpoints", "sessions"];
|
|
16738
17585
|
function hasNullByte(str) {
|
|
16739
17586
|
return str.includes("\0");
|
|
16740
17587
|
}
|
|
16741
17588
|
function normalizePath(filePath) {
|
|
16742
17589
|
let normalized = filePath.replace(/\0/g, "");
|
|
17590
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
17591
|
+
if (home && normalized.startsWith("~")) {
|
|
17592
|
+
if (normalized === "~") {
|
|
17593
|
+
normalized = home;
|
|
17594
|
+
} else if (normalized.startsWith("~/") || normalized.startsWith(`~${path17__default.sep}`)) {
|
|
17595
|
+
normalized = path17__default.join(home, normalized.slice(2));
|
|
17596
|
+
}
|
|
17597
|
+
}
|
|
16743
17598
|
normalized = path17__default.normalize(normalized);
|
|
16744
17599
|
return normalized;
|
|
16745
17600
|
}
|
|
17601
|
+
function resolveUserPath(filePath) {
|
|
17602
|
+
return path17__default.resolve(normalizePath(filePath));
|
|
17603
|
+
}
|
|
17604
|
+
function isWithinDirectory(targetPath, baseDir) {
|
|
17605
|
+
const normalizedTarget = path17__default.normalize(targetPath);
|
|
17606
|
+
const normalizedBase = path17__default.normalize(baseDir);
|
|
17607
|
+
return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + path17__default.sep);
|
|
17608
|
+
}
|
|
17609
|
+
function isSafeCocoHomeReadPath(absolutePath, homeDir) {
|
|
17610
|
+
const cocoHome = path17__default.join(homeDir, ".coco");
|
|
17611
|
+
if (!isWithinDirectory(absolutePath, cocoHome)) {
|
|
17612
|
+
return false;
|
|
17613
|
+
}
|
|
17614
|
+
const relativePath = path17__default.relative(cocoHome, absolutePath);
|
|
17615
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
17616
|
+
return false;
|
|
17617
|
+
}
|
|
17618
|
+
const segments = relativePath.split(path17__default.sep).filter(Boolean);
|
|
17619
|
+
const firstSegment = segments[0];
|
|
17620
|
+
if (!firstSegment) {
|
|
17621
|
+
return false;
|
|
17622
|
+
}
|
|
17623
|
+
if (firstSegment === "tokens" || firstSegment === ".env") {
|
|
17624
|
+
return false;
|
|
17625
|
+
}
|
|
17626
|
+
if (segments.length === 1 && SAFE_COCO_HOME_READ_FILES.has(firstSegment)) {
|
|
17627
|
+
return true;
|
|
17628
|
+
}
|
|
17629
|
+
return SAFE_COCO_HOME_READ_DIR_PREFIXES.includes(firstSegment);
|
|
17630
|
+
}
|
|
16746
17631
|
function isPathAllowed(filePath, operation) {
|
|
16747
17632
|
if (hasNullByte(filePath)) {
|
|
16748
17633
|
return { allowed: false, reason: "Path contains invalid characters" };
|
|
16749
17634
|
}
|
|
16750
17635
|
const normalized = normalizePath(filePath);
|
|
16751
|
-
const absolute =
|
|
17636
|
+
const absolute = resolveUserPath(normalized);
|
|
16752
17637
|
const cwd = process.cwd();
|
|
16753
17638
|
for (const blocked of BLOCKED_PATHS) {
|
|
16754
17639
|
const normalizedBlocked = path17__default.normalize(blocked);
|
|
@@ -16762,6 +17647,9 @@ function isPathAllowed(filePath, operation) {
|
|
|
16762
17647
|
const normalizedCwd = path17__default.normalize(cwd);
|
|
16763
17648
|
if (absolute.startsWith(normalizedHome) && !absolute.startsWith(normalizedCwd)) {
|
|
16764
17649
|
if (isWithinAllowedPath(absolute, operation)) ; else if (operation === "read") {
|
|
17650
|
+
if (isSafeCocoHomeReadPath(absolute, normalizedHome)) {
|
|
17651
|
+
return { allowed: true };
|
|
17652
|
+
}
|
|
16765
17653
|
const allowedHomeReads = [".gitconfig", ".zshrc", ".bashrc"];
|
|
16766
17654
|
const basename5 = path17__default.basename(absolute);
|
|
16767
17655
|
if (!allowedHomeReads.includes(basename5)) {
|
|
@@ -16804,7 +17692,7 @@ function isENOENT(error) {
|
|
|
16804
17692
|
return error.code === "ENOENT";
|
|
16805
17693
|
}
|
|
16806
17694
|
async function enrichENOENT(filePath, operation) {
|
|
16807
|
-
const absPath =
|
|
17695
|
+
const absPath = resolveUserPath(filePath);
|
|
16808
17696
|
const suggestions = await suggestSimilarFilesDeep(absPath, process.cwd());
|
|
16809
17697
|
const hint = formatSuggestions(suggestions, path17__default.dirname(absPath));
|
|
16810
17698
|
const action = operation === "read" ? "Use glob or list_dir to find the correct path." : "Check that the parent directory exists.";
|
|
@@ -16812,7 +17700,7 @@ async function enrichENOENT(filePath, operation) {
|
|
|
16812
17700
|
${action}`;
|
|
16813
17701
|
}
|
|
16814
17702
|
async function enrichDirENOENT(dirPath) {
|
|
16815
|
-
const absPath =
|
|
17703
|
+
const absPath = resolveUserPath(dirPath);
|
|
16816
17704
|
const suggestions = await suggestSimilarDirsDeep(absPath, process.cwd());
|
|
16817
17705
|
const hint = formatSuggestions(suggestions, path17__default.dirname(absPath));
|
|
16818
17706
|
return `Directory not found: ${dirPath}${hint}
|
|
@@ -16835,7 +17723,7 @@ Examples:
|
|
|
16835
17723
|
async execute({ path: filePath, encoding, maxSize }) {
|
|
16836
17724
|
validatePath(filePath, "read");
|
|
16837
17725
|
try {
|
|
16838
|
-
const absolutePath =
|
|
17726
|
+
const absolutePath = resolveUserPath(filePath);
|
|
16839
17727
|
const stats = await fs16__default.stat(absolutePath);
|
|
16840
17728
|
const maxBytes = maxSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
16841
17729
|
let truncated = false;
|
|
@@ -16894,7 +17782,7 @@ Examples:
|
|
|
16894
17782
|
async execute({ path: filePath, content, createDirs, dryRun }) {
|
|
16895
17783
|
validatePath(filePath, "write");
|
|
16896
17784
|
try {
|
|
16897
|
-
const absolutePath =
|
|
17785
|
+
const absolutePath = resolveUserPath(filePath);
|
|
16898
17786
|
let wouldCreate = false;
|
|
16899
17787
|
try {
|
|
16900
17788
|
await fs16__default.access(absolutePath);
|
|
@@ -16956,7 +17844,7 @@ Examples:
|
|
|
16956
17844
|
async execute({ path: filePath, oldText, newText, all, dryRun }) {
|
|
16957
17845
|
validatePath(filePath, "write");
|
|
16958
17846
|
try {
|
|
16959
|
-
const absolutePath =
|
|
17847
|
+
const absolutePath = resolveUserPath(filePath);
|
|
16960
17848
|
let content = await fs16__default.readFile(absolutePath, "utf-8");
|
|
16961
17849
|
let replacements = 0;
|
|
16962
17850
|
if (all) {
|
|
@@ -17077,7 +17965,7 @@ Examples:
|
|
|
17077
17965
|
}),
|
|
17078
17966
|
async execute({ path: filePath }) {
|
|
17079
17967
|
try {
|
|
17080
|
-
const absolutePath =
|
|
17968
|
+
const absolutePath = resolveUserPath(filePath);
|
|
17081
17969
|
const stats = await fs16__default.stat(absolutePath);
|
|
17082
17970
|
return {
|
|
17083
17971
|
exists: true,
|
|
@@ -17108,7 +17996,7 @@ Examples:
|
|
|
17108
17996
|
}),
|
|
17109
17997
|
async execute({ path: dirPath, recursive }) {
|
|
17110
17998
|
try {
|
|
17111
|
-
const absolutePath =
|
|
17999
|
+
const absolutePath = resolveUserPath(dirPath);
|
|
17112
18000
|
const entries = [];
|
|
17113
18001
|
async function listDir(dir, prefix = "") {
|
|
17114
18002
|
const items = await fs16__default.readdir(dir, { withFileTypes: true });
|
|
@@ -17168,7 +18056,7 @@ Examples:
|
|
|
17168
18056
|
}
|
|
17169
18057
|
validatePath(filePath, "delete");
|
|
17170
18058
|
try {
|
|
17171
|
-
const absolutePath =
|
|
18059
|
+
const absolutePath = resolveUserPath(filePath);
|
|
17172
18060
|
const stats = await fs16__default.stat(absolutePath);
|
|
17173
18061
|
if (stats.isDirectory()) {
|
|
17174
18062
|
if (!recursive) {
|
|
@@ -17184,7 +18072,7 @@ Examples:
|
|
|
17184
18072
|
} catch (error) {
|
|
17185
18073
|
if (error instanceof ToolError) throw error;
|
|
17186
18074
|
if (error.code === "ENOENT") {
|
|
17187
|
-
return { deleted: false, path:
|
|
18075
|
+
return { deleted: false, path: resolveUserPath(filePath) };
|
|
17188
18076
|
}
|
|
17189
18077
|
throw new FileSystemError(`Failed to delete: ${filePath}`, {
|
|
17190
18078
|
path: filePath,
|
|
@@ -17212,8 +18100,8 @@ Examples:
|
|
|
17212
18100
|
validatePath(source, "read");
|
|
17213
18101
|
validatePath(destination, "write");
|
|
17214
18102
|
try {
|
|
17215
|
-
const srcPath =
|
|
17216
|
-
const destPath =
|
|
18103
|
+
const srcPath = resolveUserPath(source);
|
|
18104
|
+
const destPath = resolveUserPath(destination);
|
|
17217
18105
|
if (!overwrite) {
|
|
17218
18106
|
try {
|
|
17219
18107
|
await fs16__default.access(destPath);
|
|
@@ -17273,8 +18161,8 @@ Examples:
|
|
|
17273
18161
|
validatePath(source, "delete");
|
|
17274
18162
|
validatePath(destination, "write");
|
|
17275
18163
|
try {
|
|
17276
|
-
const srcPath =
|
|
17277
|
-
const destPath =
|
|
18164
|
+
const srcPath = resolveUserPath(source);
|
|
18165
|
+
const destPath = resolveUserPath(destination);
|
|
17278
18166
|
if (!overwrite) {
|
|
17279
18167
|
try {
|
|
17280
18168
|
await fs16__default.access(destPath);
|
|
@@ -17360,7 +18248,7 @@ Examples:
|
|
|
17360
18248
|
}),
|
|
17361
18249
|
async execute({ path: dirPath, depth, showHidden, dirsOnly }) {
|
|
17362
18250
|
try {
|
|
17363
|
-
const absolutePath =
|
|
18251
|
+
const absolutePath = resolveUserPath(dirPath ?? ".");
|
|
17364
18252
|
let totalFiles = 0;
|
|
17365
18253
|
let totalDirs = 0;
|
|
17366
18254
|
const lines = [path17__default.basename(absolutePath) + "/"];
|
|
@@ -18271,6 +19159,9 @@ var simpleAutoCommitTool = defineTool({
|
|
|
18271
19159
|
}
|
|
18272
19160
|
});
|
|
18273
19161
|
var gitSimpleTools = [checkProtectedBranchTool, simpleAutoCommitTool];
|
|
19162
|
+
|
|
19163
|
+
// src/cli/repl/agents/manager.ts
|
|
19164
|
+
init_logger();
|
|
18274
19165
|
var AGENT_NAMES = {
|
|
18275
19166
|
explore: "Explorer",
|
|
18276
19167
|
plan: "Planner",
|
|
@@ -21763,9 +22654,9 @@ async function fileExists(filePath) {
|
|
|
21763
22654
|
return false;
|
|
21764
22655
|
}
|
|
21765
22656
|
}
|
|
21766
|
-
async function fileExists2(
|
|
22657
|
+
async function fileExists2(path42) {
|
|
21767
22658
|
try {
|
|
21768
|
-
await access(
|
|
22659
|
+
await access(path42);
|
|
21769
22660
|
return true;
|
|
21770
22661
|
} catch {
|
|
21771
22662
|
return false;
|
|
@@ -21855,7 +22746,7 @@ async function detectMaturity(cwd) {
|
|
|
21855
22746
|
if (!hasLintConfig && hasPackageJson) {
|
|
21856
22747
|
try {
|
|
21857
22748
|
const pkgRaw = await import('fs/promises').then(
|
|
21858
|
-
(
|
|
22749
|
+
(fs39) => fs39.readFile(join(cwd, "package.json"), "utf-8")
|
|
21859
22750
|
);
|
|
21860
22751
|
const pkg = JSON.parse(pkgRaw);
|
|
21861
22752
|
if (pkg.scripts?.lint || pkg.scripts?.["lint:fix"]) {
|
|
@@ -25782,6 +26673,1624 @@ Examples:
|
|
|
25782
26673
|
}
|
|
25783
26674
|
});
|
|
25784
26675
|
var openTools = [openFileTool];
|
|
26676
|
+
|
|
26677
|
+
// src/mcp/registry.ts
|
|
26678
|
+
init_types();
|
|
26679
|
+
init_config();
|
|
26680
|
+
init_errors2();
|
|
26681
|
+
var MCPRegistryImpl = class {
|
|
26682
|
+
servers = /* @__PURE__ */ new Map();
|
|
26683
|
+
registryPath;
|
|
26684
|
+
constructor(registryPath) {
|
|
26685
|
+
this.registryPath = registryPath || getDefaultRegistryPath();
|
|
26686
|
+
}
|
|
26687
|
+
/**
|
|
26688
|
+
* Add or update a server configuration
|
|
26689
|
+
*/
|
|
26690
|
+
async addServer(config) {
|
|
26691
|
+
validateServerConfig(config);
|
|
26692
|
+
const existing = this.servers.get(config.name);
|
|
26693
|
+
if (existing) {
|
|
26694
|
+
this.servers.set(config.name, { ...existing, ...config });
|
|
26695
|
+
} else {
|
|
26696
|
+
this.servers.set(config.name, config);
|
|
26697
|
+
}
|
|
26698
|
+
await this.save();
|
|
26699
|
+
}
|
|
26700
|
+
/**
|
|
26701
|
+
* Remove a server by name
|
|
26702
|
+
*/
|
|
26703
|
+
async removeServer(name) {
|
|
26704
|
+
const existed = this.servers.has(name);
|
|
26705
|
+
if (existed) {
|
|
26706
|
+
this.servers.delete(name);
|
|
26707
|
+
await this.save();
|
|
26708
|
+
}
|
|
26709
|
+
return existed;
|
|
26710
|
+
}
|
|
26711
|
+
/**
|
|
26712
|
+
* Get a server configuration by name
|
|
26713
|
+
*/
|
|
26714
|
+
getServer(name) {
|
|
26715
|
+
return this.servers.get(name);
|
|
26716
|
+
}
|
|
26717
|
+
/**
|
|
26718
|
+
* List all registered servers
|
|
26719
|
+
*/
|
|
26720
|
+
listServers() {
|
|
26721
|
+
return Array.from(this.servers.values());
|
|
26722
|
+
}
|
|
26723
|
+
/**
|
|
26724
|
+
* List enabled servers only
|
|
26725
|
+
*/
|
|
26726
|
+
listEnabledServers() {
|
|
26727
|
+
return this.listServers().filter((s) => s.enabled !== false);
|
|
26728
|
+
}
|
|
26729
|
+
/**
|
|
26730
|
+
* Check if a server exists
|
|
26731
|
+
*/
|
|
26732
|
+
hasServer(name) {
|
|
26733
|
+
return this.servers.has(name);
|
|
26734
|
+
}
|
|
26735
|
+
/**
|
|
26736
|
+
* Get registry file path
|
|
26737
|
+
*/
|
|
26738
|
+
getRegistryPath() {
|
|
26739
|
+
return this.registryPath;
|
|
26740
|
+
}
|
|
26741
|
+
/**
|
|
26742
|
+
* Save registry to disk
|
|
26743
|
+
*/
|
|
26744
|
+
async save() {
|
|
26745
|
+
try {
|
|
26746
|
+
await this.ensureDir(this.registryPath);
|
|
26747
|
+
const data = serializeRegistry(this.listServers());
|
|
26748
|
+
await writeFile(this.registryPath, data, "utf-8");
|
|
26749
|
+
} catch (error) {
|
|
26750
|
+
throw new MCPError(
|
|
26751
|
+
-32001 /* TRANSPORT_ERROR */,
|
|
26752
|
+
`Failed to save registry: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
26753
|
+
);
|
|
26754
|
+
}
|
|
26755
|
+
}
|
|
26756
|
+
/**
|
|
26757
|
+
* Load registry from disk
|
|
26758
|
+
*/
|
|
26759
|
+
async load() {
|
|
26760
|
+
if (this.registryPath === getDefaultRegistryPath()) {
|
|
26761
|
+
await migrateMCPData();
|
|
26762
|
+
}
|
|
26763
|
+
try {
|
|
26764
|
+
await access(this.registryPath);
|
|
26765
|
+
const content = await readFile(this.registryPath, "utf-8");
|
|
26766
|
+
let servers = parseRegistry(content);
|
|
26767
|
+
if (servers.length === 0) {
|
|
26768
|
+
try {
|
|
26769
|
+
const { loadMCPConfigFile: loadMCPConfigFile2 } = await Promise.resolve().then(() => (init_config_loader(), config_loader_exports));
|
|
26770
|
+
servers = await loadMCPConfigFile2(this.registryPath);
|
|
26771
|
+
} catch {
|
|
26772
|
+
}
|
|
26773
|
+
}
|
|
26774
|
+
this.servers.clear();
|
|
26775
|
+
for (const server of servers) {
|
|
26776
|
+
try {
|
|
26777
|
+
validateServerConfig(server);
|
|
26778
|
+
this.servers.set(server.name, server);
|
|
26779
|
+
} catch {
|
|
26780
|
+
}
|
|
26781
|
+
}
|
|
26782
|
+
} catch {
|
|
26783
|
+
this.servers.clear();
|
|
26784
|
+
}
|
|
26785
|
+
}
|
|
26786
|
+
/**
|
|
26787
|
+
* Ensure directory exists
|
|
26788
|
+
*/
|
|
26789
|
+
async ensureDir(path42) {
|
|
26790
|
+
await mkdir(dirname(path42), { recursive: true });
|
|
26791
|
+
}
|
|
26792
|
+
};
|
|
26793
|
+
|
|
26794
|
+
// src/tools/mcp.ts
|
|
26795
|
+
init_config_loader();
|
|
26796
|
+
|
|
26797
|
+
// src/mcp/client.ts
|
|
26798
|
+
init_errors2();
|
|
26799
|
+
var DEFAULT_REQUEST_TIMEOUT = 6e4;
|
|
26800
|
+
var MCPClientImpl = class {
|
|
26801
|
+
constructor(transport, requestTimeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
26802
|
+
this.transport = transport;
|
|
26803
|
+
this.requestTimeout = requestTimeout;
|
|
26804
|
+
this.setupTransportHandlers();
|
|
26805
|
+
}
|
|
26806
|
+
requestId = 0;
|
|
26807
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
26808
|
+
initialized = false;
|
|
26809
|
+
serverCapabilities = null;
|
|
26810
|
+
/**
|
|
26811
|
+
* Setup transport message handlers
|
|
26812
|
+
*/
|
|
26813
|
+
setupTransportHandlers() {
|
|
26814
|
+
this.transport.onMessage((message) => {
|
|
26815
|
+
this.handleMessage(message);
|
|
26816
|
+
});
|
|
26817
|
+
this.transport.onError((error) => {
|
|
26818
|
+
this.rejectAllPending(error);
|
|
26819
|
+
});
|
|
26820
|
+
this.transport.onClose(() => {
|
|
26821
|
+
this.initialized = false;
|
|
26822
|
+
this.rejectAllPending(new MCPConnectionError("Connection closed"));
|
|
26823
|
+
});
|
|
26824
|
+
}
|
|
26825
|
+
/**
|
|
26826
|
+
* Handle incoming messages from transport
|
|
26827
|
+
*/
|
|
26828
|
+
handleMessage(message) {
|
|
26829
|
+
const pending = this.pendingRequests.get(message.id);
|
|
26830
|
+
if (!pending) return;
|
|
26831
|
+
clearTimeout(pending.timeout);
|
|
26832
|
+
this.pendingRequests.delete(message.id);
|
|
26833
|
+
if (message.error) {
|
|
26834
|
+
pending.reject(new Error(message.error.message));
|
|
26835
|
+
} else {
|
|
26836
|
+
pending.resolve(message.result);
|
|
26837
|
+
}
|
|
26838
|
+
}
|
|
26839
|
+
/**
|
|
26840
|
+
* Reject all pending requests
|
|
26841
|
+
*/
|
|
26842
|
+
rejectAllPending(error) {
|
|
26843
|
+
for (const [, pending] of this.pendingRequests) {
|
|
26844
|
+
clearTimeout(pending.timeout);
|
|
26845
|
+
pending.reject(error);
|
|
26846
|
+
}
|
|
26847
|
+
this.pendingRequests.clear();
|
|
26848
|
+
}
|
|
26849
|
+
/**
|
|
26850
|
+
* Send a request and wait for response
|
|
26851
|
+
*/
|
|
26852
|
+
async sendRequest(method, params) {
|
|
26853
|
+
if (!this.transport.isConnected()) {
|
|
26854
|
+
throw new MCPConnectionError("Transport not connected");
|
|
26855
|
+
}
|
|
26856
|
+
const id = ++this.requestId;
|
|
26857
|
+
const request = {
|
|
26858
|
+
jsonrpc: "2.0",
|
|
26859
|
+
id,
|
|
26860
|
+
method,
|
|
26861
|
+
params
|
|
26862
|
+
};
|
|
26863
|
+
return new Promise((resolve3, reject) => {
|
|
26864
|
+
const timeout = setTimeout(() => {
|
|
26865
|
+
this.pendingRequests.delete(id);
|
|
26866
|
+
reject(new MCPTimeoutError(`Request '${method}' timed out after ${this.requestTimeout}ms`));
|
|
26867
|
+
}, this.requestTimeout);
|
|
26868
|
+
this.pendingRequests.set(id, {
|
|
26869
|
+
resolve: resolve3,
|
|
26870
|
+
reject,
|
|
26871
|
+
timeout
|
|
26872
|
+
});
|
|
26873
|
+
this.transport.send(request).catch((error) => {
|
|
26874
|
+
clearTimeout(timeout);
|
|
26875
|
+
this.pendingRequests.delete(id);
|
|
26876
|
+
reject(error);
|
|
26877
|
+
});
|
|
26878
|
+
});
|
|
26879
|
+
}
|
|
26880
|
+
/**
|
|
26881
|
+
* Initialize connection to MCP server
|
|
26882
|
+
*/
|
|
26883
|
+
async initialize(params) {
|
|
26884
|
+
if (!this.transport.isConnected()) {
|
|
26885
|
+
await this.transport.connect();
|
|
26886
|
+
}
|
|
26887
|
+
const result = await this.sendRequest("initialize", params);
|
|
26888
|
+
this.serverCapabilities = result.capabilities;
|
|
26889
|
+
this.initialized = true;
|
|
26890
|
+
await this.transport.send({
|
|
26891
|
+
jsonrpc: "2.0",
|
|
26892
|
+
id: ++this.requestId,
|
|
26893
|
+
method: "notifications/initialized"
|
|
26894
|
+
});
|
|
26895
|
+
return result;
|
|
26896
|
+
}
|
|
26897
|
+
/**
|
|
26898
|
+
* List available tools
|
|
26899
|
+
*/
|
|
26900
|
+
async listTools() {
|
|
26901
|
+
this.ensureInitialized();
|
|
26902
|
+
return this.sendRequest("tools/list");
|
|
26903
|
+
}
|
|
26904
|
+
/**
|
|
26905
|
+
* Call a tool on the MCP server
|
|
26906
|
+
*/
|
|
26907
|
+
async callTool(params) {
|
|
26908
|
+
this.ensureInitialized();
|
|
26909
|
+
return this.sendRequest("tools/call", params);
|
|
26910
|
+
}
|
|
26911
|
+
/**
|
|
26912
|
+
* List available resources
|
|
26913
|
+
*/
|
|
26914
|
+
async listResources() {
|
|
26915
|
+
this.ensureInitialized();
|
|
26916
|
+
return this.sendRequest("resources/list");
|
|
26917
|
+
}
|
|
26918
|
+
/**
|
|
26919
|
+
* Read a specific resource by URI
|
|
26920
|
+
*/
|
|
26921
|
+
async readResource(uri) {
|
|
26922
|
+
this.ensureInitialized();
|
|
26923
|
+
return this.sendRequest("resources/read", { uri });
|
|
26924
|
+
}
|
|
26925
|
+
/**
|
|
26926
|
+
* List available prompts
|
|
26927
|
+
*/
|
|
26928
|
+
async listPrompts() {
|
|
26929
|
+
this.ensureInitialized();
|
|
26930
|
+
return this.sendRequest("prompts/list");
|
|
26931
|
+
}
|
|
26932
|
+
/**
|
|
26933
|
+
* Get a specific prompt with arguments
|
|
26934
|
+
*/
|
|
26935
|
+
async getPrompt(name, args) {
|
|
26936
|
+
this.ensureInitialized();
|
|
26937
|
+
return this.sendRequest("prompts/get", {
|
|
26938
|
+
name,
|
|
26939
|
+
arguments: args
|
|
26940
|
+
});
|
|
26941
|
+
}
|
|
26942
|
+
/**
|
|
26943
|
+
* Ensure client is initialized
|
|
26944
|
+
*/
|
|
26945
|
+
ensureInitialized() {
|
|
26946
|
+
if (!this.initialized) {
|
|
26947
|
+
throw new MCPConnectionError("Client not initialized. Call initialize() first.");
|
|
26948
|
+
}
|
|
26949
|
+
}
|
|
26950
|
+
/**
|
|
26951
|
+
* Close the client connection
|
|
26952
|
+
*/
|
|
26953
|
+
async close() {
|
|
26954
|
+
this.initialized = false;
|
|
26955
|
+
await this.transport.disconnect();
|
|
26956
|
+
}
|
|
26957
|
+
/**
|
|
26958
|
+
* Check if client is connected
|
|
26959
|
+
*/
|
|
26960
|
+
isConnected() {
|
|
26961
|
+
return this.transport.isConnected() && this.initialized;
|
|
26962
|
+
}
|
|
26963
|
+
/**
|
|
26964
|
+
* Get server capabilities
|
|
26965
|
+
*/
|
|
26966
|
+
getServerCapabilities() {
|
|
26967
|
+
return this.serverCapabilities;
|
|
26968
|
+
}
|
|
26969
|
+
};
|
|
26970
|
+
|
|
26971
|
+
// src/mcp/transport/stdio.ts
|
|
26972
|
+
init_errors2();
|
|
26973
|
+
var StdioTransport = class {
|
|
26974
|
+
constructor(config) {
|
|
26975
|
+
this.config = config;
|
|
26976
|
+
}
|
|
26977
|
+
process = null;
|
|
26978
|
+
messageCallback = null;
|
|
26979
|
+
errorCallback = null;
|
|
26980
|
+
closeCallback = null;
|
|
26981
|
+
buffer = "";
|
|
26982
|
+
connected = false;
|
|
26983
|
+
/**
|
|
26984
|
+
* Connect to the stdio transport by spawning the process
|
|
26985
|
+
*/
|
|
26986
|
+
async connect() {
|
|
26987
|
+
if (this.connected) {
|
|
26988
|
+
throw new MCPConnectionError("Transport already connected");
|
|
26989
|
+
}
|
|
26990
|
+
return new Promise((resolve3, reject) => {
|
|
26991
|
+
const { command, args = [], env: env2, cwd } = this.config;
|
|
26992
|
+
this.process = spawn(command, args, {
|
|
26993
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26994
|
+
env: { ...process.env, ...env2 },
|
|
26995
|
+
cwd
|
|
26996
|
+
});
|
|
26997
|
+
this.process.on("error", (error) => {
|
|
26998
|
+
reject(new MCPConnectionError(`Failed to spawn process: ${error.message}`));
|
|
26999
|
+
});
|
|
27000
|
+
this.process.on("spawn", () => {
|
|
27001
|
+
this.connected = true;
|
|
27002
|
+
this.setupHandlers();
|
|
27003
|
+
resolve3();
|
|
27004
|
+
});
|
|
27005
|
+
this.process.stderr?.on("data", (data) => {
|
|
27006
|
+
console.debug(`[MCP Server stderr]: ${data.toString()}`);
|
|
27007
|
+
});
|
|
27008
|
+
});
|
|
27009
|
+
}
|
|
27010
|
+
/**
|
|
27011
|
+
* Setup data handlers for the process
|
|
27012
|
+
*/
|
|
27013
|
+
setupHandlers() {
|
|
27014
|
+
if (!this.process?.stdout) return;
|
|
27015
|
+
this.process.stdout.on("data", (data) => {
|
|
27016
|
+
this.handleData(data);
|
|
27017
|
+
});
|
|
27018
|
+
this.process.on("exit", (code) => {
|
|
27019
|
+
this.connected = false;
|
|
27020
|
+
if (code !== 0 && code !== null) {
|
|
27021
|
+
this.errorCallback?.(new MCPTransportError(`Process exited with code ${code}`));
|
|
27022
|
+
}
|
|
27023
|
+
this.closeCallback?.();
|
|
27024
|
+
});
|
|
27025
|
+
this.process.on("close", () => {
|
|
27026
|
+
this.connected = false;
|
|
27027
|
+
this.closeCallback?.();
|
|
27028
|
+
});
|
|
27029
|
+
}
|
|
27030
|
+
/**
|
|
27031
|
+
* Handle incoming data from stdout
|
|
27032
|
+
*/
|
|
27033
|
+
handleData(data) {
|
|
27034
|
+
this.buffer += data.toString();
|
|
27035
|
+
const lines = this.buffer.split("\n");
|
|
27036
|
+
this.buffer = lines.pop() ?? "";
|
|
27037
|
+
for (const line of lines) {
|
|
27038
|
+
const trimmed = line.trim();
|
|
27039
|
+
if (!trimmed) continue;
|
|
27040
|
+
try {
|
|
27041
|
+
const message = JSON.parse(trimmed);
|
|
27042
|
+
this.messageCallback?.(message);
|
|
27043
|
+
} catch {
|
|
27044
|
+
this.errorCallback?.(new MCPTransportError(`Invalid JSON: ${trimmed}`));
|
|
27045
|
+
}
|
|
27046
|
+
}
|
|
27047
|
+
}
|
|
27048
|
+
/**
|
|
27049
|
+
* Send a message through the transport
|
|
27050
|
+
*/
|
|
27051
|
+
async send(message) {
|
|
27052
|
+
if (!this.connected || !this.process?.stdin) {
|
|
27053
|
+
throw new MCPTransportError("Transport not connected");
|
|
27054
|
+
}
|
|
27055
|
+
const line = JSON.stringify(message) + "\n";
|
|
27056
|
+
return new Promise((resolve3, reject) => {
|
|
27057
|
+
if (!this.process?.stdin) {
|
|
27058
|
+
reject(new MCPTransportError("stdin not available"));
|
|
27059
|
+
return;
|
|
27060
|
+
}
|
|
27061
|
+
const stdin = this.process.stdin;
|
|
27062
|
+
const canWrite = stdin.write(line, (error) => {
|
|
27063
|
+
if (error) {
|
|
27064
|
+
reject(new MCPTransportError(`Write error: ${error.message}`));
|
|
27065
|
+
} else {
|
|
27066
|
+
resolve3();
|
|
27067
|
+
}
|
|
27068
|
+
});
|
|
27069
|
+
if (!canWrite) {
|
|
27070
|
+
stdin.once("drain", () => resolve3());
|
|
27071
|
+
}
|
|
27072
|
+
});
|
|
27073
|
+
}
|
|
27074
|
+
/**
|
|
27075
|
+
* Disconnect from the transport
|
|
27076
|
+
*/
|
|
27077
|
+
async disconnect() {
|
|
27078
|
+
if (!this.process) return;
|
|
27079
|
+
return new Promise((resolve3) => {
|
|
27080
|
+
if (!this.process) {
|
|
27081
|
+
resolve3();
|
|
27082
|
+
return;
|
|
27083
|
+
}
|
|
27084
|
+
this.process.stdin?.end();
|
|
27085
|
+
const timeout = setTimeout(() => {
|
|
27086
|
+
this.process?.kill("SIGTERM");
|
|
27087
|
+
}, 5e3);
|
|
27088
|
+
this.process.on("close", () => {
|
|
27089
|
+
clearTimeout(timeout);
|
|
27090
|
+
this.connected = false;
|
|
27091
|
+
this.process = null;
|
|
27092
|
+
resolve3();
|
|
27093
|
+
});
|
|
27094
|
+
if (this.process.killed || !this.connected) {
|
|
27095
|
+
clearTimeout(timeout);
|
|
27096
|
+
this.process = null;
|
|
27097
|
+
resolve3();
|
|
27098
|
+
}
|
|
27099
|
+
});
|
|
27100
|
+
}
|
|
27101
|
+
/**
|
|
27102
|
+
* Set callback for received messages
|
|
27103
|
+
*/
|
|
27104
|
+
onMessage(callback) {
|
|
27105
|
+
this.messageCallback = callback;
|
|
27106
|
+
}
|
|
27107
|
+
/**
|
|
27108
|
+
* Set callback for errors
|
|
27109
|
+
*/
|
|
27110
|
+
onError(callback) {
|
|
27111
|
+
this.errorCallback = callback;
|
|
27112
|
+
}
|
|
27113
|
+
/**
|
|
27114
|
+
* Set callback for connection close
|
|
27115
|
+
*/
|
|
27116
|
+
onClose(callback) {
|
|
27117
|
+
this.closeCallback = callback;
|
|
27118
|
+
}
|
|
27119
|
+
/**
|
|
27120
|
+
* Check if transport is connected
|
|
27121
|
+
*/
|
|
27122
|
+
isConnected() {
|
|
27123
|
+
return this.connected;
|
|
27124
|
+
}
|
|
27125
|
+
};
|
|
27126
|
+
|
|
27127
|
+
// src/mcp/transport/http.ts
|
|
27128
|
+
init_errors2();
|
|
27129
|
+
|
|
27130
|
+
// src/mcp/oauth.ts
|
|
27131
|
+
init_callback_server();
|
|
27132
|
+
init_paths();
|
|
27133
|
+
init_logger();
|
|
27134
|
+
var execFileAsync2 = promisify(execFile);
|
|
27135
|
+
var TOKEN_STORE_PATH = path17__default.join(CONFIG_PATHS.tokens, "mcp-oauth.json");
|
|
27136
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
27137
|
+
var logger = getLogger();
|
|
27138
|
+
function getResourceKey(resourceUrl) {
|
|
27139
|
+
const resource = canonicalizeResourceUrl(resourceUrl);
|
|
27140
|
+
return resource.toLowerCase();
|
|
27141
|
+
}
|
|
27142
|
+
function canonicalizeResourceUrl(resourceUrl) {
|
|
27143
|
+
const parsed = new URL(resourceUrl);
|
|
27144
|
+
parsed.search = "";
|
|
27145
|
+
parsed.hash = "";
|
|
27146
|
+
if (parsed.pathname === "/") {
|
|
27147
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
27148
|
+
}
|
|
27149
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
27150
|
+
return parsed.toString();
|
|
27151
|
+
}
|
|
27152
|
+
async function loadStore2() {
|
|
27153
|
+
try {
|
|
27154
|
+
const content = await fs16__default.readFile(TOKEN_STORE_PATH, "utf-8");
|
|
27155
|
+
const parsed = JSON.parse(content);
|
|
27156
|
+
return {
|
|
27157
|
+
tokens: parsed.tokens ?? {},
|
|
27158
|
+
clients: parsed.clients ?? {}
|
|
27159
|
+
};
|
|
27160
|
+
} catch {
|
|
27161
|
+
return { tokens: {}, clients: {} };
|
|
27162
|
+
}
|
|
27163
|
+
}
|
|
27164
|
+
async function saveStore2(store) {
|
|
27165
|
+
await fs16__default.mkdir(path17__default.dirname(TOKEN_STORE_PATH), { recursive: true });
|
|
27166
|
+
await fs16__default.writeFile(TOKEN_STORE_PATH, JSON.stringify(store, null, 2), {
|
|
27167
|
+
encoding: "utf-8",
|
|
27168
|
+
mode: 384
|
|
27169
|
+
});
|
|
27170
|
+
}
|
|
27171
|
+
function isTokenExpired2(token) {
|
|
27172
|
+
if (!token.expiresAt) return false;
|
|
27173
|
+
return Date.now() >= token.expiresAt - 3e4;
|
|
27174
|
+
}
|
|
27175
|
+
async function getStoredMcpOAuthToken(resourceUrl) {
|
|
27176
|
+
const store = await loadStore2();
|
|
27177
|
+
const token = store.tokens[getResourceKey(resourceUrl)];
|
|
27178
|
+
if (!token) return void 0;
|
|
27179
|
+
if (isTokenExpired2(token)) return void 0;
|
|
27180
|
+
return token.accessToken;
|
|
27181
|
+
}
|
|
27182
|
+
function createCodeVerifier() {
|
|
27183
|
+
return randomBytes(32).toString("base64url");
|
|
27184
|
+
}
|
|
27185
|
+
function createCodeChallenge(verifier) {
|
|
27186
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
27187
|
+
}
|
|
27188
|
+
function createState() {
|
|
27189
|
+
return randomBytes(16).toString("hex");
|
|
27190
|
+
}
|
|
27191
|
+
async function openBrowser(url) {
|
|
27192
|
+
let safeUrl;
|
|
27193
|
+
try {
|
|
27194
|
+
const parsed = new URL(url);
|
|
27195
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
27196
|
+
return false;
|
|
27197
|
+
}
|
|
27198
|
+
safeUrl = parsed.toString();
|
|
27199
|
+
} catch {
|
|
27200
|
+
return false;
|
|
27201
|
+
}
|
|
27202
|
+
const isWSL2 = process.platform === "linux" && (process.env["WSL_DISTRO_NAME"] !== void 0 || process.env["WSL_INTEROP"] !== void 0 || process.env["TERM_PROGRAM"]?.toLowerCase().includes("wsl") === true);
|
|
27203
|
+
const commands = [];
|
|
27204
|
+
if (process.platform === "darwin") {
|
|
27205
|
+
commands.push(
|
|
27206
|
+
{ cmd: "open", args: [safeUrl] },
|
|
27207
|
+
{ cmd: "open", args: ["-a", "Safari", safeUrl] },
|
|
27208
|
+
{ cmd: "open", args: ["-a", "Google Chrome", safeUrl] }
|
|
27209
|
+
);
|
|
27210
|
+
} else if (process.platform === "win32") {
|
|
27211
|
+
commands.push({ cmd: "rundll32", args: ["url.dll,FileProtocolHandler", safeUrl] });
|
|
27212
|
+
} else if (isWSL2) {
|
|
27213
|
+
commands.push(
|
|
27214
|
+
{ cmd: "cmd.exe", args: ["/c", "start", "", safeUrl] },
|
|
27215
|
+
{ cmd: "powershell.exe", args: ["-Command", `Start-Process '${safeUrl}'`] },
|
|
27216
|
+
{ cmd: "wslview", args: [safeUrl] }
|
|
27217
|
+
);
|
|
27218
|
+
} else {
|
|
27219
|
+
commands.push(
|
|
27220
|
+
{ cmd: "xdg-open", args: [safeUrl] },
|
|
27221
|
+
{ cmd: "sensible-browser", args: [safeUrl] },
|
|
27222
|
+
{ cmd: "x-www-browser", args: [safeUrl] },
|
|
27223
|
+
{ cmd: "gnome-open", args: [safeUrl] },
|
|
27224
|
+
{ cmd: "firefox", args: [safeUrl] },
|
|
27225
|
+
{ cmd: "chromium-browser", args: [safeUrl] },
|
|
27226
|
+
{ cmd: "google-chrome", args: [safeUrl] }
|
|
27227
|
+
);
|
|
27228
|
+
}
|
|
27229
|
+
for (const { cmd, args } of commands) {
|
|
27230
|
+
try {
|
|
27231
|
+
await execFileAsync2(cmd, args);
|
|
27232
|
+
return true;
|
|
27233
|
+
} catch {
|
|
27234
|
+
continue;
|
|
27235
|
+
}
|
|
27236
|
+
}
|
|
27237
|
+
return false;
|
|
27238
|
+
}
|
|
27239
|
+
function maskUrlForLogs(rawUrl) {
|
|
27240
|
+
try {
|
|
27241
|
+
const url = new URL(rawUrl);
|
|
27242
|
+
url.search = "";
|
|
27243
|
+
url.hash = "";
|
|
27244
|
+
return url.toString();
|
|
27245
|
+
} catch {
|
|
27246
|
+
return "[invalid-url]";
|
|
27247
|
+
}
|
|
27248
|
+
}
|
|
27249
|
+
function parseResourceMetadataUrl(wwwAuthenticateHeader) {
|
|
27250
|
+
if (!wwwAuthenticateHeader) return void 0;
|
|
27251
|
+
const match = wwwAuthenticateHeader.match(/resource_metadata="([^"]+)"/i);
|
|
27252
|
+
return match?.[1];
|
|
27253
|
+
}
|
|
27254
|
+
function createProtectedMetadataCandidates(resourceUrl, headerUrl) {
|
|
27255
|
+
const candidates = [];
|
|
27256
|
+
if (headerUrl) {
|
|
27257
|
+
candidates.push(headerUrl);
|
|
27258
|
+
}
|
|
27259
|
+
const resource = new URL(resourceUrl);
|
|
27260
|
+
const origin = `${resource.protocol}//${resource.host}`;
|
|
27261
|
+
const pathPart = resource.pathname.replace(/\/+$/, "");
|
|
27262
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource`);
|
|
27263
|
+
if (pathPart && pathPart !== "/") {
|
|
27264
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource${pathPart}`);
|
|
27265
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource/${pathPart.replace(/^\//, "")}`);
|
|
27266
|
+
}
|
|
27267
|
+
return Array.from(new Set(candidates));
|
|
27268
|
+
}
|
|
27269
|
+
async function fetchJson(url) {
|
|
27270
|
+
const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
27271
|
+
if (!res.ok) {
|
|
27272
|
+
throw new Error(`HTTP ${res.status} while fetching ${url}`);
|
|
27273
|
+
}
|
|
27274
|
+
return await res.json();
|
|
27275
|
+
}
|
|
27276
|
+
function buildAuthorizationMetadataCandidates(issuer) {
|
|
27277
|
+
const parsed = new URL(issuer);
|
|
27278
|
+
const base = `${parsed.protocol}//${parsed.host}`;
|
|
27279
|
+
const issuerPath = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
|
|
27280
|
+
const candidates = [
|
|
27281
|
+
`${base}/.well-known/oauth-authorization-server${issuerPath}`,
|
|
27282
|
+
`${base}/.well-known/oauth-authorization-server`,
|
|
27283
|
+
`${base}/.well-known/openid-configuration${issuerPath}`,
|
|
27284
|
+
`${base}/.well-known/openid-configuration`
|
|
27285
|
+
];
|
|
27286
|
+
return Array.from(new Set(candidates));
|
|
27287
|
+
}
|
|
27288
|
+
async function discoverProtectedResourceMetadata(resourceUrl, wwwAuthenticateHeader) {
|
|
27289
|
+
const headerUrl = parseResourceMetadataUrl(wwwAuthenticateHeader);
|
|
27290
|
+
const candidates = createProtectedMetadataCandidates(resourceUrl, headerUrl);
|
|
27291
|
+
for (const candidate of candidates) {
|
|
27292
|
+
try {
|
|
27293
|
+
const metadata = await fetchJson(candidate);
|
|
27294
|
+
if (Array.isArray(metadata.authorization_servers) && metadata.authorization_servers.length > 0) {
|
|
27295
|
+
return metadata;
|
|
27296
|
+
}
|
|
27297
|
+
} catch {
|
|
27298
|
+
}
|
|
27299
|
+
}
|
|
27300
|
+
throw new Error("Could not discover OAuth protected resource metadata for MCP server");
|
|
27301
|
+
}
|
|
27302
|
+
async function discoverAuthorizationServerMetadata(authorizationServer) {
|
|
27303
|
+
const candidates = buildAuthorizationMetadataCandidates(authorizationServer);
|
|
27304
|
+
for (const candidate of candidates) {
|
|
27305
|
+
try {
|
|
27306
|
+
const metadata = await fetchJson(candidate);
|
|
27307
|
+
if (metadata.authorization_endpoint && metadata.token_endpoint) {
|
|
27308
|
+
return metadata;
|
|
27309
|
+
}
|
|
27310
|
+
} catch {
|
|
27311
|
+
}
|
|
27312
|
+
}
|
|
27313
|
+
throw new Error("Could not discover OAuth authorization server metadata");
|
|
27314
|
+
}
|
|
27315
|
+
async function ensureClientId(authorizationMetadata, authorizationServer, redirectUri) {
|
|
27316
|
+
const store = await loadStore2();
|
|
27317
|
+
const clientKey = `${authorizationServer}|${redirectUri}`;
|
|
27318
|
+
const existing = store.clients[clientKey]?.clientId;
|
|
27319
|
+
if (existing) return existing;
|
|
27320
|
+
const registrationEndpoint = authorizationMetadata.registration_endpoint;
|
|
27321
|
+
if (!registrationEndpoint) {
|
|
27322
|
+
throw new Error(
|
|
27323
|
+
"Authorization server does not expose dynamic client registration; configure a static OAuth client ID for this MCP server."
|
|
27324
|
+
);
|
|
27325
|
+
}
|
|
27326
|
+
const registrationPayload = {
|
|
27327
|
+
client_name: "corbat-coco-mcp",
|
|
27328
|
+
redirect_uris: [redirectUri],
|
|
27329
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
27330
|
+
response_types: ["code"],
|
|
27331
|
+
token_endpoint_auth_method: "none"
|
|
27332
|
+
};
|
|
27333
|
+
const response = await fetch(registrationEndpoint, {
|
|
27334
|
+
method: "POST",
|
|
27335
|
+
headers: {
|
|
27336
|
+
"Content-Type": "application/json",
|
|
27337
|
+
Accept: "application/json"
|
|
27338
|
+
},
|
|
27339
|
+
body: JSON.stringify(registrationPayload)
|
|
27340
|
+
});
|
|
27341
|
+
if (!response.ok) {
|
|
27342
|
+
throw new Error(`Dynamic client registration failed: HTTP ${response.status}`);
|
|
27343
|
+
}
|
|
27344
|
+
const data = await response.json();
|
|
27345
|
+
const clientId = data.client_id;
|
|
27346
|
+
if (!clientId) {
|
|
27347
|
+
throw new Error("Dynamic client registration did not return client_id");
|
|
27348
|
+
}
|
|
27349
|
+
store.clients[clientKey] = { clientId };
|
|
27350
|
+
await saveStore2(store);
|
|
27351
|
+
return clientId;
|
|
27352
|
+
}
|
|
27353
|
+
async function refreshAccessToken2(params) {
|
|
27354
|
+
const body = new URLSearchParams({
|
|
27355
|
+
grant_type: "refresh_token",
|
|
27356
|
+
client_id: params.clientId,
|
|
27357
|
+
refresh_token: params.refreshToken,
|
|
27358
|
+
resource: params.resource
|
|
27359
|
+
});
|
|
27360
|
+
const response = await fetch(params.tokenEndpoint, {
|
|
27361
|
+
method: "POST",
|
|
27362
|
+
headers: {
|
|
27363
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
27364
|
+
Accept: "application/json"
|
|
27365
|
+
},
|
|
27366
|
+
body: body.toString()
|
|
27367
|
+
});
|
|
27368
|
+
if (!response.ok) {
|
|
27369
|
+
throw new Error(`Refresh token exchange failed: HTTP ${response.status}`);
|
|
27370
|
+
}
|
|
27371
|
+
const tokenResponse = await response.json();
|
|
27372
|
+
if (!tokenResponse.access_token) {
|
|
27373
|
+
throw new Error("Refresh token response missing access_token");
|
|
27374
|
+
}
|
|
27375
|
+
return tokenResponse;
|
|
27376
|
+
}
|
|
27377
|
+
async function exchangeCodeForToken(tokenEndpoint, clientId, code, codeVerifier, redirectUri, resource) {
|
|
27378
|
+
const body = new URLSearchParams({
|
|
27379
|
+
grant_type: "authorization_code",
|
|
27380
|
+
code,
|
|
27381
|
+
client_id: clientId,
|
|
27382
|
+
redirect_uri: redirectUri,
|
|
27383
|
+
code_verifier: codeVerifier,
|
|
27384
|
+
resource
|
|
27385
|
+
});
|
|
27386
|
+
const response = await fetch(tokenEndpoint, {
|
|
27387
|
+
method: "POST",
|
|
27388
|
+
headers: {
|
|
27389
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
27390
|
+
Accept: "application/json"
|
|
27391
|
+
},
|
|
27392
|
+
body: body.toString()
|
|
27393
|
+
});
|
|
27394
|
+
if (!response.ok) {
|
|
27395
|
+
throw new Error(`Token exchange failed: HTTP ${response.status}`);
|
|
27396
|
+
}
|
|
27397
|
+
const tokenResponse = await response.json();
|
|
27398
|
+
if (!tokenResponse.access_token) {
|
|
27399
|
+
throw new Error("Token exchange response missing access_token");
|
|
27400
|
+
}
|
|
27401
|
+
return tokenResponse;
|
|
27402
|
+
}
|
|
27403
|
+
async function persistToken(resourceUrl, token, metadata) {
|
|
27404
|
+
const store = await loadStore2();
|
|
27405
|
+
const expiresAt = typeof token.expires_in === "number" ? Date.now() + Math.max(0, token.expires_in) * 1e3 : void 0;
|
|
27406
|
+
store.tokens[getResourceKey(resourceUrl)] = {
|
|
27407
|
+
accessToken: token.access_token,
|
|
27408
|
+
tokenType: token.token_type,
|
|
27409
|
+
refreshToken: token.refresh_token,
|
|
27410
|
+
authorizationServer: metadata?.authorizationServer,
|
|
27411
|
+
clientId: metadata?.clientId,
|
|
27412
|
+
resource: canonicalizeResourceUrl(resourceUrl),
|
|
27413
|
+
...expiresAt ? { expiresAt } : {}
|
|
27414
|
+
};
|
|
27415
|
+
await saveStore2(store);
|
|
27416
|
+
}
|
|
27417
|
+
async function authenticateMcpOAuth(params) {
|
|
27418
|
+
const resource = canonicalizeResourceUrl(params.resourceUrl);
|
|
27419
|
+
const store = await loadStore2();
|
|
27420
|
+
const stored = store.tokens[getResourceKey(resource)];
|
|
27421
|
+
if (stored && !params.forceRefresh && !isTokenExpired2(stored)) {
|
|
27422
|
+
return stored.accessToken;
|
|
27423
|
+
}
|
|
27424
|
+
if (!process.stdout.isTTY) {
|
|
27425
|
+
throw new Error(
|
|
27426
|
+
`MCP server '${params.serverName}' requires interactive OAuth in a TTY session. Run Coco in a terminal, or use mcp-remote (e.g. npx -y mcp-remote@latest ${resource}) for IDE bridge workflows.`
|
|
27427
|
+
);
|
|
27428
|
+
}
|
|
27429
|
+
let authorizationServer;
|
|
27430
|
+
let authorizationMetadata;
|
|
27431
|
+
try {
|
|
27432
|
+
const protectedMetadata = await discoverProtectedResourceMetadata(
|
|
27433
|
+
resource,
|
|
27434
|
+
params.wwwAuthenticateHeader
|
|
27435
|
+
);
|
|
27436
|
+
authorizationServer = protectedMetadata.authorization_servers?.[0];
|
|
27437
|
+
if (authorizationServer) {
|
|
27438
|
+
authorizationMetadata = await discoverAuthorizationServerMetadata(authorizationServer);
|
|
27439
|
+
}
|
|
27440
|
+
} catch {
|
|
27441
|
+
}
|
|
27442
|
+
if (!authorizationMetadata) {
|
|
27443
|
+
authorizationMetadata = await discoverAuthorizationServerMetadata(resource);
|
|
27444
|
+
}
|
|
27445
|
+
authorizationServer = authorizationServer ?? authorizationMetadata.issuer ?? new URL(resource).origin;
|
|
27446
|
+
if (stored && stored.refreshToken && stored.clientId && (params.forceRefresh || isTokenExpired2(stored))) {
|
|
27447
|
+
try {
|
|
27448
|
+
const refreshed = await refreshAccessToken2({
|
|
27449
|
+
tokenEndpoint: authorizationMetadata.token_endpoint,
|
|
27450
|
+
clientId: stored.clientId,
|
|
27451
|
+
refreshToken: stored.refreshToken,
|
|
27452
|
+
resource
|
|
27453
|
+
});
|
|
27454
|
+
await persistToken(resource, refreshed, {
|
|
27455
|
+
authorizationServer,
|
|
27456
|
+
clientId: stored.clientId
|
|
27457
|
+
});
|
|
27458
|
+
return refreshed.access_token;
|
|
27459
|
+
} catch {
|
|
27460
|
+
}
|
|
27461
|
+
}
|
|
27462
|
+
const codeVerifier = createCodeVerifier();
|
|
27463
|
+
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
27464
|
+
const state = createState();
|
|
27465
|
+
const { port, resultPromise } = await createCallbackServer(
|
|
27466
|
+
state,
|
|
27467
|
+
OAUTH_TIMEOUT_MS,
|
|
27468
|
+
OAUTH_CALLBACK_PORT
|
|
27469
|
+
);
|
|
27470
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
27471
|
+
const clientId = await ensureClientId(authorizationMetadata, authorizationServer, redirectUri);
|
|
27472
|
+
const authUrl = new URL(authorizationMetadata.authorization_endpoint);
|
|
27473
|
+
authUrl.searchParams.set("response_type", "code");
|
|
27474
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
27475
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
27476
|
+
authUrl.searchParams.set("state", state);
|
|
27477
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
27478
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
27479
|
+
authUrl.searchParams.set("resource", resource);
|
|
27480
|
+
if (authorizationMetadata.scopes_supported?.includes("offline_access")) {
|
|
27481
|
+
authUrl.searchParams.set("scope", "offline_access");
|
|
27482
|
+
}
|
|
27483
|
+
const opened = await openBrowser(authUrl.toString());
|
|
27484
|
+
if (!opened) {
|
|
27485
|
+
logger.warn(`[MCP OAuth] Could not open browser automatically for '${params.serverName}'`);
|
|
27486
|
+
logger.warn(`[MCP OAuth] Manual auth URL base: ${maskUrlForLogs(authUrl.toString())}`);
|
|
27487
|
+
console.log(`[MCP OAuth] Open this URL manually: ${authUrl.toString()}`);
|
|
27488
|
+
} else {
|
|
27489
|
+
logger.info(
|
|
27490
|
+
`[MCP OAuth] Opened browser for '${params.serverName}'. Complete login to continue.`
|
|
27491
|
+
);
|
|
27492
|
+
}
|
|
27493
|
+
const callback = await resultPromise;
|
|
27494
|
+
const token = await exchangeCodeForToken(
|
|
27495
|
+
authorizationMetadata.token_endpoint,
|
|
27496
|
+
clientId,
|
|
27497
|
+
callback.code,
|
|
27498
|
+
codeVerifier,
|
|
27499
|
+
redirectUri,
|
|
27500
|
+
resource
|
|
27501
|
+
);
|
|
27502
|
+
await persistToken(resource, token, { authorizationServer, clientId });
|
|
27503
|
+
return token.access_token;
|
|
27504
|
+
}
|
|
27505
|
+
|
|
27506
|
+
// src/mcp/transport/http.ts
|
|
27507
|
+
init_errors2();
|
|
27508
|
+
var HTTPTransport = class {
|
|
27509
|
+
constructor(config) {
|
|
27510
|
+
this.config = config;
|
|
27511
|
+
this.config.timeout = config.timeout ?? 6e4;
|
|
27512
|
+
this.config.retries = config.retries ?? 3;
|
|
27513
|
+
}
|
|
27514
|
+
messageCallback = null;
|
|
27515
|
+
errorCallback = null;
|
|
27516
|
+
// Used to report transport errors to the client
|
|
27517
|
+
reportError(error) {
|
|
27518
|
+
this.errorCallback?.(error);
|
|
27519
|
+
}
|
|
27520
|
+
closeCallback = null;
|
|
27521
|
+
connected = false;
|
|
27522
|
+
abortController = null;
|
|
27523
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
27524
|
+
oauthToken;
|
|
27525
|
+
oauthInFlight = null;
|
|
27526
|
+
/**
|
|
27527
|
+
* Get authentication token
|
|
27528
|
+
*/
|
|
27529
|
+
getAuthToken() {
|
|
27530
|
+
if (this.oauthToken) {
|
|
27531
|
+
return this.oauthToken;
|
|
27532
|
+
}
|
|
27533
|
+
if (!this.config.auth) return void 0;
|
|
27534
|
+
if (this.config.auth.token) {
|
|
27535
|
+
return this.config.auth.token;
|
|
27536
|
+
}
|
|
27537
|
+
if (this.config.auth.tokenEnv) {
|
|
27538
|
+
return process.env[this.config.auth.tokenEnv];
|
|
27539
|
+
}
|
|
27540
|
+
return void 0;
|
|
27541
|
+
}
|
|
27542
|
+
/**
|
|
27543
|
+
* Build request headers
|
|
27544
|
+
*/
|
|
27545
|
+
buildHeaders() {
|
|
27546
|
+
const headers = {
|
|
27547
|
+
"Content-Type": "application/json",
|
|
27548
|
+
Accept: "application/json",
|
|
27549
|
+
...this.config.headers
|
|
27550
|
+
};
|
|
27551
|
+
if (this.oauthToken) {
|
|
27552
|
+
headers["Authorization"] = `Bearer ${this.oauthToken}`;
|
|
27553
|
+
return headers;
|
|
27554
|
+
}
|
|
27555
|
+
const token = this.getAuthToken();
|
|
27556
|
+
if (token && this.config.auth) {
|
|
27557
|
+
if (this.config.auth.type === "apikey") {
|
|
27558
|
+
headers[this.config.auth.headerName || "X-API-Key"] = token;
|
|
27559
|
+
} else {
|
|
27560
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
27561
|
+
}
|
|
27562
|
+
}
|
|
27563
|
+
return headers;
|
|
27564
|
+
}
|
|
27565
|
+
shouldAttemptOAuth() {
|
|
27566
|
+
if (this.config.auth?.type === "apikey") {
|
|
27567
|
+
return false;
|
|
27568
|
+
}
|
|
27569
|
+
if (this.config.auth?.type === "bearer") {
|
|
27570
|
+
return !this.getAuthToken();
|
|
27571
|
+
}
|
|
27572
|
+
return true;
|
|
27573
|
+
}
|
|
27574
|
+
async ensureOAuthToken(wwwAuthenticateHeader, options) {
|
|
27575
|
+
if (this.oauthToken && !options?.forceRefresh) {
|
|
27576
|
+
return this.oauthToken;
|
|
27577
|
+
}
|
|
27578
|
+
if (this.oauthInFlight) {
|
|
27579
|
+
return this.oauthInFlight;
|
|
27580
|
+
}
|
|
27581
|
+
const serverName = this.config.name ?? this.config.url;
|
|
27582
|
+
if (options?.forceRefresh) {
|
|
27583
|
+
this.oauthToken = void 0;
|
|
27584
|
+
}
|
|
27585
|
+
this.oauthInFlight = authenticateMcpOAuth({
|
|
27586
|
+
serverName,
|
|
27587
|
+
resourceUrl: this.config.url,
|
|
27588
|
+
wwwAuthenticateHeader,
|
|
27589
|
+
forceRefresh: options?.forceRefresh
|
|
27590
|
+
}).then((token) => {
|
|
27591
|
+
this.oauthToken = token;
|
|
27592
|
+
return token;
|
|
27593
|
+
}).finally(() => {
|
|
27594
|
+
this.oauthInFlight = null;
|
|
27595
|
+
});
|
|
27596
|
+
return this.oauthInFlight;
|
|
27597
|
+
}
|
|
27598
|
+
async sendRequestWithOAuthRetry(method, body, signal) {
|
|
27599
|
+
const doFetch = async () => fetch(this.config.url, {
|
|
27600
|
+
method,
|
|
27601
|
+
headers: this.buildHeaders(),
|
|
27602
|
+
...body ? { body } : {},
|
|
27603
|
+
signal
|
|
27604
|
+
});
|
|
27605
|
+
let response = await doFetch();
|
|
27606
|
+
if (response.status !== 401 || !this.shouldAttemptOAuth()) {
|
|
27607
|
+
if (this.shouldAttemptOAuth() && !this.oauthToken && response.headers.get("www-authenticate")) {
|
|
27608
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"));
|
|
27609
|
+
response = await doFetch();
|
|
27610
|
+
}
|
|
27611
|
+
return response;
|
|
27612
|
+
}
|
|
27613
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"), { forceRefresh: true });
|
|
27614
|
+
response = await doFetch();
|
|
27615
|
+
return response;
|
|
27616
|
+
}
|
|
27617
|
+
looksLikeAuthErrorMessage(message) {
|
|
27618
|
+
if (!message) return false;
|
|
27619
|
+
const msg = message.toLowerCase();
|
|
27620
|
+
const hasStrongAuthSignal = msg.includes("unauthorized") || msg.includes("unauthorised") || msg.includes("authentication") || msg.includes("oauth") || msg.includes("access token") || msg.includes("invalid_token") || msg.includes("invalid token") || msg.includes("token expired") || msg.includes("bearer") || msg.includes("not authenticated") || msg.includes("not logged") || msg.includes("login") || msg.includes("generate") && msg.includes("token");
|
|
27621
|
+
const hasVendorHint = msg.includes("gemini cli") || msg.includes("jira") || msg.includes("confluence") || msg.includes("atlassian");
|
|
27622
|
+
const hasWeakAuthSignal = msg.includes("authenticate") || msg.includes("token") || msg.includes("authorization");
|
|
27623
|
+
return hasStrongAuthSignal || // Vendor-specific hints alone are not enough; require an auth-related token too.
|
|
27624
|
+
hasVendorHint && hasWeakAuthSignal;
|
|
27625
|
+
}
|
|
27626
|
+
isJsonRpcAuthError(payload) {
|
|
27627
|
+
if (!payload.error) return false;
|
|
27628
|
+
return this.looksLikeAuthErrorMessage(payload.error.message);
|
|
27629
|
+
}
|
|
27630
|
+
/**
|
|
27631
|
+
* Connect to the HTTP transport
|
|
27632
|
+
*/
|
|
27633
|
+
async connect() {
|
|
27634
|
+
if (this.connected) {
|
|
27635
|
+
throw new MCPConnectionError("Transport already connected");
|
|
27636
|
+
}
|
|
27637
|
+
try {
|
|
27638
|
+
new URL(this.config.url);
|
|
27639
|
+
} catch {
|
|
27640
|
+
throw new MCPConnectionError(`Invalid URL: ${this.config.url}`);
|
|
27641
|
+
}
|
|
27642
|
+
try {
|
|
27643
|
+
this.abortController = new AbortController();
|
|
27644
|
+
if (this.shouldAttemptOAuth()) {
|
|
27645
|
+
this.oauthToken = await getStoredMcpOAuthToken(this.config.url);
|
|
27646
|
+
}
|
|
27647
|
+
const response = await this.sendRequestWithOAuthRetry(
|
|
27648
|
+
"GET",
|
|
27649
|
+
void 0,
|
|
27650
|
+
this.abortController.signal
|
|
27651
|
+
);
|
|
27652
|
+
if (!response.ok && response.status !== 404) {
|
|
27653
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
27654
|
+
}
|
|
27655
|
+
this.connected = true;
|
|
27656
|
+
} catch (error) {
|
|
27657
|
+
if (error instanceof MCPError) {
|
|
27658
|
+
this.reportError(error);
|
|
27659
|
+
throw error;
|
|
27660
|
+
}
|
|
27661
|
+
const connError = new MCPConnectionError(
|
|
27662
|
+
`Failed to connect: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
27663
|
+
);
|
|
27664
|
+
this.reportError(connError);
|
|
27665
|
+
throw connError;
|
|
27666
|
+
}
|
|
27667
|
+
}
|
|
27668
|
+
/**
|
|
27669
|
+
* Send a message through the transport
|
|
27670
|
+
*/
|
|
27671
|
+
async send(message) {
|
|
27672
|
+
if (!this.connected) {
|
|
27673
|
+
throw new MCPTransportError("Transport not connected");
|
|
27674
|
+
}
|
|
27675
|
+
const abortController = new AbortController();
|
|
27676
|
+
this.pendingRequests.set(message.id, abortController);
|
|
27677
|
+
let lastError;
|
|
27678
|
+
for (let attempt = 0; attempt < this.config.retries; attempt++) {
|
|
27679
|
+
try {
|
|
27680
|
+
const timeoutId = setTimeout(() => {
|
|
27681
|
+
abortController.abort();
|
|
27682
|
+
}, this.config.timeout);
|
|
27683
|
+
const response = await this.sendRequestWithOAuthRetry(
|
|
27684
|
+
"POST",
|
|
27685
|
+
JSON.stringify(message),
|
|
27686
|
+
abortController.signal
|
|
27687
|
+
);
|
|
27688
|
+
clearTimeout(timeoutId);
|
|
27689
|
+
if (!response.ok) {
|
|
27690
|
+
throw new MCPTransportError(`HTTP error ${response.status}: ${response.statusText}`);
|
|
27691
|
+
}
|
|
27692
|
+
const data = await response.json();
|
|
27693
|
+
if (this.shouldAttemptOAuth() && this.isJsonRpcAuthError(data)) {
|
|
27694
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"), {
|
|
27695
|
+
forceRefresh: true
|
|
27696
|
+
});
|
|
27697
|
+
const retryResponse = await this.sendRequestWithOAuthRetry(
|
|
27698
|
+
"POST",
|
|
27699
|
+
JSON.stringify(message),
|
|
27700
|
+
abortController.signal
|
|
27701
|
+
);
|
|
27702
|
+
if (!retryResponse.ok) {
|
|
27703
|
+
throw new MCPTransportError(
|
|
27704
|
+
`HTTP error ${retryResponse.status}: ${retryResponse.statusText}`
|
|
27705
|
+
);
|
|
27706
|
+
}
|
|
27707
|
+
const retryData = await retryResponse.json();
|
|
27708
|
+
this.messageCallback?.(retryData);
|
|
27709
|
+
return;
|
|
27710
|
+
}
|
|
27711
|
+
this.messageCallback?.(data);
|
|
27712
|
+
return;
|
|
27713
|
+
} catch (error) {
|
|
27714
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
27715
|
+
if (error instanceof MCPTransportError) {
|
|
27716
|
+
this.reportError(error);
|
|
27717
|
+
throw error;
|
|
27718
|
+
}
|
|
27719
|
+
if (attempt < this.config.retries - 1) {
|
|
27720
|
+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1e3));
|
|
27721
|
+
}
|
|
27722
|
+
}
|
|
27723
|
+
}
|
|
27724
|
+
this.pendingRequests.delete(message.id);
|
|
27725
|
+
throw new MCPTransportError(
|
|
27726
|
+
`Request failed after ${this.config.retries} attempts: ${lastError?.message}`
|
|
27727
|
+
);
|
|
27728
|
+
}
|
|
27729
|
+
/**
|
|
27730
|
+
* Disconnect from the transport
|
|
27731
|
+
*/
|
|
27732
|
+
async disconnect() {
|
|
27733
|
+
for (const [, controller] of this.pendingRequests) {
|
|
27734
|
+
controller.abort();
|
|
27735
|
+
}
|
|
27736
|
+
this.pendingRequests.clear();
|
|
27737
|
+
this.abortController?.abort();
|
|
27738
|
+
this.connected = false;
|
|
27739
|
+
this.closeCallback?.();
|
|
27740
|
+
}
|
|
27741
|
+
/**
|
|
27742
|
+
* Set callback for received messages
|
|
27743
|
+
*/
|
|
27744
|
+
onMessage(callback) {
|
|
27745
|
+
this.messageCallback = callback;
|
|
27746
|
+
}
|
|
27747
|
+
/**
|
|
27748
|
+
* Set callback for errors
|
|
27749
|
+
*/
|
|
27750
|
+
onError(callback) {
|
|
27751
|
+
this.errorCallback = callback;
|
|
27752
|
+
}
|
|
27753
|
+
/**
|
|
27754
|
+
* Set callback for connection close
|
|
27755
|
+
*/
|
|
27756
|
+
onClose(callback) {
|
|
27757
|
+
this.closeCallback = callback;
|
|
27758
|
+
}
|
|
27759
|
+
/**
|
|
27760
|
+
* Check if transport is connected
|
|
27761
|
+
*/
|
|
27762
|
+
isConnected() {
|
|
27763
|
+
return this.connected;
|
|
27764
|
+
}
|
|
27765
|
+
/**
|
|
27766
|
+
* Get transport URL
|
|
27767
|
+
*/
|
|
27768
|
+
getURL() {
|
|
27769
|
+
return this.config.url;
|
|
27770
|
+
}
|
|
27771
|
+
/**
|
|
27772
|
+
* Get auth type
|
|
27773
|
+
*/
|
|
27774
|
+
getAuthType() {
|
|
27775
|
+
return this.config.auth?.type;
|
|
27776
|
+
}
|
|
27777
|
+
};
|
|
27778
|
+
|
|
27779
|
+
// src/mcp/transport/sse.ts
|
|
27780
|
+
init_errors2();
|
|
27781
|
+
var DEFAULT_CONFIG2 = {
|
|
27782
|
+
initialReconnectDelay: 1e3,
|
|
27783
|
+
maxReconnectDelay: 3e4,
|
|
27784
|
+
maxReconnectAttempts: 10
|
|
27785
|
+
};
|
|
27786
|
+
var SSETransport = class {
|
|
27787
|
+
config;
|
|
27788
|
+
connected = false;
|
|
27789
|
+
abortController = null;
|
|
27790
|
+
reconnectAttempts = 0;
|
|
27791
|
+
lastEventId = null;
|
|
27792
|
+
messageEndpoint = null;
|
|
27793
|
+
messageHandler = null;
|
|
27794
|
+
errorHandler = null;
|
|
27795
|
+
closeHandler = null;
|
|
27796
|
+
constructor(config) {
|
|
27797
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
27798
|
+
}
|
|
27799
|
+
/**
|
|
27800
|
+
* Connect to the SSE endpoint
|
|
27801
|
+
*/
|
|
27802
|
+
async connect() {
|
|
27803
|
+
if (this.connected) return;
|
|
27804
|
+
this.abortController = new AbortController();
|
|
27805
|
+
this.reconnectAttempts = 0;
|
|
27806
|
+
this.connected = true;
|
|
27807
|
+
try {
|
|
27808
|
+
await this.startListening();
|
|
27809
|
+
} catch (error) {
|
|
27810
|
+
this.connected = false;
|
|
27811
|
+
throw error;
|
|
27812
|
+
}
|
|
27813
|
+
}
|
|
27814
|
+
/**
|
|
27815
|
+
* Disconnect from the SSE endpoint
|
|
27816
|
+
*/
|
|
27817
|
+
async disconnect() {
|
|
27818
|
+
this.connected = false;
|
|
27819
|
+
this.abortController?.abort();
|
|
27820
|
+
this.abortController = null;
|
|
27821
|
+
this.messageEndpoint = null;
|
|
27822
|
+
this.closeHandler?.();
|
|
27823
|
+
}
|
|
27824
|
+
/**
|
|
27825
|
+
* Send a JSON-RPC message via HTTP POST
|
|
27826
|
+
*/
|
|
27827
|
+
async send(message) {
|
|
27828
|
+
if (!this.connected) {
|
|
27829
|
+
throw new MCPConnectionError("Not connected to SSE endpoint");
|
|
27830
|
+
}
|
|
27831
|
+
const endpoint = this.messageEndpoint ?? `${this.config.url}/message`;
|
|
27832
|
+
try {
|
|
27833
|
+
const response = await fetch(endpoint, {
|
|
27834
|
+
method: "POST",
|
|
27835
|
+
headers: {
|
|
27836
|
+
"Content-Type": "application/json",
|
|
27837
|
+
...this.config.headers
|
|
27838
|
+
},
|
|
27839
|
+
body: JSON.stringify(message),
|
|
27840
|
+
signal: this.abortController?.signal
|
|
27841
|
+
});
|
|
27842
|
+
if (!response.ok) {
|
|
27843
|
+
throw new MCPTransportError(`HTTP POST failed: ${response.status} ${response.statusText}`);
|
|
27844
|
+
}
|
|
27845
|
+
} catch (error) {
|
|
27846
|
+
if (error.name === "AbortError") return;
|
|
27847
|
+
if (error instanceof MCPTransportError) throw error;
|
|
27848
|
+
throw new MCPTransportError(
|
|
27849
|
+
`Failed to send message: ${error instanceof Error ? error.message : String(error)}`
|
|
27850
|
+
);
|
|
27851
|
+
}
|
|
27852
|
+
}
|
|
27853
|
+
/**
|
|
27854
|
+
* Register message handler
|
|
27855
|
+
*/
|
|
27856
|
+
onMessage(handler) {
|
|
27857
|
+
this.messageHandler = handler;
|
|
27858
|
+
}
|
|
27859
|
+
/**
|
|
27860
|
+
* Register error handler
|
|
27861
|
+
*/
|
|
27862
|
+
onError(handler) {
|
|
27863
|
+
this.errorHandler = handler;
|
|
27864
|
+
}
|
|
27865
|
+
/**
|
|
27866
|
+
* Register close handler
|
|
27867
|
+
*/
|
|
27868
|
+
onClose(handler) {
|
|
27869
|
+
this.closeHandler = handler;
|
|
27870
|
+
}
|
|
27871
|
+
/**
|
|
27872
|
+
* Check if connected
|
|
27873
|
+
*/
|
|
27874
|
+
isConnected() {
|
|
27875
|
+
return this.connected;
|
|
27876
|
+
}
|
|
27877
|
+
/**
|
|
27878
|
+
* Start listening to the SSE stream
|
|
27879
|
+
*/
|
|
27880
|
+
async startListening() {
|
|
27881
|
+
const headers = {
|
|
27882
|
+
Accept: "text/event-stream",
|
|
27883
|
+
"Cache-Control": "no-cache",
|
|
27884
|
+
...this.config.headers
|
|
27885
|
+
};
|
|
27886
|
+
if (this.lastEventId) {
|
|
27887
|
+
headers["Last-Event-ID"] = this.lastEventId;
|
|
27888
|
+
}
|
|
27889
|
+
try {
|
|
27890
|
+
const response = await fetch(this.config.url, {
|
|
27891
|
+
method: "GET",
|
|
27892
|
+
headers,
|
|
27893
|
+
signal: this.abortController?.signal
|
|
27894
|
+
});
|
|
27895
|
+
if (!response.ok) {
|
|
27896
|
+
throw new MCPConnectionError(
|
|
27897
|
+
`SSE connection failed: ${response.status} ${response.statusText}`
|
|
27898
|
+
);
|
|
27899
|
+
}
|
|
27900
|
+
if (!response.body) {
|
|
27901
|
+
throw new MCPConnectionError("SSE response has no body");
|
|
27902
|
+
}
|
|
27903
|
+
this.processStream(response.body);
|
|
27904
|
+
} catch (error) {
|
|
27905
|
+
if (error.name === "AbortError") return;
|
|
27906
|
+
if (error instanceof MCPConnectionError) throw error;
|
|
27907
|
+
throw new MCPConnectionError(
|
|
27908
|
+
`Failed to connect to SSE: ${error instanceof Error ? error.message : String(error)}`
|
|
27909
|
+
);
|
|
27910
|
+
}
|
|
27911
|
+
}
|
|
27912
|
+
/**
|
|
27913
|
+
* Process the SSE stream
|
|
27914
|
+
*/
|
|
27915
|
+
async processStream(body) {
|
|
27916
|
+
const reader = body.getReader();
|
|
27917
|
+
const decoder = new TextDecoder();
|
|
27918
|
+
let buffer = "";
|
|
27919
|
+
let eventType = "";
|
|
27920
|
+
let eventData = "";
|
|
27921
|
+
let eventId = "";
|
|
27922
|
+
try {
|
|
27923
|
+
while (this.connected) {
|
|
27924
|
+
const { done, value } = await reader.read();
|
|
27925
|
+
if (done) {
|
|
27926
|
+
if (this.connected) {
|
|
27927
|
+
await this.handleReconnect();
|
|
27928
|
+
}
|
|
27929
|
+
return;
|
|
27930
|
+
}
|
|
27931
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27932
|
+
const lines = buffer.split("\n");
|
|
27933
|
+
buffer = lines.pop() ?? "";
|
|
27934
|
+
for (const line of lines) {
|
|
27935
|
+
if (line === "") {
|
|
27936
|
+
if (eventData) {
|
|
27937
|
+
this.handleEvent(eventType, eventData, eventId);
|
|
27938
|
+
eventType = "";
|
|
27939
|
+
eventData = "";
|
|
27940
|
+
eventId = "";
|
|
27941
|
+
}
|
|
27942
|
+
continue;
|
|
27943
|
+
}
|
|
27944
|
+
if (line.startsWith(":")) {
|
|
27945
|
+
continue;
|
|
27946
|
+
}
|
|
27947
|
+
const colonIdx = line.indexOf(":");
|
|
27948
|
+
if (colonIdx === -1) continue;
|
|
27949
|
+
const field = line.slice(0, colonIdx);
|
|
27950
|
+
const value2 = line.slice(colonIdx + 1).trimStart();
|
|
27951
|
+
switch (field) {
|
|
27952
|
+
case "event":
|
|
27953
|
+
eventType = value2;
|
|
27954
|
+
break;
|
|
27955
|
+
case "data":
|
|
27956
|
+
eventData += (eventData ? "\n" : "") + value2;
|
|
27957
|
+
break;
|
|
27958
|
+
case "id":
|
|
27959
|
+
eventId = value2;
|
|
27960
|
+
break;
|
|
27961
|
+
case "retry":
|
|
27962
|
+
const delay = parseInt(value2, 10);
|
|
27963
|
+
if (!isNaN(delay)) {
|
|
27964
|
+
this.config.initialReconnectDelay = delay;
|
|
27965
|
+
}
|
|
27966
|
+
break;
|
|
27967
|
+
}
|
|
27968
|
+
}
|
|
27969
|
+
}
|
|
27970
|
+
} catch (error) {
|
|
27971
|
+
if (error.name === "AbortError") return;
|
|
27972
|
+
this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
|
|
27973
|
+
if (this.connected) {
|
|
27974
|
+
await this.handleReconnect();
|
|
27975
|
+
}
|
|
27976
|
+
} finally {
|
|
27977
|
+
reader.releaseLock();
|
|
27978
|
+
}
|
|
27979
|
+
}
|
|
27980
|
+
/**
|
|
27981
|
+
* Handle a complete SSE event
|
|
27982
|
+
*/
|
|
27983
|
+
handleEvent(type, data, id) {
|
|
27984
|
+
if (id) {
|
|
27985
|
+
this.lastEventId = id;
|
|
27986
|
+
}
|
|
27987
|
+
if (type === "endpoint") {
|
|
27988
|
+
this.messageEndpoint = data;
|
|
27989
|
+
return;
|
|
27990
|
+
}
|
|
27991
|
+
try {
|
|
27992
|
+
const message = JSON.parse(data);
|
|
27993
|
+
this.messageHandler?.(message);
|
|
27994
|
+
} catch {
|
|
27995
|
+
this.errorHandler?.(new Error(`Invalid JSON in SSE event: ${data.slice(0, 100)}`));
|
|
27996
|
+
}
|
|
27997
|
+
}
|
|
27998
|
+
/**
|
|
27999
|
+
* Handle reconnection with exponential backoff
|
|
28000
|
+
*/
|
|
28001
|
+
async handleReconnect() {
|
|
28002
|
+
if (!this.connected || this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
28003
|
+
this.connected = false;
|
|
28004
|
+
this.closeHandler?.();
|
|
28005
|
+
return;
|
|
28006
|
+
}
|
|
28007
|
+
this.reconnectAttempts++;
|
|
28008
|
+
const delay = Math.min(
|
|
28009
|
+
this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
28010
|
+
this.config.maxReconnectDelay
|
|
28011
|
+
);
|
|
28012
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
28013
|
+
if (!this.connected) return;
|
|
28014
|
+
try {
|
|
28015
|
+
await this.startListening();
|
|
28016
|
+
this.reconnectAttempts = 0;
|
|
28017
|
+
} catch {
|
|
28018
|
+
if (this.connected) {
|
|
28019
|
+
await this.handleReconnect();
|
|
28020
|
+
}
|
|
28021
|
+
}
|
|
28022
|
+
}
|
|
28023
|
+
};
|
|
28024
|
+
|
|
28025
|
+
// src/mcp/lifecycle.ts
|
|
28026
|
+
init_errors2();
|
|
28027
|
+
init_logger();
|
|
28028
|
+
var MCPServerManager = class {
|
|
28029
|
+
connections = /* @__PURE__ */ new Map();
|
|
28030
|
+
logger = getLogger();
|
|
28031
|
+
/**
|
|
28032
|
+
* Create transport for a server config
|
|
28033
|
+
*/
|
|
28034
|
+
createTransport(config) {
|
|
28035
|
+
switch (config.transport) {
|
|
28036
|
+
case "stdio": {
|
|
28037
|
+
if (!config.stdio?.command) {
|
|
28038
|
+
throw new MCPConnectionError(`Server '${config.name}' requires stdio.command`);
|
|
28039
|
+
}
|
|
28040
|
+
return new StdioTransport({
|
|
28041
|
+
command: config.stdio.command,
|
|
28042
|
+
args: config.stdio.args ?? [],
|
|
28043
|
+
env: config.stdio.env
|
|
28044
|
+
});
|
|
28045
|
+
}
|
|
28046
|
+
case "http": {
|
|
28047
|
+
if (!config.http?.url) {
|
|
28048
|
+
throw new MCPConnectionError(`Server '${config.name}' requires http.url`);
|
|
28049
|
+
}
|
|
28050
|
+
return new HTTPTransport({
|
|
28051
|
+
name: config.name,
|
|
28052
|
+
url: config.http.url,
|
|
28053
|
+
headers: config.http.headers,
|
|
28054
|
+
auth: config.http.auth
|
|
28055
|
+
});
|
|
28056
|
+
}
|
|
28057
|
+
case "sse": {
|
|
28058
|
+
if (!config.http?.url) {
|
|
28059
|
+
throw new MCPConnectionError(`Server '${config.name}' requires http.url for SSE`);
|
|
28060
|
+
}
|
|
28061
|
+
return new SSETransport({
|
|
28062
|
+
url: config.http.url,
|
|
28063
|
+
headers: config.http.headers
|
|
28064
|
+
});
|
|
28065
|
+
}
|
|
28066
|
+
default:
|
|
28067
|
+
throw new MCPConnectionError(`Unsupported transport: ${config.transport}`);
|
|
28068
|
+
}
|
|
28069
|
+
}
|
|
28070
|
+
/**
|
|
28071
|
+
* Start a single server
|
|
28072
|
+
*/
|
|
28073
|
+
async startServer(config) {
|
|
28074
|
+
if (this.connections.has(config.name)) {
|
|
28075
|
+
this.logger.warn(`Server '${config.name}' already connected`);
|
|
28076
|
+
return this.connections.get(config.name);
|
|
28077
|
+
}
|
|
28078
|
+
this.logger.info(`Starting MCP server: ${config.name}`);
|
|
28079
|
+
const transport = this.createTransport(config);
|
|
28080
|
+
await transport.connect();
|
|
28081
|
+
const client = new MCPClientImpl(transport);
|
|
28082
|
+
await client.initialize({
|
|
28083
|
+
protocolVersion: "2024-11-05",
|
|
28084
|
+
capabilities: {},
|
|
28085
|
+
clientInfo: { name: "coco-mcp-client", version: VERSION }
|
|
28086
|
+
});
|
|
28087
|
+
let toolCount = 0;
|
|
28088
|
+
try {
|
|
28089
|
+
const { tools } = await client.listTools();
|
|
28090
|
+
toolCount = tools.length;
|
|
28091
|
+
} catch {
|
|
28092
|
+
}
|
|
28093
|
+
const connection = {
|
|
28094
|
+
name: config.name,
|
|
28095
|
+
client,
|
|
28096
|
+
transport,
|
|
28097
|
+
config,
|
|
28098
|
+
connectedAt: /* @__PURE__ */ new Date(),
|
|
28099
|
+
toolCount,
|
|
28100
|
+
healthy: true
|
|
28101
|
+
};
|
|
28102
|
+
this.connections.set(config.name, connection);
|
|
28103
|
+
this.logger.info(`Server '${config.name}' started with ${toolCount} tools`);
|
|
28104
|
+
return connection;
|
|
28105
|
+
}
|
|
28106
|
+
/**
|
|
28107
|
+
* Stop a single server
|
|
28108
|
+
*/
|
|
28109
|
+
async stopServer(name) {
|
|
28110
|
+
const connection = this.connections.get(name);
|
|
28111
|
+
if (!connection) {
|
|
28112
|
+
this.logger.warn(`Server '${name}' not found`);
|
|
28113
|
+
return;
|
|
28114
|
+
}
|
|
28115
|
+
this.logger.info(`Stopping MCP server: ${name}`);
|
|
28116
|
+
try {
|
|
28117
|
+
await connection.transport.disconnect();
|
|
28118
|
+
} catch (error) {
|
|
28119
|
+
this.logger.error(
|
|
28120
|
+
`Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
|
|
28121
|
+
);
|
|
28122
|
+
}
|
|
28123
|
+
this.connections.delete(name);
|
|
28124
|
+
}
|
|
28125
|
+
/**
|
|
28126
|
+
* Restart a server
|
|
28127
|
+
*/
|
|
28128
|
+
async restartServer(name) {
|
|
28129
|
+
const connection = this.connections.get(name);
|
|
28130
|
+
if (!connection) {
|
|
28131
|
+
throw new MCPConnectionError(`Server '${name}' not found`);
|
|
28132
|
+
}
|
|
28133
|
+
const config = connection.config;
|
|
28134
|
+
await this.stopServer(name);
|
|
28135
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
28136
|
+
return this.startServer(config);
|
|
28137
|
+
}
|
|
28138
|
+
/**
|
|
28139
|
+
* Health check for a server
|
|
28140
|
+
*/
|
|
28141
|
+
async healthCheck(name) {
|
|
28142
|
+
const connection = this.connections.get(name);
|
|
28143
|
+
if (!connection) {
|
|
28144
|
+
return {
|
|
28145
|
+
name,
|
|
28146
|
+
healthy: false,
|
|
28147
|
+
toolCount: 0,
|
|
28148
|
+
latencyMs: 0,
|
|
28149
|
+
error: "Server not connected"
|
|
28150
|
+
};
|
|
28151
|
+
}
|
|
28152
|
+
const startTime = performance.now();
|
|
28153
|
+
try {
|
|
28154
|
+
const { tools } = await Promise.race([
|
|
28155
|
+
connection.client.listTools(),
|
|
28156
|
+
new Promise(
|
|
28157
|
+
(_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
|
|
28158
|
+
)
|
|
28159
|
+
]);
|
|
28160
|
+
const latencyMs = performance.now() - startTime;
|
|
28161
|
+
connection.healthy = true;
|
|
28162
|
+
connection.toolCount = tools.length;
|
|
28163
|
+
return {
|
|
28164
|
+
name,
|
|
28165
|
+
healthy: true,
|
|
28166
|
+
toolCount: tools.length,
|
|
28167
|
+
latencyMs
|
|
28168
|
+
};
|
|
28169
|
+
} catch (error) {
|
|
28170
|
+
const latencyMs = performance.now() - startTime;
|
|
28171
|
+
connection.healthy = false;
|
|
28172
|
+
return {
|
|
28173
|
+
name,
|
|
28174
|
+
healthy: false,
|
|
28175
|
+
toolCount: 0,
|
|
28176
|
+
latencyMs,
|
|
28177
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28178
|
+
};
|
|
28179
|
+
}
|
|
28180
|
+
}
|
|
28181
|
+
/**
|
|
28182
|
+
* Start all servers from config list
|
|
28183
|
+
*/
|
|
28184
|
+
async startAll(configs) {
|
|
28185
|
+
const results = /* @__PURE__ */ new Map();
|
|
28186
|
+
for (const config of configs) {
|
|
28187
|
+
if (config.enabled === false) continue;
|
|
28188
|
+
try {
|
|
28189
|
+
const connection = await this.startServer(config);
|
|
28190
|
+
results.set(config.name, connection);
|
|
28191
|
+
} catch (error) {
|
|
28192
|
+
this.logger.error(
|
|
28193
|
+
`Failed to start server '${config.name}': ${error instanceof Error ? error.message : String(error)}`
|
|
28194
|
+
);
|
|
28195
|
+
}
|
|
28196
|
+
}
|
|
28197
|
+
return results;
|
|
28198
|
+
}
|
|
28199
|
+
/**
|
|
28200
|
+
* Stop all servers
|
|
28201
|
+
*/
|
|
28202
|
+
async stopAll() {
|
|
28203
|
+
const names = Array.from(this.connections.keys());
|
|
28204
|
+
for (const name of names) {
|
|
28205
|
+
await this.stopServer(name);
|
|
28206
|
+
}
|
|
28207
|
+
}
|
|
28208
|
+
/**
|
|
28209
|
+
* Get list of connected server names
|
|
28210
|
+
*/
|
|
28211
|
+
getConnectedServers() {
|
|
28212
|
+
return Array.from(this.connections.keys());
|
|
28213
|
+
}
|
|
28214
|
+
/**
|
|
28215
|
+
* Get a specific server connection
|
|
28216
|
+
*/
|
|
28217
|
+
getConnection(name) {
|
|
28218
|
+
return this.connections.get(name);
|
|
28219
|
+
}
|
|
28220
|
+
/**
|
|
28221
|
+
* Get all connections
|
|
28222
|
+
*/
|
|
28223
|
+
getAllConnections() {
|
|
28224
|
+
return Array.from(this.connections.values());
|
|
28225
|
+
}
|
|
28226
|
+
/**
|
|
28227
|
+
* Get the client for a server
|
|
28228
|
+
*/
|
|
28229
|
+
getClient(name) {
|
|
28230
|
+
return this.connections.get(name)?.client;
|
|
28231
|
+
}
|
|
28232
|
+
};
|
|
28233
|
+
var globalManager = null;
|
|
28234
|
+
function getMCPServerManager() {
|
|
28235
|
+
if (!globalManager) {
|
|
28236
|
+
globalManager = new MCPServerManager();
|
|
28237
|
+
}
|
|
28238
|
+
return globalManager;
|
|
28239
|
+
}
|
|
28240
|
+
|
|
28241
|
+
// src/tools/mcp.ts
|
|
28242
|
+
var mcpListServersTool = defineTool({
|
|
28243
|
+
name: "mcp_list_servers",
|
|
28244
|
+
description: `Inspect Coco's MCP configuration and current runtime connections.
|
|
28245
|
+
|
|
28246
|
+
Use this instead of bash_exec with "coco mcp ..." and instead of manually reading ~/.coco/mcp.json
|
|
28247
|
+
when you need to know which MCP servers are configured, connected, healthy, or which tools they expose.`,
|
|
28248
|
+
category: "config",
|
|
28249
|
+
parameters: z.object({
|
|
28250
|
+
includeDisabled: z.boolean().optional().default(false).describe("Include disabled MCP servers in the result"),
|
|
28251
|
+
includeTools: z.boolean().optional().default(false).describe("Include the list of exposed tool names for connected servers"),
|
|
28252
|
+
projectPath: z.string().optional().describe("Project path whose .mcp.json should be merged. Defaults to process.cwd()")
|
|
28253
|
+
}),
|
|
28254
|
+
async execute({ includeDisabled, includeTools, projectPath }) {
|
|
28255
|
+
const registry = new MCPRegistryImpl();
|
|
28256
|
+
await registry.load();
|
|
28257
|
+
const resolvedProjectPath = projectPath || process.cwd();
|
|
28258
|
+
const configuredServers = mergeMCPConfigs(
|
|
28259
|
+
registry.listServers(),
|
|
28260
|
+
await loadMCPServersFromCOCOConfig(),
|
|
28261
|
+
await loadProjectMCPFile(resolvedProjectPath)
|
|
28262
|
+
).filter((server) => includeDisabled || server.enabled !== false);
|
|
28263
|
+
const manager = getMCPServerManager();
|
|
28264
|
+
const servers = [];
|
|
28265
|
+
for (const server of configuredServers) {
|
|
28266
|
+
const connection = manager.getConnection(server.name);
|
|
28267
|
+
let tools;
|
|
28268
|
+
if (includeTools && connection) {
|
|
28269
|
+
try {
|
|
28270
|
+
const listed = await connection.client.listTools();
|
|
28271
|
+
tools = listed.tools.map((tool) => tool.name);
|
|
28272
|
+
} catch {
|
|
28273
|
+
tools = [];
|
|
28274
|
+
}
|
|
28275
|
+
}
|
|
28276
|
+
servers.push({
|
|
28277
|
+
name: server.name,
|
|
28278
|
+
transport: server.transport,
|
|
28279
|
+
enabled: server.enabled !== false,
|
|
28280
|
+
connected: connection !== void 0,
|
|
28281
|
+
healthy: connection?.healthy ?? false,
|
|
28282
|
+
toolCount: connection?.toolCount ?? 0,
|
|
28283
|
+
...includeTools ? { tools: tools ?? [] } : {}
|
|
28284
|
+
});
|
|
28285
|
+
}
|
|
28286
|
+
return {
|
|
28287
|
+
configuredCount: servers.length,
|
|
28288
|
+
connectedCount: servers.filter((server) => server.connected).length,
|
|
28289
|
+
servers
|
|
28290
|
+
};
|
|
28291
|
+
}
|
|
28292
|
+
});
|
|
28293
|
+
var mcpTools = [mcpListServersTool];
|
|
25785
28294
|
init_allowed_paths();
|
|
25786
28295
|
var BLOCKED_SYSTEM_PATHS = [
|
|
25787
28296
|
"/etc",
|
|
@@ -25915,6 +28424,7 @@ function registerAllTools(registry) {
|
|
|
25915
28424
|
...gitEnhancedTools,
|
|
25916
28425
|
...githubTools,
|
|
25917
28426
|
...openTools,
|
|
28427
|
+
...mcpTools,
|
|
25918
28428
|
...authorizePathTools
|
|
25919
28429
|
];
|
|
25920
28430
|
for (const tool of allTools) {
|
|
@@ -25929,6 +28439,7 @@ function createFullToolRegistry() {
|
|
|
25929
28439
|
|
|
25930
28440
|
// src/index.ts
|
|
25931
28441
|
init_errors();
|
|
28442
|
+
init_logger();
|
|
25932
28443
|
|
|
25933
28444
|
export { ADRGenerator, AnthropicProvider, ArchitectureGenerator, BacklogGenerator, CICDGenerator, CocoError, CodeGenerator, CodeReviewer, CompleteExecutor, ConfigError, ConvergeExecutor, DiscoveryEngine, DockerGenerator, DocsGenerator, OrchestrateExecutor, OutputExecutor, PhaseError, SessionManager, SpecificationGenerator, TaskError, TaskIterator, ToolRegistry, VERSION, configExists, createADRGenerator, createAnthropicProvider, createArchitectureGenerator, createBacklogGenerator, createCICDGenerator, createCodeGenerator, createCodeReviewer, createCompleteExecutor, createConvergeExecutor, createDefaultConfig, createDiscoveryEngine, createDockerGenerator, createDocsGenerator, createFullToolRegistry, createLogger, createOrchestrateExecutor, createOrchestrator, createOutputExecutor, createProvider, createSessionManager, createSpecificationGenerator, createTaskIterator, createToolRegistry, loadConfig, registerAllTools, saveConfig };
|
|
25934
28445
|
//# sourceMappingURL=index.js.map
|