@corbat-tech/coco 2.25.6 → 2.25.8
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 +762 -592
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +2692 -202
- 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();
|
|
@@ -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)
|
|
@@ -16171,9 +16997,9 @@ function createInitialState(config) {
|
|
|
16171
16997
|
}
|
|
16172
16998
|
async function loadExistingState(projectPath) {
|
|
16173
16999
|
try {
|
|
16174
|
-
const
|
|
17000
|
+
const fs39 = await import('fs/promises');
|
|
16175
17001
|
const statePath = `${projectPath}/.coco/state/project.json`;
|
|
16176
|
-
const content = await
|
|
17002
|
+
const content = await fs39.readFile(statePath, "utf-8");
|
|
16177
17003
|
const data = JSON.parse(content);
|
|
16178
17004
|
data.createdAt = new Date(data.createdAt);
|
|
16179
17005
|
data.updatedAt = new Date(data.updatedAt);
|
|
@@ -16183,13 +17009,13 @@ async function loadExistingState(projectPath) {
|
|
|
16183
17009
|
}
|
|
16184
17010
|
}
|
|
16185
17011
|
async function saveState(state) {
|
|
16186
|
-
const
|
|
17012
|
+
const fs39 = await import('fs/promises');
|
|
16187
17013
|
const statePath = `${state.path}/.coco/state`;
|
|
16188
|
-
await
|
|
17014
|
+
await fs39.mkdir(statePath, { recursive: true });
|
|
16189
17015
|
const filePath = `${statePath}/project.json`;
|
|
16190
17016
|
const tmpPath = `${filePath}.tmp.${Date.now()}`;
|
|
16191
|
-
await
|
|
16192
|
-
await
|
|
17017
|
+
await fs39.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8");
|
|
17018
|
+
await fs39.rename(tmpPath, filePath);
|
|
16193
17019
|
}
|
|
16194
17020
|
function getPhaseExecutor(phase) {
|
|
16195
17021
|
switch (phase) {
|
|
@@ -16248,20 +17074,20 @@ async function createPhaseContext(config, state) {
|
|
|
16248
17074
|
};
|
|
16249
17075
|
const tools = {
|
|
16250
17076
|
file: {
|
|
16251
|
-
async read(
|
|
16252
|
-
const
|
|
16253
|
-
return
|
|
17077
|
+
async read(path42) {
|
|
17078
|
+
const fs39 = await import('fs/promises');
|
|
17079
|
+
return fs39.readFile(path42, "utf-8");
|
|
16254
17080
|
},
|
|
16255
|
-
async write(
|
|
16256
|
-
const
|
|
17081
|
+
async write(path42, content) {
|
|
17082
|
+
const fs39 = await import('fs/promises');
|
|
16257
17083
|
const nodePath = await import('path');
|
|
16258
|
-
await
|
|
16259
|
-
await
|
|
17084
|
+
await fs39.mkdir(nodePath.dirname(path42), { recursive: true });
|
|
17085
|
+
await fs39.writeFile(path42, content, "utf-8");
|
|
16260
17086
|
},
|
|
16261
|
-
async exists(
|
|
16262
|
-
const
|
|
17087
|
+
async exists(path42) {
|
|
17088
|
+
const fs39 = await import('fs/promises');
|
|
16263
17089
|
try {
|
|
16264
|
-
await
|
|
17090
|
+
await fs39.access(path42);
|
|
16265
17091
|
return true;
|
|
16266
17092
|
} catch {
|
|
16267
17093
|
return false;
|
|
@@ -16410,9 +17236,9 @@ async function createSnapshot(state) {
|
|
|
16410
17236
|
var MAX_CHECKPOINT_VERSIONS = 5;
|
|
16411
17237
|
async function getCheckpointFiles(state, phase) {
|
|
16412
17238
|
try {
|
|
16413
|
-
const
|
|
17239
|
+
const fs39 = await import('fs/promises');
|
|
16414
17240
|
const checkpointDir = `${state.path}/.coco/checkpoints`;
|
|
16415
|
-
const files = await
|
|
17241
|
+
const files = await fs39.readdir(checkpointDir);
|
|
16416
17242
|
const phaseFiles = files.filter((f) => f.startsWith(`snapshot-pre-${phase}-`) && f.endsWith(".json")).sort((a, b) => {
|
|
16417
17243
|
const tsA = parseInt(a.split("-").pop()?.replace(".json", "") ?? "0", 10);
|
|
16418
17244
|
const tsB = parseInt(b.split("-").pop()?.replace(".json", "") ?? "0", 10);
|
|
@@ -16425,11 +17251,11 @@ async function getCheckpointFiles(state, phase) {
|
|
|
16425
17251
|
}
|
|
16426
17252
|
async function cleanupOldCheckpoints(state, phase) {
|
|
16427
17253
|
try {
|
|
16428
|
-
const
|
|
17254
|
+
const fs39 = await import('fs/promises');
|
|
16429
17255
|
const files = await getCheckpointFiles(state, phase);
|
|
16430
17256
|
if (files.length > MAX_CHECKPOINT_VERSIONS) {
|
|
16431
17257
|
const filesToDelete = files.slice(MAX_CHECKPOINT_VERSIONS);
|
|
16432
|
-
await Promise.all(filesToDelete.map((f) =>
|
|
17258
|
+
await Promise.all(filesToDelete.map((f) => fs39.unlink(f).catch(() => {
|
|
16433
17259
|
})));
|
|
16434
17260
|
}
|
|
16435
17261
|
} catch {
|
|
@@ -16437,13 +17263,13 @@ async function cleanupOldCheckpoints(state, phase) {
|
|
|
16437
17263
|
}
|
|
16438
17264
|
async function saveSnapshot(state, snapshotId) {
|
|
16439
17265
|
try {
|
|
16440
|
-
const
|
|
17266
|
+
const fs39 = await import('fs/promises');
|
|
16441
17267
|
const snapshotPath = `${state.path}/.coco/checkpoints/snapshot-${snapshotId}.json`;
|
|
16442
17268
|
const snapshotDir = `${state.path}/.coco/checkpoints`;
|
|
16443
|
-
await
|
|
17269
|
+
await fs39.mkdir(snapshotDir, { recursive: true });
|
|
16444
17270
|
const createdAt = state.createdAt instanceof Date ? state.createdAt.toISOString() : String(state.createdAt);
|
|
16445
17271
|
const updatedAt = state.updatedAt instanceof Date ? state.updatedAt.toISOString() : String(state.updatedAt);
|
|
16446
|
-
await
|
|
17272
|
+
await fs39.writeFile(
|
|
16447
17273
|
snapshotPath,
|
|
16448
17274
|
JSON.stringify(
|
|
16449
17275
|
{
|
|
@@ -16735,6 +17561,17 @@ var SENSITIVE_PATTERNS = [
|
|
|
16735
17561
|
// PyPI auth
|
|
16736
17562
|
];
|
|
16737
17563
|
var BLOCKED_PATHS = ["/etc", "/var", "/usr", "/root", "/sys", "/proc", "/boot"];
|
|
17564
|
+
var SAFE_COCO_HOME_READ_FILES = /* @__PURE__ */ new Set([
|
|
17565
|
+
"mcp.json",
|
|
17566
|
+
"config.json",
|
|
17567
|
+
"COCO.md",
|
|
17568
|
+
"AGENTS.md",
|
|
17569
|
+
"CLAUDE.md",
|
|
17570
|
+
"projects.json",
|
|
17571
|
+
"trusted-tools.json",
|
|
17572
|
+
"allowed-paths.json"
|
|
17573
|
+
]);
|
|
17574
|
+
var SAFE_COCO_HOME_READ_DIR_PREFIXES = ["skills", "memories", "logs", "checkpoints", "sessions"];
|
|
16738
17575
|
function hasNullByte(str) {
|
|
16739
17576
|
return str.includes("\0");
|
|
16740
17577
|
}
|
|
@@ -16743,6 +17580,33 @@ function normalizePath(filePath) {
|
|
|
16743
17580
|
normalized = path17__default.normalize(normalized);
|
|
16744
17581
|
return normalized;
|
|
16745
17582
|
}
|
|
17583
|
+
function isWithinDirectory(targetPath, baseDir) {
|
|
17584
|
+
const normalizedTarget = path17__default.normalize(targetPath);
|
|
17585
|
+
const normalizedBase = path17__default.normalize(baseDir);
|
|
17586
|
+
return normalizedTarget === normalizedBase || normalizedTarget.startsWith(normalizedBase + path17__default.sep);
|
|
17587
|
+
}
|
|
17588
|
+
function isSafeCocoHomeReadPath(absolutePath, homeDir) {
|
|
17589
|
+
const cocoHome = path17__default.join(homeDir, ".coco");
|
|
17590
|
+
if (!isWithinDirectory(absolutePath, cocoHome)) {
|
|
17591
|
+
return false;
|
|
17592
|
+
}
|
|
17593
|
+
const relativePath = path17__default.relative(cocoHome, absolutePath);
|
|
17594
|
+
if (!relativePath || relativePath.startsWith("..")) {
|
|
17595
|
+
return false;
|
|
17596
|
+
}
|
|
17597
|
+
const segments = relativePath.split(path17__default.sep).filter(Boolean);
|
|
17598
|
+
const firstSegment = segments[0];
|
|
17599
|
+
if (!firstSegment) {
|
|
17600
|
+
return false;
|
|
17601
|
+
}
|
|
17602
|
+
if (firstSegment === "tokens" || firstSegment === ".env") {
|
|
17603
|
+
return false;
|
|
17604
|
+
}
|
|
17605
|
+
if (segments.length === 1 && SAFE_COCO_HOME_READ_FILES.has(firstSegment)) {
|
|
17606
|
+
return true;
|
|
17607
|
+
}
|
|
17608
|
+
return SAFE_COCO_HOME_READ_DIR_PREFIXES.includes(firstSegment);
|
|
17609
|
+
}
|
|
16746
17610
|
function isPathAllowed(filePath, operation) {
|
|
16747
17611
|
if (hasNullByte(filePath)) {
|
|
16748
17612
|
return { allowed: false, reason: "Path contains invalid characters" };
|
|
@@ -16762,6 +17626,9 @@ function isPathAllowed(filePath, operation) {
|
|
|
16762
17626
|
const normalizedCwd = path17__default.normalize(cwd);
|
|
16763
17627
|
if (absolute.startsWith(normalizedHome) && !absolute.startsWith(normalizedCwd)) {
|
|
16764
17628
|
if (isWithinAllowedPath(absolute, operation)) ; else if (operation === "read") {
|
|
17629
|
+
if (isSafeCocoHomeReadPath(absolute, normalizedHome)) {
|
|
17630
|
+
return { allowed: true };
|
|
17631
|
+
}
|
|
16765
17632
|
const allowedHomeReads = [".gitconfig", ".zshrc", ".bashrc"];
|
|
16766
17633
|
const basename5 = path17__default.basename(absolute);
|
|
16767
17634
|
if (!allowedHomeReads.includes(basename5)) {
|
|
@@ -18271,6 +19138,9 @@ var simpleAutoCommitTool = defineTool({
|
|
|
18271
19138
|
}
|
|
18272
19139
|
});
|
|
18273
19140
|
var gitSimpleTools = [checkProtectedBranchTool, simpleAutoCommitTool];
|
|
19141
|
+
|
|
19142
|
+
// src/cli/repl/agents/manager.ts
|
|
19143
|
+
init_logger();
|
|
18274
19144
|
var AGENT_NAMES = {
|
|
18275
19145
|
explore: "Explorer",
|
|
18276
19146
|
plan: "Planner",
|
|
@@ -21763,9 +22633,9 @@ async function fileExists(filePath) {
|
|
|
21763
22633
|
return false;
|
|
21764
22634
|
}
|
|
21765
22635
|
}
|
|
21766
|
-
async function fileExists2(
|
|
22636
|
+
async function fileExists2(path42) {
|
|
21767
22637
|
try {
|
|
21768
|
-
await access(
|
|
22638
|
+
await access(path42);
|
|
21769
22639
|
return true;
|
|
21770
22640
|
} catch {
|
|
21771
22641
|
return false;
|
|
@@ -21855,7 +22725,7 @@ async function detectMaturity(cwd) {
|
|
|
21855
22725
|
if (!hasLintConfig && hasPackageJson) {
|
|
21856
22726
|
try {
|
|
21857
22727
|
const pkgRaw = await import('fs/promises').then(
|
|
21858
|
-
(
|
|
22728
|
+
(fs39) => fs39.readFile(join(cwd, "package.json"), "utf-8")
|
|
21859
22729
|
);
|
|
21860
22730
|
const pkg = JSON.parse(pkgRaw);
|
|
21861
22731
|
if (pkg.scripts?.lint || pkg.scripts?.["lint:fix"]) {
|
|
@@ -25782,6 +26652,1624 @@ Examples:
|
|
|
25782
26652
|
}
|
|
25783
26653
|
});
|
|
25784
26654
|
var openTools = [openFileTool];
|
|
26655
|
+
|
|
26656
|
+
// src/mcp/registry.ts
|
|
26657
|
+
init_types();
|
|
26658
|
+
init_config();
|
|
26659
|
+
init_errors2();
|
|
26660
|
+
var MCPRegistryImpl = class {
|
|
26661
|
+
servers = /* @__PURE__ */ new Map();
|
|
26662
|
+
registryPath;
|
|
26663
|
+
constructor(registryPath) {
|
|
26664
|
+
this.registryPath = registryPath || getDefaultRegistryPath();
|
|
26665
|
+
}
|
|
26666
|
+
/**
|
|
26667
|
+
* Add or update a server configuration
|
|
26668
|
+
*/
|
|
26669
|
+
async addServer(config) {
|
|
26670
|
+
validateServerConfig(config);
|
|
26671
|
+
const existing = this.servers.get(config.name);
|
|
26672
|
+
if (existing) {
|
|
26673
|
+
this.servers.set(config.name, { ...existing, ...config });
|
|
26674
|
+
} else {
|
|
26675
|
+
this.servers.set(config.name, config);
|
|
26676
|
+
}
|
|
26677
|
+
await this.save();
|
|
26678
|
+
}
|
|
26679
|
+
/**
|
|
26680
|
+
* Remove a server by name
|
|
26681
|
+
*/
|
|
26682
|
+
async removeServer(name) {
|
|
26683
|
+
const existed = this.servers.has(name);
|
|
26684
|
+
if (existed) {
|
|
26685
|
+
this.servers.delete(name);
|
|
26686
|
+
await this.save();
|
|
26687
|
+
}
|
|
26688
|
+
return existed;
|
|
26689
|
+
}
|
|
26690
|
+
/**
|
|
26691
|
+
* Get a server configuration by name
|
|
26692
|
+
*/
|
|
26693
|
+
getServer(name) {
|
|
26694
|
+
return this.servers.get(name);
|
|
26695
|
+
}
|
|
26696
|
+
/**
|
|
26697
|
+
* List all registered servers
|
|
26698
|
+
*/
|
|
26699
|
+
listServers() {
|
|
26700
|
+
return Array.from(this.servers.values());
|
|
26701
|
+
}
|
|
26702
|
+
/**
|
|
26703
|
+
* List enabled servers only
|
|
26704
|
+
*/
|
|
26705
|
+
listEnabledServers() {
|
|
26706
|
+
return this.listServers().filter((s) => s.enabled !== false);
|
|
26707
|
+
}
|
|
26708
|
+
/**
|
|
26709
|
+
* Check if a server exists
|
|
26710
|
+
*/
|
|
26711
|
+
hasServer(name) {
|
|
26712
|
+
return this.servers.has(name);
|
|
26713
|
+
}
|
|
26714
|
+
/**
|
|
26715
|
+
* Get registry file path
|
|
26716
|
+
*/
|
|
26717
|
+
getRegistryPath() {
|
|
26718
|
+
return this.registryPath;
|
|
26719
|
+
}
|
|
26720
|
+
/**
|
|
26721
|
+
* Save registry to disk
|
|
26722
|
+
*/
|
|
26723
|
+
async save() {
|
|
26724
|
+
try {
|
|
26725
|
+
await this.ensureDir(this.registryPath);
|
|
26726
|
+
const data = serializeRegistry(this.listServers());
|
|
26727
|
+
await writeFile(this.registryPath, data, "utf-8");
|
|
26728
|
+
} catch (error) {
|
|
26729
|
+
throw new MCPError(
|
|
26730
|
+
-32001 /* TRANSPORT_ERROR */,
|
|
26731
|
+
`Failed to save registry: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
26732
|
+
);
|
|
26733
|
+
}
|
|
26734
|
+
}
|
|
26735
|
+
/**
|
|
26736
|
+
* Load registry from disk
|
|
26737
|
+
*/
|
|
26738
|
+
async load() {
|
|
26739
|
+
if (this.registryPath === getDefaultRegistryPath()) {
|
|
26740
|
+
await migrateMCPData();
|
|
26741
|
+
}
|
|
26742
|
+
try {
|
|
26743
|
+
await access(this.registryPath);
|
|
26744
|
+
const content = await readFile(this.registryPath, "utf-8");
|
|
26745
|
+
let servers = parseRegistry(content);
|
|
26746
|
+
if (servers.length === 0) {
|
|
26747
|
+
try {
|
|
26748
|
+
const { loadMCPConfigFile: loadMCPConfigFile2 } = await Promise.resolve().then(() => (init_config_loader(), config_loader_exports));
|
|
26749
|
+
servers = await loadMCPConfigFile2(this.registryPath);
|
|
26750
|
+
} catch {
|
|
26751
|
+
}
|
|
26752
|
+
}
|
|
26753
|
+
this.servers.clear();
|
|
26754
|
+
for (const server of servers) {
|
|
26755
|
+
try {
|
|
26756
|
+
validateServerConfig(server);
|
|
26757
|
+
this.servers.set(server.name, server);
|
|
26758
|
+
} catch {
|
|
26759
|
+
}
|
|
26760
|
+
}
|
|
26761
|
+
} catch {
|
|
26762
|
+
this.servers.clear();
|
|
26763
|
+
}
|
|
26764
|
+
}
|
|
26765
|
+
/**
|
|
26766
|
+
* Ensure directory exists
|
|
26767
|
+
*/
|
|
26768
|
+
async ensureDir(path42) {
|
|
26769
|
+
await mkdir(dirname(path42), { recursive: true });
|
|
26770
|
+
}
|
|
26771
|
+
};
|
|
26772
|
+
|
|
26773
|
+
// src/tools/mcp.ts
|
|
26774
|
+
init_config_loader();
|
|
26775
|
+
|
|
26776
|
+
// src/mcp/client.ts
|
|
26777
|
+
init_errors2();
|
|
26778
|
+
var DEFAULT_REQUEST_TIMEOUT = 6e4;
|
|
26779
|
+
var MCPClientImpl = class {
|
|
26780
|
+
constructor(transport, requestTimeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
26781
|
+
this.transport = transport;
|
|
26782
|
+
this.requestTimeout = requestTimeout;
|
|
26783
|
+
this.setupTransportHandlers();
|
|
26784
|
+
}
|
|
26785
|
+
requestId = 0;
|
|
26786
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
26787
|
+
initialized = false;
|
|
26788
|
+
serverCapabilities = null;
|
|
26789
|
+
/**
|
|
26790
|
+
* Setup transport message handlers
|
|
26791
|
+
*/
|
|
26792
|
+
setupTransportHandlers() {
|
|
26793
|
+
this.transport.onMessage((message) => {
|
|
26794
|
+
this.handleMessage(message);
|
|
26795
|
+
});
|
|
26796
|
+
this.transport.onError((error) => {
|
|
26797
|
+
this.rejectAllPending(error);
|
|
26798
|
+
});
|
|
26799
|
+
this.transport.onClose(() => {
|
|
26800
|
+
this.initialized = false;
|
|
26801
|
+
this.rejectAllPending(new MCPConnectionError("Connection closed"));
|
|
26802
|
+
});
|
|
26803
|
+
}
|
|
26804
|
+
/**
|
|
26805
|
+
* Handle incoming messages from transport
|
|
26806
|
+
*/
|
|
26807
|
+
handleMessage(message) {
|
|
26808
|
+
const pending = this.pendingRequests.get(message.id);
|
|
26809
|
+
if (!pending) return;
|
|
26810
|
+
clearTimeout(pending.timeout);
|
|
26811
|
+
this.pendingRequests.delete(message.id);
|
|
26812
|
+
if (message.error) {
|
|
26813
|
+
pending.reject(new Error(message.error.message));
|
|
26814
|
+
} else {
|
|
26815
|
+
pending.resolve(message.result);
|
|
26816
|
+
}
|
|
26817
|
+
}
|
|
26818
|
+
/**
|
|
26819
|
+
* Reject all pending requests
|
|
26820
|
+
*/
|
|
26821
|
+
rejectAllPending(error) {
|
|
26822
|
+
for (const [, pending] of this.pendingRequests) {
|
|
26823
|
+
clearTimeout(pending.timeout);
|
|
26824
|
+
pending.reject(error);
|
|
26825
|
+
}
|
|
26826
|
+
this.pendingRequests.clear();
|
|
26827
|
+
}
|
|
26828
|
+
/**
|
|
26829
|
+
* Send a request and wait for response
|
|
26830
|
+
*/
|
|
26831
|
+
async sendRequest(method, params) {
|
|
26832
|
+
if (!this.transport.isConnected()) {
|
|
26833
|
+
throw new MCPConnectionError("Transport not connected");
|
|
26834
|
+
}
|
|
26835
|
+
const id = ++this.requestId;
|
|
26836
|
+
const request = {
|
|
26837
|
+
jsonrpc: "2.0",
|
|
26838
|
+
id,
|
|
26839
|
+
method,
|
|
26840
|
+
params
|
|
26841
|
+
};
|
|
26842
|
+
return new Promise((resolve3, reject) => {
|
|
26843
|
+
const timeout = setTimeout(() => {
|
|
26844
|
+
this.pendingRequests.delete(id);
|
|
26845
|
+
reject(new MCPTimeoutError(`Request '${method}' timed out after ${this.requestTimeout}ms`));
|
|
26846
|
+
}, this.requestTimeout);
|
|
26847
|
+
this.pendingRequests.set(id, {
|
|
26848
|
+
resolve: resolve3,
|
|
26849
|
+
reject,
|
|
26850
|
+
timeout
|
|
26851
|
+
});
|
|
26852
|
+
this.transport.send(request).catch((error) => {
|
|
26853
|
+
clearTimeout(timeout);
|
|
26854
|
+
this.pendingRequests.delete(id);
|
|
26855
|
+
reject(error);
|
|
26856
|
+
});
|
|
26857
|
+
});
|
|
26858
|
+
}
|
|
26859
|
+
/**
|
|
26860
|
+
* Initialize connection to MCP server
|
|
26861
|
+
*/
|
|
26862
|
+
async initialize(params) {
|
|
26863
|
+
if (!this.transport.isConnected()) {
|
|
26864
|
+
await this.transport.connect();
|
|
26865
|
+
}
|
|
26866
|
+
const result = await this.sendRequest("initialize", params);
|
|
26867
|
+
this.serverCapabilities = result.capabilities;
|
|
26868
|
+
this.initialized = true;
|
|
26869
|
+
await this.transport.send({
|
|
26870
|
+
jsonrpc: "2.0",
|
|
26871
|
+
id: ++this.requestId,
|
|
26872
|
+
method: "notifications/initialized"
|
|
26873
|
+
});
|
|
26874
|
+
return result;
|
|
26875
|
+
}
|
|
26876
|
+
/**
|
|
26877
|
+
* List available tools
|
|
26878
|
+
*/
|
|
26879
|
+
async listTools() {
|
|
26880
|
+
this.ensureInitialized();
|
|
26881
|
+
return this.sendRequest("tools/list");
|
|
26882
|
+
}
|
|
26883
|
+
/**
|
|
26884
|
+
* Call a tool on the MCP server
|
|
26885
|
+
*/
|
|
26886
|
+
async callTool(params) {
|
|
26887
|
+
this.ensureInitialized();
|
|
26888
|
+
return this.sendRequest("tools/call", params);
|
|
26889
|
+
}
|
|
26890
|
+
/**
|
|
26891
|
+
* List available resources
|
|
26892
|
+
*/
|
|
26893
|
+
async listResources() {
|
|
26894
|
+
this.ensureInitialized();
|
|
26895
|
+
return this.sendRequest("resources/list");
|
|
26896
|
+
}
|
|
26897
|
+
/**
|
|
26898
|
+
* Read a specific resource by URI
|
|
26899
|
+
*/
|
|
26900
|
+
async readResource(uri) {
|
|
26901
|
+
this.ensureInitialized();
|
|
26902
|
+
return this.sendRequest("resources/read", { uri });
|
|
26903
|
+
}
|
|
26904
|
+
/**
|
|
26905
|
+
* List available prompts
|
|
26906
|
+
*/
|
|
26907
|
+
async listPrompts() {
|
|
26908
|
+
this.ensureInitialized();
|
|
26909
|
+
return this.sendRequest("prompts/list");
|
|
26910
|
+
}
|
|
26911
|
+
/**
|
|
26912
|
+
* Get a specific prompt with arguments
|
|
26913
|
+
*/
|
|
26914
|
+
async getPrompt(name, args) {
|
|
26915
|
+
this.ensureInitialized();
|
|
26916
|
+
return this.sendRequest("prompts/get", {
|
|
26917
|
+
name,
|
|
26918
|
+
arguments: args
|
|
26919
|
+
});
|
|
26920
|
+
}
|
|
26921
|
+
/**
|
|
26922
|
+
* Ensure client is initialized
|
|
26923
|
+
*/
|
|
26924
|
+
ensureInitialized() {
|
|
26925
|
+
if (!this.initialized) {
|
|
26926
|
+
throw new MCPConnectionError("Client not initialized. Call initialize() first.");
|
|
26927
|
+
}
|
|
26928
|
+
}
|
|
26929
|
+
/**
|
|
26930
|
+
* Close the client connection
|
|
26931
|
+
*/
|
|
26932
|
+
async close() {
|
|
26933
|
+
this.initialized = false;
|
|
26934
|
+
await this.transport.disconnect();
|
|
26935
|
+
}
|
|
26936
|
+
/**
|
|
26937
|
+
* Check if client is connected
|
|
26938
|
+
*/
|
|
26939
|
+
isConnected() {
|
|
26940
|
+
return this.transport.isConnected() && this.initialized;
|
|
26941
|
+
}
|
|
26942
|
+
/**
|
|
26943
|
+
* Get server capabilities
|
|
26944
|
+
*/
|
|
26945
|
+
getServerCapabilities() {
|
|
26946
|
+
return this.serverCapabilities;
|
|
26947
|
+
}
|
|
26948
|
+
};
|
|
26949
|
+
|
|
26950
|
+
// src/mcp/transport/stdio.ts
|
|
26951
|
+
init_errors2();
|
|
26952
|
+
var StdioTransport = class {
|
|
26953
|
+
constructor(config) {
|
|
26954
|
+
this.config = config;
|
|
26955
|
+
}
|
|
26956
|
+
process = null;
|
|
26957
|
+
messageCallback = null;
|
|
26958
|
+
errorCallback = null;
|
|
26959
|
+
closeCallback = null;
|
|
26960
|
+
buffer = "";
|
|
26961
|
+
connected = false;
|
|
26962
|
+
/**
|
|
26963
|
+
* Connect to the stdio transport by spawning the process
|
|
26964
|
+
*/
|
|
26965
|
+
async connect() {
|
|
26966
|
+
if (this.connected) {
|
|
26967
|
+
throw new MCPConnectionError("Transport already connected");
|
|
26968
|
+
}
|
|
26969
|
+
return new Promise((resolve3, reject) => {
|
|
26970
|
+
const { command, args = [], env: env2, cwd } = this.config;
|
|
26971
|
+
this.process = spawn(command, args, {
|
|
26972
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26973
|
+
env: { ...process.env, ...env2 },
|
|
26974
|
+
cwd
|
|
26975
|
+
});
|
|
26976
|
+
this.process.on("error", (error) => {
|
|
26977
|
+
reject(new MCPConnectionError(`Failed to spawn process: ${error.message}`));
|
|
26978
|
+
});
|
|
26979
|
+
this.process.on("spawn", () => {
|
|
26980
|
+
this.connected = true;
|
|
26981
|
+
this.setupHandlers();
|
|
26982
|
+
resolve3();
|
|
26983
|
+
});
|
|
26984
|
+
this.process.stderr?.on("data", (data) => {
|
|
26985
|
+
console.debug(`[MCP Server stderr]: ${data.toString()}`);
|
|
26986
|
+
});
|
|
26987
|
+
});
|
|
26988
|
+
}
|
|
26989
|
+
/**
|
|
26990
|
+
* Setup data handlers for the process
|
|
26991
|
+
*/
|
|
26992
|
+
setupHandlers() {
|
|
26993
|
+
if (!this.process?.stdout) return;
|
|
26994
|
+
this.process.stdout.on("data", (data) => {
|
|
26995
|
+
this.handleData(data);
|
|
26996
|
+
});
|
|
26997
|
+
this.process.on("exit", (code) => {
|
|
26998
|
+
this.connected = false;
|
|
26999
|
+
if (code !== 0 && code !== null) {
|
|
27000
|
+
this.errorCallback?.(new MCPTransportError(`Process exited with code ${code}`));
|
|
27001
|
+
}
|
|
27002
|
+
this.closeCallback?.();
|
|
27003
|
+
});
|
|
27004
|
+
this.process.on("close", () => {
|
|
27005
|
+
this.connected = false;
|
|
27006
|
+
this.closeCallback?.();
|
|
27007
|
+
});
|
|
27008
|
+
}
|
|
27009
|
+
/**
|
|
27010
|
+
* Handle incoming data from stdout
|
|
27011
|
+
*/
|
|
27012
|
+
handleData(data) {
|
|
27013
|
+
this.buffer += data.toString();
|
|
27014
|
+
const lines = this.buffer.split("\n");
|
|
27015
|
+
this.buffer = lines.pop() ?? "";
|
|
27016
|
+
for (const line of lines) {
|
|
27017
|
+
const trimmed = line.trim();
|
|
27018
|
+
if (!trimmed) continue;
|
|
27019
|
+
try {
|
|
27020
|
+
const message = JSON.parse(trimmed);
|
|
27021
|
+
this.messageCallback?.(message);
|
|
27022
|
+
} catch {
|
|
27023
|
+
this.errorCallback?.(new MCPTransportError(`Invalid JSON: ${trimmed}`));
|
|
27024
|
+
}
|
|
27025
|
+
}
|
|
27026
|
+
}
|
|
27027
|
+
/**
|
|
27028
|
+
* Send a message through the transport
|
|
27029
|
+
*/
|
|
27030
|
+
async send(message) {
|
|
27031
|
+
if (!this.connected || !this.process?.stdin) {
|
|
27032
|
+
throw new MCPTransportError("Transport not connected");
|
|
27033
|
+
}
|
|
27034
|
+
const line = JSON.stringify(message) + "\n";
|
|
27035
|
+
return new Promise((resolve3, reject) => {
|
|
27036
|
+
if (!this.process?.stdin) {
|
|
27037
|
+
reject(new MCPTransportError("stdin not available"));
|
|
27038
|
+
return;
|
|
27039
|
+
}
|
|
27040
|
+
const stdin = this.process.stdin;
|
|
27041
|
+
const canWrite = stdin.write(line, (error) => {
|
|
27042
|
+
if (error) {
|
|
27043
|
+
reject(new MCPTransportError(`Write error: ${error.message}`));
|
|
27044
|
+
} else {
|
|
27045
|
+
resolve3();
|
|
27046
|
+
}
|
|
27047
|
+
});
|
|
27048
|
+
if (!canWrite) {
|
|
27049
|
+
stdin.once("drain", () => resolve3());
|
|
27050
|
+
}
|
|
27051
|
+
});
|
|
27052
|
+
}
|
|
27053
|
+
/**
|
|
27054
|
+
* Disconnect from the transport
|
|
27055
|
+
*/
|
|
27056
|
+
async disconnect() {
|
|
27057
|
+
if (!this.process) return;
|
|
27058
|
+
return new Promise((resolve3) => {
|
|
27059
|
+
if (!this.process) {
|
|
27060
|
+
resolve3();
|
|
27061
|
+
return;
|
|
27062
|
+
}
|
|
27063
|
+
this.process.stdin?.end();
|
|
27064
|
+
const timeout = setTimeout(() => {
|
|
27065
|
+
this.process?.kill("SIGTERM");
|
|
27066
|
+
}, 5e3);
|
|
27067
|
+
this.process.on("close", () => {
|
|
27068
|
+
clearTimeout(timeout);
|
|
27069
|
+
this.connected = false;
|
|
27070
|
+
this.process = null;
|
|
27071
|
+
resolve3();
|
|
27072
|
+
});
|
|
27073
|
+
if (this.process.killed || !this.connected) {
|
|
27074
|
+
clearTimeout(timeout);
|
|
27075
|
+
this.process = null;
|
|
27076
|
+
resolve3();
|
|
27077
|
+
}
|
|
27078
|
+
});
|
|
27079
|
+
}
|
|
27080
|
+
/**
|
|
27081
|
+
* Set callback for received messages
|
|
27082
|
+
*/
|
|
27083
|
+
onMessage(callback) {
|
|
27084
|
+
this.messageCallback = callback;
|
|
27085
|
+
}
|
|
27086
|
+
/**
|
|
27087
|
+
* Set callback for errors
|
|
27088
|
+
*/
|
|
27089
|
+
onError(callback) {
|
|
27090
|
+
this.errorCallback = callback;
|
|
27091
|
+
}
|
|
27092
|
+
/**
|
|
27093
|
+
* Set callback for connection close
|
|
27094
|
+
*/
|
|
27095
|
+
onClose(callback) {
|
|
27096
|
+
this.closeCallback = callback;
|
|
27097
|
+
}
|
|
27098
|
+
/**
|
|
27099
|
+
* Check if transport is connected
|
|
27100
|
+
*/
|
|
27101
|
+
isConnected() {
|
|
27102
|
+
return this.connected;
|
|
27103
|
+
}
|
|
27104
|
+
};
|
|
27105
|
+
|
|
27106
|
+
// src/mcp/transport/http.ts
|
|
27107
|
+
init_errors2();
|
|
27108
|
+
|
|
27109
|
+
// src/mcp/oauth.ts
|
|
27110
|
+
init_callback_server();
|
|
27111
|
+
init_paths();
|
|
27112
|
+
init_logger();
|
|
27113
|
+
var execFileAsync2 = promisify(execFile);
|
|
27114
|
+
var TOKEN_STORE_PATH = path17__default.join(CONFIG_PATHS.tokens, "mcp-oauth.json");
|
|
27115
|
+
var OAUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
27116
|
+
var logger = getLogger();
|
|
27117
|
+
function getResourceKey(resourceUrl) {
|
|
27118
|
+
const resource = canonicalizeResourceUrl(resourceUrl);
|
|
27119
|
+
return resource.toLowerCase();
|
|
27120
|
+
}
|
|
27121
|
+
function canonicalizeResourceUrl(resourceUrl) {
|
|
27122
|
+
const parsed = new URL(resourceUrl);
|
|
27123
|
+
parsed.search = "";
|
|
27124
|
+
parsed.hash = "";
|
|
27125
|
+
if (parsed.pathname === "/") {
|
|
27126
|
+
return `${parsed.protocol}//${parsed.host}`;
|
|
27127
|
+
}
|
|
27128
|
+
parsed.pathname = parsed.pathname.replace(/\/+$/, "");
|
|
27129
|
+
return parsed.toString();
|
|
27130
|
+
}
|
|
27131
|
+
async function loadStore2() {
|
|
27132
|
+
try {
|
|
27133
|
+
const content = await fs16__default.readFile(TOKEN_STORE_PATH, "utf-8");
|
|
27134
|
+
const parsed = JSON.parse(content);
|
|
27135
|
+
return {
|
|
27136
|
+
tokens: parsed.tokens ?? {},
|
|
27137
|
+
clients: parsed.clients ?? {}
|
|
27138
|
+
};
|
|
27139
|
+
} catch {
|
|
27140
|
+
return { tokens: {}, clients: {} };
|
|
27141
|
+
}
|
|
27142
|
+
}
|
|
27143
|
+
async function saveStore2(store) {
|
|
27144
|
+
await fs16__default.mkdir(path17__default.dirname(TOKEN_STORE_PATH), { recursive: true });
|
|
27145
|
+
await fs16__default.writeFile(TOKEN_STORE_PATH, JSON.stringify(store, null, 2), {
|
|
27146
|
+
encoding: "utf-8",
|
|
27147
|
+
mode: 384
|
|
27148
|
+
});
|
|
27149
|
+
}
|
|
27150
|
+
function isTokenExpired2(token) {
|
|
27151
|
+
if (!token.expiresAt) return false;
|
|
27152
|
+
return Date.now() >= token.expiresAt - 3e4;
|
|
27153
|
+
}
|
|
27154
|
+
async function getStoredMcpOAuthToken(resourceUrl) {
|
|
27155
|
+
const store = await loadStore2();
|
|
27156
|
+
const token = store.tokens[getResourceKey(resourceUrl)];
|
|
27157
|
+
if (!token) return void 0;
|
|
27158
|
+
if (isTokenExpired2(token)) return void 0;
|
|
27159
|
+
return token.accessToken;
|
|
27160
|
+
}
|
|
27161
|
+
function createCodeVerifier() {
|
|
27162
|
+
return randomBytes(32).toString("base64url");
|
|
27163
|
+
}
|
|
27164
|
+
function createCodeChallenge(verifier) {
|
|
27165
|
+
return createHash("sha256").update(verifier).digest("base64url");
|
|
27166
|
+
}
|
|
27167
|
+
function createState() {
|
|
27168
|
+
return randomBytes(16).toString("hex");
|
|
27169
|
+
}
|
|
27170
|
+
async function openBrowser(url) {
|
|
27171
|
+
let safeUrl;
|
|
27172
|
+
try {
|
|
27173
|
+
const parsed = new URL(url);
|
|
27174
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
27175
|
+
return false;
|
|
27176
|
+
}
|
|
27177
|
+
safeUrl = parsed.toString();
|
|
27178
|
+
} catch {
|
|
27179
|
+
return false;
|
|
27180
|
+
}
|
|
27181
|
+
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);
|
|
27182
|
+
const commands = [];
|
|
27183
|
+
if (process.platform === "darwin") {
|
|
27184
|
+
commands.push(
|
|
27185
|
+
{ cmd: "open", args: [safeUrl] },
|
|
27186
|
+
{ cmd: "open", args: ["-a", "Safari", safeUrl] },
|
|
27187
|
+
{ cmd: "open", args: ["-a", "Google Chrome", safeUrl] }
|
|
27188
|
+
);
|
|
27189
|
+
} else if (process.platform === "win32") {
|
|
27190
|
+
commands.push({ cmd: "rundll32", args: ["url.dll,FileProtocolHandler", safeUrl] });
|
|
27191
|
+
} else if (isWSL2) {
|
|
27192
|
+
commands.push(
|
|
27193
|
+
{ cmd: "cmd.exe", args: ["/c", "start", "", safeUrl] },
|
|
27194
|
+
{ cmd: "powershell.exe", args: ["-Command", `Start-Process '${safeUrl}'`] },
|
|
27195
|
+
{ cmd: "wslview", args: [safeUrl] }
|
|
27196
|
+
);
|
|
27197
|
+
} else {
|
|
27198
|
+
commands.push(
|
|
27199
|
+
{ cmd: "xdg-open", args: [safeUrl] },
|
|
27200
|
+
{ cmd: "sensible-browser", args: [safeUrl] },
|
|
27201
|
+
{ cmd: "x-www-browser", args: [safeUrl] },
|
|
27202
|
+
{ cmd: "gnome-open", args: [safeUrl] },
|
|
27203
|
+
{ cmd: "firefox", args: [safeUrl] },
|
|
27204
|
+
{ cmd: "chromium-browser", args: [safeUrl] },
|
|
27205
|
+
{ cmd: "google-chrome", args: [safeUrl] }
|
|
27206
|
+
);
|
|
27207
|
+
}
|
|
27208
|
+
for (const { cmd, args } of commands) {
|
|
27209
|
+
try {
|
|
27210
|
+
await execFileAsync2(cmd, args);
|
|
27211
|
+
return true;
|
|
27212
|
+
} catch {
|
|
27213
|
+
continue;
|
|
27214
|
+
}
|
|
27215
|
+
}
|
|
27216
|
+
return false;
|
|
27217
|
+
}
|
|
27218
|
+
function maskUrlForLogs(rawUrl) {
|
|
27219
|
+
try {
|
|
27220
|
+
const url = new URL(rawUrl);
|
|
27221
|
+
url.search = "";
|
|
27222
|
+
url.hash = "";
|
|
27223
|
+
return url.toString();
|
|
27224
|
+
} catch {
|
|
27225
|
+
return "[invalid-url]";
|
|
27226
|
+
}
|
|
27227
|
+
}
|
|
27228
|
+
function parseResourceMetadataUrl(wwwAuthenticateHeader) {
|
|
27229
|
+
if (!wwwAuthenticateHeader) return void 0;
|
|
27230
|
+
const match = wwwAuthenticateHeader.match(/resource_metadata="([^"]+)"/i);
|
|
27231
|
+
return match?.[1];
|
|
27232
|
+
}
|
|
27233
|
+
function createProtectedMetadataCandidates(resourceUrl, headerUrl) {
|
|
27234
|
+
const candidates = [];
|
|
27235
|
+
if (headerUrl) {
|
|
27236
|
+
candidates.push(headerUrl);
|
|
27237
|
+
}
|
|
27238
|
+
const resource = new URL(resourceUrl);
|
|
27239
|
+
const origin = `${resource.protocol}//${resource.host}`;
|
|
27240
|
+
const pathPart = resource.pathname.replace(/\/+$/, "");
|
|
27241
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource`);
|
|
27242
|
+
if (pathPart && pathPart !== "/") {
|
|
27243
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource${pathPart}`);
|
|
27244
|
+
candidates.push(`${origin}/.well-known/oauth-protected-resource/${pathPart.replace(/^\//, "")}`);
|
|
27245
|
+
}
|
|
27246
|
+
return Array.from(new Set(candidates));
|
|
27247
|
+
}
|
|
27248
|
+
async function fetchJson(url) {
|
|
27249
|
+
const res = await fetch(url, { method: "GET", headers: { Accept: "application/json" } });
|
|
27250
|
+
if (!res.ok) {
|
|
27251
|
+
throw new Error(`HTTP ${res.status} while fetching ${url}`);
|
|
27252
|
+
}
|
|
27253
|
+
return await res.json();
|
|
27254
|
+
}
|
|
27255
|
+
function buildAuthorizationMetadataCandidates(issuer) {
|
|
27256
|
+
const parsed = new URL(issuer);
|
|
27257
|
+
const base = `${parsed.protocol}//${parsed.host}`;
|
|
27258
|
+
const issuerPath = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/+$/, "");
|
|
27259
|
+
const candidates = [
|
|
27260
|
+
`${base}/.well-known/oauth-authorization-server${issuerPath}`,
|
|
27261
|
+
`${base}/.well-known/oauth-authorization-server`,
|
|
27262
|
+
`${base}/.well-known/openid-configuration${issuerPath}`,
|
|
27263
|
+
`${base}/.well-known/openid-configuration`
|
|
27264
|
+
];
|
|
27265
|
+
return Array.from(new Set(candidates));
|
|
27266
|
+
}
|
|
27267
|
+
async function discoverProtectedResourceMetadata(resourceUrl, wwwAuthenticateHeader) {
|
|
27268
|
+
const headerUrl = parseResourceMetadataUrl(wwwAuthenticateHeader);
|
|
27269
|
+
const candidates = createProtectedMetadataCandidates(resourceUrl, headerUrl);
|
|
27270
|
+
for (const candidate of candidates) {
|
|
27271
|
+
try {
|
|
27272
|
+
const metadata = await fetchJson(candidate);
|
|
27273
|
+
if (Array.isArray(metadata.authorization_servers) && metadata.authorization_servers.length > 0) {
|
|
27274
|
+
return metadata;
|
|
27275
|
+
}
|
|
27276
|
+
} catch {
|
|
27277
|
+
}
|
|
27278
|
+
}
|
|
27279
|
+
throw new Error("Could not discover OAuth protected resource metadata for MCP server");
|
|
27280
|
+
}
|
|
27281
|
+
async function discoverAuthorizationServerMetadata(authorizationServer) {
|
|
27282
|
+
const candidates = buildAuthorizationMetadataCandidates(authorizationServer);
|
|
27283
|
+
for (const candidate of candidates) {
|
|
27284
|
+
try {
|
|
27285
|
+
const metadata = await fetchJson(candidate);
|
|
27286
|
+
if (metadata.authorization_endpoint && metadata.token_endpoint) {
|
|
27287
|
+
return metadata;
|
|
27288
|
+
}
|
|
27289
|
+
} catch {
|
|
27290
|
+
}
|
|
27291
|
+
}
|
|
27292
|
+
throw new Error("Could not discover OAuth authorization server metadata");
|
|
27293
|
+
}
|
|
27294
|
+
async function ensureClientId(authorizationMetadata, authorizationServer, redirectUri) {
|
|
27295
|
+
const store = await loadStore2();
|
|
27296
|
+
const clientKey = `${authorizationServer}|${redirectUri}`;
|
|
27297
|
+
const existing = store.clients[clientKey]?.clientId;
|
|
27298
|
+
if (existing) return existing;
|
|
27299
|
+
const registrationEndpoint = authorizationMetadata.registration_endpoint;
|
|
27300
|
+
if (!registrationEndpoint) {
|
|
27301
|
+
throw new Error(
|
|
27302
|
+
"Authorization server does not expose dynamic client registration; configure a static OAuth client ID for this MCP server."
|
|
27303
|
+
);
|
|
27304
|
+
}
|
|
27305
|
+
const registrationPayload = {
|
|
27306
|
+
client_name: "corbat-coco-mcp",
|
|
27307
|
+
redirect_uris: [redirectUri],
|
|
27308
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
27309
|
+
response_types: ["code"],
|
|
27310
|
+
token_endpoint_auth_method: "none"
|
|
27311
|
+
};
|
|
27312
|
+
const response = await fetch(registrationEndpoint, {
|
|
27313
|
+
method: "POST",
|
|
27314
|
+
headers: {
|
|
27315
|
+
"Content-Type": "application/json",
|
|
27316
|
+
Accept: "application/json"
|
|
27317
|
+
},
|
|
27318
|
+
body: JSON.stringify(registrationPayload)
|
|
27319
|
+
});
|
|
27320
|
+
if (!response.ok) {
|
|
27321
|
+
throw new Error(`Dynamic client registration failed: HTTP ${response.status}`);
|
|
27322
|
+
}
|
|
27323
|
+
const data = await response.json();
|
|
27324
|
+
const clientId = data.client_id;
|
|
27325
|
+
if (!clientId) {
|
|
27326
|
+
throw new Error("Dynamic client registration did not return client_id");
|
|
27327
|
+
}
|
|
27328
|
+
store.clients[clientKey] = { clientId };
|
|
27329
|
+
await saveStore2(store);
|
|
27330
|
+
return clientId;
|
|
27331
|
+
}
|
|
27332
|
+
async function refreshAccessToken2(params) {
|
|
27333
|
+
const body = new URLSearchParams({
|
|
27334
|
+
grant_type: "refresh_token",
|
|
27335
|
+
client_id: params.clientId,
|
|
27336
|
+
refresh_token: params.refreshToken,
|
|
27337
|
+
resource: params.resource
|
|
27338
|
+
});
|
|
27339
|
+
const response = await fetch(params.tokenEndpoint, {
|
|
27340
|
+
method: "POST",
|
|
27341
|
+
headers: {
|
|
27342
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
27343
|
+
Accept: "application/json"
|
|
27344
|
+
},
|
|
27345
|
+
body: body.toString()
|
|
27346
|
+
});
|
|
27347
|
+
if (!response.ok) {
|
|
27348
|
+
throw new Error(`Refresh token exchange failed: HTTP ${response.status}`);
|
|
27349
|
+
}
|
|
27350
|
+
const tokenResponse = await response.json();
|
|
27351
|
+
if (!tokenResponse.access_token) {
|
|
27352
|
+
throw new Error("Refresh token response missing access_token");
|
|
27353
|
+
}
|
|
27354
|
+
return tokenResponse;
|
|
27355
|
+
}
|
|
27356
|
+
async function exchangeCodeForToken(tokenEndpoint, clientId, code, codeVerifier, redirectUri, resource) {
|
|
27357
|
+
const body = new URLSearchParams({
|
|
27358
|
+
grant_type: "authorization_code",
|
|
27359
|
+
code,
|
|
27360
|
+
client_id: clientId,
|
|
27361
|
+
redirect_uri: redirectUri,
|
|
27362
|
+
code_verifier: codeVerifier,
|
|
27363
|
+
resource
|
|
27364
|
+
});
|
|
27365
|
+
const response = await fetch(tokenEndpoint, {
|
|
27366
|
+
method: "POST",
|
|
27367
|
+
headers: {
|
|
27368
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
27369
|
+
Accept: "application/json"
|
|
27370
|
+
},
|
|
27371
|
+
body: body.toString()
|
|
27372
|
+
});
|
|
27373
|
+
if (!response.ok) {
|
|
27374
|
+
throw new Error(`Token exchange failed: HTTP ${response.status}`);
|
|
27375
|
+
}
|
|
27376
|
+
const tokenResponse = await response.json();
|
|
27377
|
+
if (!tokenResponse.access_token) {
|
|
27378
|
+
throw new Error("Token exchange response missing access_token");
|
|
27379
|
+
}
|
|
27380
|
+
return tokenResponse;
|
|
27381
|
+
}
|
|
27382
|
+
async function persistToken(resourceUrl, token, metadata) {
|
|
27383
|
+
const store = await loadStore2();
|
|
27384
|
+
const expiresAt = typeof token.expires_in === "number" ? Date.now() + Math.max(0, token.expires_in) * 1e3 : void 0;
|
|
27385
|
+
store.tokens[getResourceKey(resourceUrl)] = {
|
|
27386
|
+
accessToken: token.access_token,
|
|
27387
|
+
tokenType: token.token_type,
|
|
27388
|
+
refreshToken: token.refresh_token,
|
|
27389
|
+
authorizationServer: metadata?.authorizationServer,
|
|
27390
|
+
clientId: metadata?.clientId,
|
|
27391
|
+
resource: canonicalizeResourceUrl(resourceUrl),
|
|
27392
|
+
...expiresAt ? { expiresAt } : {}
|
|
27393
|
+
};
|
|
27394
|
+
await saveStore2(store);
|
|
27395
|
+
}
|
|
27396
|
+
async function authenticateMcpOAuth(params) {
|
|
27397
|
+
const resource = canonicalizeResourceUrl(params.resourceUrl);
|
|
27398
|
+
const store = await loadStore2();
|
|
27399
|
+
const stored = store.tokens[getResourceKey(resource)];
|
|
27400
|
+
if (stored && !params.forceRefresh && !isTokenExpired2(stored)) {
|
|
27401
|
+
return stored.accessToken;
|
|
27402
|
+
}
|
|
27403
|
+
if (!process.stdout.isTTY) {
|
|
27404
|
+
throw new Error(
|
|
27405
|
+
`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.`
|
|
27406
|
+
);
|
|
27407
|
+
}
|
|
27408
|
+
let authorizationServer;
|
|
27409
|
+
let authorizationMetadata;
|
|
27410
|
+
try {
|
|
27411
|
+
const protectedMetadata = await discoverProtectedResourceMetadata(
|
|
27412
|
+
resource,
|
|
27413
|
+
params.wwwAuthenticateHeader
|
|
27414
|
+
);
|
|
27415
|
+
authorizationServer = protectedMetadata.authorization_servers?.[0];
|
|
27416
|
+
if (authorizationServer) {
|
|
27417
|
+
authorizationMetadata = await discoverAuthorizationServerMetadata(authorizationServer);
|
|
27418
|
+
}
|
|
27419
|
+
} catch {
|
|
27420
|
+
}
|
|
27421
|
+
if (!authorizationMetadata) {
|
|
27422
|
+
authorizationMetadata = await discoverAuthorizationServerMetadata(resource);
|
|
27423
|
+
}
|
|
27424
|
+
authorizationServer = authorizationServer ?? authorizationMetadata.issuer ?? new URL(resource).origin;
|
|
27425
|
+
if (stored && stored.refreshToken && stored.clientId && (params.forceRefresh || isTokenExpired2(stored))) {
|
|
27426
|
+
try {
|
|
27427
|
+
const refreshed = await refreshAccessToken2({
|
|
27428
|
+
tokenEndpoint: authorizationMetadata.token_endpoint,
|
|
27429
|
+
clientId: stored.clientId,
|
|
27430
|
+
refreshToken: stored.refreshToken,
|
|
27431
|
+
resource
|
|
27432
|
+
});
|
|
27433
|
+
await persistToken(resource, refreshed, {
|
|
27434
|
+
authorizationServer,
|
|
27435
|
+
clientId: stored.clientId
|
|
27436
|
+
});
|
|
27437
|
+
return refreshed.access_token;
|
|
27438
|
+
} catch {
|
|
27439
|
+
}
|
|
27440
|
+
}
|
|
27441
|
+
const codeVerifier = createCodeVerifier();
|
|
27442
|
+
const codeChallenge = createCodeChallenge(codeVerifier);
|
|
27443
|
+
const state = createState();
|
|
27444
|
+
const { port, resultPromise } = await createCallbackServer(
|
|
27445
|
+
state,
|
|
27446
|
+
OAUTH_TIMEOUT_MS,
|
|
27447
|
+
OAUTH_CALLBACK_PORT
|
|
27448
|
+
);
|
|
27449
|
+
const redirectUri = `http://localhost:${port}/auth/callback`;
|
|
27450
|
+
const clientId = await ensureClientId(authorizationMetadata, authorizationServer, redirectUri);
|
|
27451
|
+
const authUrl = new URL(authorizationMetadata.authorization_endpoint);
|
|
27452
|
+
authUrl.searchParams.set("response_type", "code");
|
|
27453
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
27454
|
+
authUrl.searchParams.set("redirect_uri", redirectUri);
|
|
27455
|
+
authUrl.searchParams.set("state", state);
|
|
27456
|
+
authUrl.searchParams.set("code_challenge", codeChallenge);
|
|
27457
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
27458
|
+
authUrl.searchParams.set("resource", resource);
|
|
27459
|
+
if (authorizationMetadata.scopes_supported?.includes("offline_access")) {
|
|
27460
|
+
authUrl.searchParams.set("scope", "offline_access");
|
|
27461
|
+
}
|
|
27462
|
+
const opened = await openBrowser(authUrl.toString());
|
|
27463
|
+
if (!opened) {
|
|
27464
|
+
logger.warn(`[MCP OAuth] Could not open browser automatically for '${params.serverName}'`);
|
|
27465
|
+
logger.warn(`[MCP OAuth] Manual auth URL base: ${maskUrlForLogs(authUrl.toString())}`);
|
|
27466
|
+
console.log(`[MCP OAuth] Open this URL manually: ${authUrl.toString()}`);
|
|
27467
|
+
} else {
|
|
27468
|
+
logger.info(
|
|
27469
|
+
`[MCP OAuth] Opened browser for '${params.serverName}'. Complete login to continue.`
|
|
27470
|
+
);
|
|
27471
|
+
}
|
|
27472
|
+
const callback = await resultPromise;
|
|
27473
|
+
const token = await exchangeCodeForToken(
|
|
27474
|
+
authorizationMetadata.token_endpoint,
|
|
27475
|
+
clientId,
|
|
27476
|
+
callback.code,
|
|
27477
|
+
codeVerifier,
|
|
27478
|
+
redirectUri,
|
|
27479
|
+
resource
|
|
27480
|
+
);
|
|
27481
|
+
await persistToken(resource, token, { authorizationServer, clientId });
|
|
27482
|
+
return token.access_token;
|
|
27483
|
+
}
|
|
27484
|
+
|
|
27485
|
+
// src/mcp/transport/http.ts
|
|
27486
|
+
init_errors2();
|
|
27487
|
+
var HTTPTransport = class {
|
|
27488
|
+
constructor(config) {
|
|
27489
|
+
this.config = config;
|
|
27490
|
+
this.config.timeout = config.timeout ?? 6e4;
|
|
27491
|
+
this.config.retries = config.retries ?? 3;
|
|
27492
|
+
}
|
|
27493
|
+
messageCallback = null;
|
|
27494
|
+
errorCallback = null;
|
|
27495
|
+
// Used to report transport errors to the client
|
|
27496
|
+
reportError(error) {
|
|
27497
|
+
this.errorCallback?.(error);
|
|
27498
|
+
}
|
|
27499
|
+
closeCallback = null;
|
|
27500
|
+
connected = false;
|
|
27501
|
+
abortController = null;
|
|
27502
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
27503
|
+
oauthToken;
|
|
27504
|
+
oauthInFlight = null;
|
|
27505
|
+
/**
|
|
27506
|
+
* Get authentication token
|
|
27507
|
+
*/
|
|
27508
|
+
getAuthToken() {
|
|
27509
|
+
if (this.oauthToken) {
|
|
27510
|
+
return this.oauthToken;
|
|
27511
|
+
}
|
|
27512
|
+
if (!this.config.auth) return void 0;
|
|
27513
|
+
if (this.config.auth.token) {
|
|
27514
|
+
return this.config.auth.token;
|
|
27515
|
+
}
|
|
27516
|
+
if (this.config.auth.tokenEnv) {
|
|
27517
|
+
return process.env[this.config.auth.tokenEnv];
|
|
27518
|
+
}
|
|
27519
|
+
return void 0;
|
|
27520
|
+
}
|
|
27521
|
+
/**
|
|
27522
|
+
* Build request headers
|
|
27523
|
+
*/
|
|
27524
|
+
buildHeaders() {
|
|
27525
|
+
const headers = {
|
|
27526
|
+
"Content-Type": "application/json",
|
|
27527
|
+
Accept: "application/json",
|
|
27528
|
+
...this.config.headers
|
|
27529
|
+
};
|
|
27530
|
+
if (this.oauthToken) {
|
|
27531
|
+
headers["Authorization"] = `Bearer ${this.oauthToken}`;
|
|
27532
|
+
return headers;
|
|
27533
|
+
}
|
|
27534
|
+
const token = this.getAuthToken();
|
|
27535
|
+
if (token && this.config.auth) {
|
|
27536
|
+
if (this.config.auth.type === "apikey") {
|
|
27537
|
+
headers[this.config.auth.headerName || "X-API-Key"] = token;
|
|
27538
|
+
} else {
|
|
27539
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
27540
|
+
}
|
|
27541
|
+
}
|
|
27542
|
+
return headers;
|
|
27543
|
+
}
|
|
27544
|
+
shouldAttemptOAuth() {
|
|
27545
|
+
if (this.config.auth?.type === "apikey") {
|
|
27546
|
+
return false;
|
|
27547
|
+
}
|
|
27548
|
+
if (this.config.auth?.type === "bearer") {
|
|
27549
|
+
return !this.getAuthToken();
|
|
27550
|
+
}
|
|
27551
|
+
return true;
|
|
27552
|
+
}
|
|
27553
|
+
async ensureOAuthToken(wwwAuthenticateHeader, options) {
|
|
27554
|
+
if (this.oauthToken && !options?.forceRefresh) {
|
|
27555
|
+
return this.oauthToken;
|
|
27556
|
+
}
|
|
27557
|
+
if (this.oauthInFlight) {
|
|
27558
|
+
return this.oauthInFlight;
|
|
27559
|
+
}
|
|
27560
|
+
const serverName = this.config.name ?? this.config.url;
|
|
27561
|
+
if (options?.forceRefresh) {
|
|
27562
|
+
this.oauthToken = void 0;
|
|
27563
|
+
}
|
|
27564
|
+
this.oauthInFlight = authenticateMcpOAuth({
|
|
27565
|
+
serverName,
|
|
27566
|
+
resourceUrl: this.config.url,
|
|
27567
|
+
wwwAuthenticateHeader,
|
|
27568
|
+
forceRefresh: options?.forceRefresh
|
|
27569
|
+
}).then((token) => {
|
|
27570
|
+
this.oauthToken = token;
|
|
27571
|
+
return token;
|
|
27572
|
+
}).finally(() => {
|
|
27573
|
+
this.oauthInFlight = null;
|
|
27574
|
+
});
|
|
27575
|
+
return this.oauthInFlight;
|
|
27576
|
+
}
|
|
27577
|
+
async sendRequestWithOAuthRetry(method, body, signal) {
|
|
27578
|
+
const doFetch = async () => fetch(this.config.url, {
|
|
27579
|
+
method,
|
|
27580
|
+
headers: this.buildHeaders(),
|
|
27581
|
+
...body ? { body } : {},
|
|
27582
|
+
signal
|
|
27583
|
+
});
|
|
27584
|
+
let response = await doFetch();
|
|
27585
|
+
if (response.status !== 401 || !this.shouldAttemptOAuth()) {
|
|
27586
|
+
if (this.shouldAttemptOAuth() && !this.oauthToken && response.headers.get("www-authenticate")) {
|
|
27587
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"));
|
|
27588
|
+
response = await doFetch();
|
|
27589
|
+
}
|
|
27590
|
+
return response;
|
|
27591
|
+
}
|
|
27592
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"), { forceRefresh: true });
|
|
27593
|
+
response = await doFetch();
|
|
27594
|
+
return response;
|
|
27595
|
+
}
|
|
27596
|
+
looksLikeAuthErrorMessage(message) {
|
|
27597
|
+
if (!message) return false;
|
|
27598
|
+
const msg = message.toLowerCase();
|
|
27599
|
+
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");
|
|
27600
|
+
const hasVendorHint = msg.includes("gemini cli") || msg.includes("jira") || msg.includes("confluence") || msg.includes("atlassian");
|
|
27601
|
+
const hasWeakAuthSignal = msg.includes("authenticate") || msg.includes("token") || msg.includes("authorization");
|
|
27602
|
+
return hasStrongAuthSignal || // Vendor-specific hints alone are not enough; require an auth-related token too.
|
|
27603
|
+
hasVendorHint && hasWeakAuthSignal;
|
|
27604
|
+
}
|
|
27605
|
+
isJsonRpcAuthError(payload) {
|
|
27606
|
+
if (!payload.error) return false;
|
|
27607
|
+
return this.looksLikeAuthErrorMessage(payload.error.message);
|
|
27608
|
+
}
|
|
27609
|
+
/**
|
|
27610
|
+
* Connect to the HTTP transport
|
|
27611
|
+
*/
|
|
27612
|
+
async connect() {
|
|
27613
|
+
if (this.connected) {
|
|
27614
|
+
throw new MCPConnectionError("Transport already connected");
|
|
27615
|
+
}
|
|
27616
|
+
try {
|
|
27617
|
+
new URL(this.config.url);
|
|
27618
|
+
} catch {
|
|
27619
|
+
throw new MCPConnectionError(`Invalid URL: ${this.config.url}`);
|
|
27620
|
+
}
|
|
27621
|
+
try {
|
|
27622
|
+
this.abortController = new AbortController();
|
|
27623
|
+
if (this.shouldAttemptOAuth()) {
|
|
27624
|
+
this.oauthToken = await getStoredMcpOAuthToken(this.config.url);
|
|
27625
|
+
}
|
|
27626
|
+
const response = await this.sendRequestWithOAuthRetry(
|
|
27627
|
+
"GET",
|
|
27628
|
+
void 0,
|
|
27629
|
+
this.abortController.signal
|
|
27630
|
+
);
|
|
27631
|
+
if (!response.ok && response.status !== 404) {
|
|
27632
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
27633
|
+
}
|
|
27634
|
+
this.connected = true;
|
|
27635
|
+
} catch (error) {
|
|
27636
|
+
if (error instanceof MCPError) {
|
|
27637
|
+
this.reportError(error);
|
|
27638
|
+
throw error;
|
|
27639
|
+
}
|
|
27640
|
+
const connError = new MCPConnectionError(
|
|
27641
|
+
`Failed to connect: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
27642
|
+
);
|
|
27643
|
+
this.reportError(connError);
|
|
27644
|
+
throw connError;
|
|
27645
|
+
}
|
|
27646
|
+
}
|
|
27647
|
+
/**
|
|
27648
|
+
* Send a message through the transport
|
|
27649
|
+
*/
|
|
27650
|
+
async send(message) {
|
|
27651
|
+
if (!this.connected) {
|
|
27652
|
+
throw new MCPTransportError("Transport not connected");
|
|
27653
|
+
}
|
|
27654
|
+
const abortController = new AbortController();
|
|
27655
|
+
this.pendingRequests.set(message.id, abortController);
|
|
27656
|
+
let lastError;
|
|
27657
|
+
for (let attempt = 0; attempt < this.config.retries; attempt++) {
|
|
27658
|
+
try {
|
|
27659
|
+
const timeoutId = setTimeout(() => {
|
|
27660
|
+
abortController.abort();
|
|
27661
|
+
}, this.config.timeout);
|
|
27662
|
+
const response = await this.sendRequestWithOAuthRetry(
|
|
27663
|
+
"POST",
|
|
27664
|
+
JSON.stringify(message),
|
|
27665
|
+
abortController.signal
|
|
27666
|
+
);
|
|
27667
|
+
clearTimeout(timeoutId);
|
|
27668
|
+
if (!response.ok) {
|
|
27669
|
+
throw new MCPTransportError(`HTTP error ${response.status}: ${response.statusText}`);
|
|
27670
|
+
}
|
|
27671
|
+
const data = await response.json();
|
|
27672
|
+
if (this.shouldAttemptOAuth() && this.isJsonRpcAuthError(data)) {
|
|
27673
|
+
await this.ensureOAuthToken(response.headers.get("www-authenticate"), {
|
|
27674
|
+
forceRefresh: true
|
|
27675
|
+
});
|
|
27676
|
+
const retryResponse = await this.sendRequestWithOAuthRetry(
|
|
27677
|
+
"POST",
|
|
27678
|
+
JSON.stringify(message),
|
|
27679
|
+
abortController.signal
|
|
27680
|
+
);
|
|
27681
|
+
if (!retryResponse.ok) {
|
|
27682
|
+
throw new MCPTransportError(
|
|
27683
|
+
`HTTP error ${retryResponse.status}: ${retryResponse.statusText}`
|
|
27684
|
+
);
|
|
27685
|
+
}
|
|
27686
|
+
const retryData = await retryResponse.json();
|
|
27687
|
+
this.messageCallback?.(retryData);
|
|
27688
|
+
return;
|
|
27689
|
+
}
|
|
27690
|
+
this.messageCallback?.(data);
|
|
27691
|
+
return;
|
|
27692
|
+
} catch (error) {
|
|
27693
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
27694
|
+
if (error instanceof MCPTransportError) {
|
|
27695
|
+
this.reportError(error);
|
|
27696
|
+
throw error;
|
|
27697
|
+
}
|
|
27698
|
+
if (attempt < this.config.retries - 1) {
|
|
27699
|
+
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 1e3));
|
|
27700
|
+
}
|
|
27701
|
+
}
|
|
27702
|
+
}
|
|
27703
|
+
this.pendingRequests.delete(message.id);
|
|
27704
|
+
throw new MCPTransportError(
|
|
27705
|
+
`Request failed after ${this.config.retries} attempts: ${lastError?.message}`
|
|
27706
|
+
);
|
|
27707
|
+
}
|
|
27708
|
+
/**
|
|
27709
|
+
* Disconnect from the transport
|
|
27710
|
+
*/
|
|
27711
|
+
async disconnect() {
|
|
27712
|
+
for (const [, controller] of this.pendingRequests) {
|
|
27713
|
+
controller.abort();
|
|
27714
|
+
}
|
|
27715
|
+
this.pendingRequests.clear();
|
|
27716
|
+
this.abortController?.abort();
|
|
27717
|
+
this.connected = false;
|
|
27718
|
+
this.closeCallback?.();
|
|
27719
|
+
}
|
|
27720
|
+
/**
|
|
27721
|
+
* Set callback for received messages
|
|
27722
|
+
*/
|
|
27723
|
+
onMessage(callback) {
|
|
27724
|
+
this.messageCallback = callback;
|
|
27725
|
+
}
|
|
27726
|
+
/**
|
|
27727
|
+
* Set callback for errors
|
|
27728
|
+
*/
|
|
27729
|
+
onError(callback) {
|
|
27730
|
+
this.errorCallback = callback;
|
|
27731
|
+
}
|
|
27732
|
+
/**
|
|
27733
|
+
* Set callback for connection close
|
|
27734
|
+
*/
|
|
27735
|
+
onClose(callback) {
|
|
27736
|
+
this.closeCallback = callback;
|
|
27737
|
+
}
|
|
27738
|
+
/**
|
|
27739
|
+
* Check if transport is connected
|
|
27740
|
+
*/
|
|
27741
|
+
isConnected() {
|
|
27742
|
+
return this.connected;
|
|
27743
|
+
}
|
|
27744
|
+
/**
|
|
27745
|
+
* Get transport URL
|
|
27746
|
+
*/
|
|
27747
|
+
getURL() {
|
|
27748
|
+
return this.config.url;
|
|
27749
|
+
}
|
|
27750
|
+
/**
|
|
27751
|
+
* Get auth type
|
|
27752
|
+
*/
|
|
27753
|
+
getAuthType() {
|
|
27754
|
+
return this.config.auth?.type;
|
|
27755
|
+
}
|
|
27756
|
+
};
|
|
27757
|
+
|
|
27758
|
+
// src/mcp/transport/sse.ts
|
|
27759
|
+
init_errors2();
|
|
27760
|
+
var DEFAULT_CONFIG2 = {
|
|
27761
|
+
initialReconnectDelay: 1e3,
|
|
27762
|
+
maxReconnectDelay: 3e4,
|
|
27763
|
+
maxReconnectAttempts: 10
|
|
27764
|
+
};
|
|
27765
|
+
var SSETransport = class {
|
|
27766
|
+
config;
|
|
27767
|
+
connected = false;
|
|
27768
|
+
abortController = null;
|
|
27769
|
+
reconnectAttempts = 0;
|
|
27770
|
+
lastEventId = null;
|
|
27771
|
+
messageEndpoint = null;
|
|
27772
|
+
messageHandler = null;
|
|
27773
|
+
errorHandler = null;
|
|
27774
|
+
closeHandler = null;
|
|
27775
|
+
constructor(config) {
|
|
27776
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
27777
|
+
}
|
|
27778
|
+
/**
|
|
27779
|
+
* Connect to the SSE endpoint
|
|
27780
|
+
*/
|
|
27781
|
+
async connect() {
|
|
27782
|
+
if (this.connected) return;
|
|
27783
|
+
this.abortController = new AbortController();
|
|
27784
|
+
this.reconnectAttempts = 0;
|
|
27785
|
+
this.connected = true;
|
|
27786
|
+
try {
|
|
27787
|
+
await this.startListening();
|
|
27788
|
+
} catch (error) {
|
|
27789
|
+
this.connected = false;
|
|
27790
|
+
throw error;
|
|
27791
|
+
}
|
|
27792
|
+
}
|
|
27793
|
+
/**
|
|
27794
|
+
* Disconnect from the SSE endpoint
|
|
27795
|
+
*/
|
|
27796
|
+
async disconnect() {
|
|
27797
|
+
this.connected = false;
|
|
27798
|
+
this.abortController?.abort();
|
|
27799
|
+
this.abortController = null;
|
|
27800
|
+
this.messageEndpoint = null;
|
|
27801
|
+
this.closeHandler?.();
|
|
27802
|
+
}
|
|
27803
|
+
/**
|
|
27804
|
+
* Send a JSON-RPC message via HTTP POST
|
|
27805
|
+
*/
|
|
27806
|
+
async send(message) {
|
|
27807
|
+
if (!this.connected) {
|
|
27808
|
+
throw new MCPConnectionError("Not connected to SSE endpoint");
|
|
27809
|
+
}
|
|
27810
|
+
const endpoint = this.messageEndpoint ?? `${this.config.url}/message`;
|
|
27811
|
+
try {
|
|
27812
|
+
const response = await fetch(endpoint, {
|
|
27813
|
+
method: "POST",
|
|
27814
|
+
headers: {
|
|
27815
|
+
"Content-Type": "application/json",
|
|
27816
|
+
...this.config.headers
|
|
27817
|
+
},
|
|
27818
|
+
body: JSON.stringify(message),
|
|
27819
|
+
signal: this.abortController?.signal
|
|
27820
|
+
});
|
|
27821
|
+
if (!response.ok) {
|
|
27822
|
+
throw new MCPTransportError(`HTTP POST failed: ${response.status} ${response.statusText}`);
|
|
27823
|
+
}
|
|
27824
|
+
} catch (error) {
|
|
27825
|
+
if (error.name === "AbortError") return;
|
|
27826
|
+
if (error instanceof MCPTransportError) throw error;
|
|
27827
|
+
throw new MCPTransportError(
|
|
27828
|
+
`Failed to send message: ${error instanceof Error ? error.message : String(error)}`
|
|
27829
|
+
);
|
|
27830
|
+
}
|
|
27831
|
+
}
|
|
27832
|
+
/**
|
|
27833
|
+
* Register message handler
|
|
27834
|
+
*/
|
|
27835
|
+
onMessage(handler) {
|
|
27836
|
+
this.messageHandler = handler;
|
|
27837
|
+
}
|
|
27838
|
+
/**
|
|
27839
|
+
* Register error handler
|
|
27840
|
+
*/
|
|
27841
|
+
onError(handler) {
|
|
27842
|
+
this.errorHandler = handler;
|
|
27843
|
+
}
|
|
27844
|
+
/**
|
|
27845
|
+
* Register close handler
|
|
27846
|
+
*/
|
|
27847
|
+
onClose(handler) {
|
|
27848
|
+
this.closeHandler = handler;
|
|
27849
|
+
}
|
|
27850
|
+
/**
|
|
27851
|
+
* Check if connected
|
|
27852
|
+
*/
|
|
27853
|
+
isConnected() {
|
|
27854
|
+
return this.connected;
|
|
27855
|
+
}
|
|
27856
|
+
/**
|
|
27857
|
+
* Start listening to the SSE stream
|
|
27858
|
+
*/
|
|
27859
|
+
async startListening() {
|
|
27860
|
+
const headers = {
|
|
27861
|
+
Accept: "text/event-stream",
|
|
27862
|
+
"Cache-Control": "no-cache",
|
|
27863
|
+
...this.config.headers
|
|
27864
|
+
};
|
|
27865
|
+
if (this.lastEventId) {
|
|
27866
|
+
headers["Last-Event-ID"] = this.lastEventId;
|
|
27867
|
+
}
|
|
27868
|
+
try {
|
|
27869
|
+
const response = await fetch(this.config.url, {
|
|
27870
|
+
method: "GET",
|
|
27871
|
+
headers,
|
|
27872
|
+
signal: this.abortController?.signal
|
|
27873
|
+
});
|
|
27874
|
+
if (!response.ok) {
|
|
27875
|
+
throw new MCPConnectionError(
|
|
27876
|
+
`SSE connection failed: ${response.status} ${response.statusText}`
|
|
27877
|
+
);
|
|
27878
|
+
}
|
|
27879
|
+
if (!response.body) {
|
|
27880
|
+
throw new MCPConnectionError("SSE response has no body");
|
|
27881
|
+
}
|
|
27882
|
+
this.processStream(response.body);
|
|
27883
|
+
} catch (error) {
|
|
27884
|
+
if (error.name === "AbortError") return;
|
|
27885
|
+
if (error instanceof MCPConnectionError) throw error;
|
|
27886
|
+
throw new MCPConnectionError(
|
|
27887
|
+
`Failed to connect to SSE: ${error instanceof Error ? error.message : String(error)}`
|
|
27888
|
+
);
|
|
27889
|
+
}
|
|
27890
|
+
}
|
|
27891
|
+
/**
|
|
27892
|
+
* Process the SSE stream
|
|
27893
|
+
*/
|
|
27894
|
+
async processStream(body) {
|
|
27895
|
+
const reader = body.getReader();
|
|
27896
|
+
const decoder = new TextDecoder();
|
|
27897
|
+
let buffer = "";
|
|
27898
|
+
let eventType = "";
|
|
27899
|
+
let eventData = "";
|
|
27900
|
+
let eventId = "";
|
|
27901
|
+
try {
|
|
27902
|
+
while (this.connected) {
|
|
27903
|
+
const { done, value } = await reader.read();
|
|
27904
|
+
if (done) {
|
|
27905
|
+
if (this.connected) {
|
|
27906
|
+
await this.handleReconnect();
|
|
27907
|
+
}
|
|
27908
|
+
return;
|
|
27909
|
+
}
|
|
27910
|
+
buffer += decoder.decode(value, { stream: true });
|
|
27911
|
+
const lines = buffer.split("\n");
|
|
27912
|
+
buffer = lines.pop() ?? "";
|
|
27913
|
+
for (const line of lines) {
|
|
27914
|
+
if (line === "") {
|
|
27915
|
+
if (eventData) {
|
|
27916
|
+
this.handleEvent(eventType, eventData, eventId);
|
|
27917
|
+
eventType = "";
|
|
27918
|
+
eventData = "";
|
|
27919
|
+
eventId = "";
|
|
27920
|
+
}
|
|
27921
|
+
continue;
|
|
27922
|
+
}
|
|
27923
|
+
if (line.startsWith(":")) {
|
|
27924
|
+
continue;
|
|
27925
|
+
}
|
|
27926
|
+
const colonIdx = line.indexOf(":");
|
|
27927
|
+
if (colonIdx === -1) continue;
|
|
27928
|
+
const field = line.slice(0, colonIdx);
|
|
27929
|
+
const value2 = line.slice(colonIdx + 1).trimStart();
|
|
27930
|
+
switch (field) {
|
|
27931
|
+
case "event":
|
|
27932
|
+
eventType = value2;
|
|
27933
|
+
break;
|
|
27934
|
+
case "data":
|
|
27935
|
+
eventData += (eventData ? "\n" : "") + value2;
|
|
27936
|
+
break;
|
|
27937
|
+
case "id":
|
|
27938
|
+
eventId = value2;
|
|
27939
|
+
break;
|
|
27940
|
+
case "retry":
|
|
27941
|
+
const delay = parseInt(value2, 10);
|
|
27942
|
+
if (!isNaN(delay)) {
|
|
27943
|
+
this.config.initialReconnectDelay = delay;
|
|
27944
|
+
}
|
|
27945
|
+
break;
|
|
27946
|
+
}
|
|
27947
|
+
}
|
|
27948
|
+
}
|
|
27949
|
+
} catch (error) {
|
|
27950
|
+
if (error.name === "AbortError") return;
|
|
27951
|
+
this.errorHandler?.(error instanceof Error ? error : new Error(String(error)));
|
|
27952
|
+
if (this.connected) {
|
|
27953
|
+
await this.handleReconnect();
|
|
27954
|
+
}
|
|
27955
|
+
} finally {
|
|
27956
|
+
reader.releaseLock();
|
|
27957
|
+
}
|
|
27958
|
+
}
|
|
27959
|
+
/**
|
|
27960
|
+
* Handle a complete SSE event
|
|
27961
|
+
*/
|
|
27962
|
+
handleEvent(type, data, id) {
|
|
27963
|
+
if (id) {
|
|
27964
|
+
this.lastEventId = id;
|
|
27965
|
+
}
|
|
27966
|
+
if (type === "endpoint") {
|
|
27967
|
+
this.messageEndpoint = data;
|
|
27968
|
+
return;
|
|
27969
|
+
}
|
|
27970
|
+
try {
|
|
27971
|
+
const message = JSON.parse(data);
|
|
27972
|
+
this.messageHandler?.(message);
|
|
27973
|
+
} catch {
|
|
27974
|
+
this.errorHandler?.(new Error(`Invalid JSON in SSE event: ${data.slice(0, 100)}`));
|
|
27975
|
+
}
|
|
27976
|
+
}
|
|
27977
|
+
/**
|
|
27978
|
+
* Handle reconnection with exponential backoff
|
|
27979
|
+
*/
|
|
27980
|
+
async handleReconnect() {
|
|
27981
|
+
if (!this.connected || this.reconnectAttempts >= this.config.maxReconnectAttempts) {
|
|
27982
|
+
this.connected = false;
|
|
27983
|
+
this.closeHandler?.();
|
|
27984
|
+
return;
|
|
27985
|
+
}
|
|
27986
|
+
this.reconnectAttempts++;
|
|
27987
|
+
const delay = Math.min(
|
|
27988
|
+
this.config.initialReconnectDelay * Math.pow(2, this.reconnectAttempts - 1),
|
|
27989
|
+
this.config.maxReconnectDelay
|
|
27990
|
+
);
|
|
27991
|
+
await new Promise((resolve3) => setTimeout(resolve3, delay));
|
|
27992
|
+
if (!this.connected) return;
|
|
27993
|
+
try {
|
|
27994
|
+
await this.startListening();
|
|
27995
|
+
this.reconnectAttempts = 0;
|
|
27996
|
+
} catch {
|
|
27997
|
+
if (this.connected) {
|
|
27998
|
+
await this.handleReconnect();
|
|
27999
|
+
}
|
|
28000
|
+
}
|
|
28001
|
+
}
|
|
28002
|
+
};
|
|
28003
|
+
|
|
28004
|
+
// src/mcp/lifecycle.ts
|
|
28005
|
+
init_errors2();
|
|
28006
|
+
init_logger();
|
|
28007
|
+
var MCPServerManager = class {
|
|
28008
|
+
connections = /* @__PURE__ */ new Map();
|
|
28009
|
+
logger = getLogger();
|
|
28010
|
+
/**
|
|
28011
|
+
* Create transport for a server config
|
|
28012
|
+
*/
|
|
28013
|
+
createTransport(config) {
|
|
28014
|
+
switch (config.transport) {
|
|
28015
|
+
case "stdio": {
|
|
28016
|
+
if (!config.stdio?.command) {
|
|
28017
|
+
throw new MCPConnectionError(`Server '${config.name}' requires stdio.command`);
|
|
28018
|
+
}
|
|
28019
|
+
return new StdioTransport({
|
|
28020
|
+
command: config.stdio.command,
|
|
28021
|
+
args: config.stdio.args ?? [],
|
|
28022
|
+
env: config.stdio.env
|
|
28023
|
+
});
|
|
28024
|
+
}
|
|
28025
|
+
case "http": {
|
|
28026
|
+
if (!config.http?.url) {
|
|
28027
|
+
throw new MCPConnectionError(`Server '${config.name}' requires http.url`);
|
|
28028
|
+
}
|
|
28029
|
+
return new HTTPTransport({
|
|
28030
|
+
name: config.name,
|
|
28031
|
+
url: config.http.url,
|
|
28032
|
+
headers: config.http.headers,
|
|
28033
|
+
auth: config.http.auth
|
|
28034
|
+
});
|
|
28035
|
+
}
|
|
28036
|
+
case "sse": {
|
|
28037
|
+
if (!config.http?.url) {
|
|
28038
|
+
throw new MCPConnectionError(`Server '${config.name}' requires http.url for SSE`);
|
|
28039
|
+
}
|
|
28040
|
+
return new SSETransport({
|
|
28041
|
+
url: config.http.url,
|
|
28042
|
+
headers: config.http.headers
|
|
28043
|
+
});
|
|
28044
|
+
}
|
|
28045
|
+
default:
|
|
28046
|
+
throw new MCPConnectionError(`Unsupported transport: ${config.transport}`);
|
|
28047
|
+
}
|
|
28048
|
+
}
|
|
28049
|
+
/**
|
|
28050
|
+
* Start a single server
|
|
28051
|
+
*/
|
|
28052
|
+
async startServer(config) {
|
|
28053
|
+
if (this.connections.has(config.name)) {
|
|
28054
|
+
this.logger.warn(`Server '${config.name}' already connected`);
|
|
28055
|
+
return this.connections.get(config.name);
|
|
28056
|
+
}
|
|
28057
|
+
this.logger.info(`Starting MCP server: ${config.name}`);
|
|
28058
|
+
const transport = this.createTransport(config);
|
|
28059
|
+
await transport.connect();
|
|
28060
|
+
const client = new MCPClientImpl(transport);
|
|
28061
|
+
await client.initialize({
|
|
28062
|
+
protocolVersion: "2024-11-05",
|
|
28063
|
+
capabilities: {},
|
|
28064
|
+
clientInfo: { name: "coco-mcp-client", version: VERSION }
|
|
28065
|
+
});
|
|
28066
|
+
let toolCount = 0;
|
|
28067
|
+
try {
|
|
28068
|
+
const { tools } = await client.listTools();
|
|
28069
|
+
toolCount = tools.length;
|
|
28070
|
+
} catch {
|
|
28071
|
+
}
|
|
28072
|
+
const connection = {
|
|
28073
|
+
name: config.name,
|
|
28074
|
+
client,
|
|
28075
|
+
transport,
|
|
28076
|
+
config,
|
|
28077
|
+
connectedAt: /* @__PURE__ */ new Date(),
|
|
28078
|
+
toolCount,
|
|
28079
|
+
healthy: true
|
|
28080
|
+
};
|
|
28081
|
+
this.connections.set(config.name, connection);
|
|
28082
|
+
this.logger.info(`Server '${config.name}' started with ${toolCount} tools`);
|
|
28083
|
+
return connection;
|
|
28084
|
+
}
|
|
28085
|
+
/**
|
|
28086
|
+
* Stop a single server
|
|
28087
|
+
*/
|
|
28088
|
+
async stopServer(name) {
|
|
28089
|
+
const connection = this.connections.get(name);
|
|
28090
|
+
if (!connection) {
|
|
28091
|
+
this.logger.warn(`Server '${name}' not found`);
|
|
28092
|
+
return;
|
|
28093
|
+
}
|
|
28094
|
+
this.logger.info(`Stopping MCP server: ${name}`);
|
|
28095
|
+
try {
|
|
28096
|
+
await connection.transport.disconnect();
|
|
28097
|
+
} catch (error) {
|
|
28098
|
+
this.logger.error(
|
|
28099
|
+
`Error disconnecting server '${name}': ${error instanceof Error ? error.message : String(error)}`
|
|
28100
|
+
);
|
|
28101
|
+
}
|
|
28102
|
+
this.connections.delete(name);
|
|
28103
|
+
}
|
|
28104
|
+
/**
|
|
28105
|
+
* Restart a server
|
|
28106
|
+
*/
|
|
28107
|
+
async restartServer(name) {
|
|
28108
|
+
const connection = this.connections.get(name);
|
|
28109
|
+
if (!connection) {
|
|
28110
|
+
throw new MCPConnectionError(`Server '${name}' not found`);
|
|
28111
|
+
}
|
|
28112
|
+
const config = connection.config;
|
|
28113
|
+
await this.stopServer(name);
|
|
28114
|
+
await new Promise((resolve3) => setTimeout(resolve3, 500));
|
|
28115
|
+
return this.startServer(config);
|
|
28116
|
+
}
|
|
28117
|
+
/**
|
|
28118
|
+
* Health check for a server
|
|
28119
|
+
*/
|
|
28120
|
+
async healthCheck(name) {
|
|
28121
|
+
const connection = this.connections.get(name);
|
|
28122
|
+
if (!connection) {
|
|
28123
|
+
return {
|
|
28124
|
+
name,
|
|
28125
|
+
healthy: false,
|
|
28126
|
+
toolCount: 0,
|
|
28127
|
+
latencyMs: 0,
|
|
28128
|
+
error: "Server not connected"
|
|
28129
|
+
};
|
|
28130
|
+
}
|
|
28131
|
+
const startTime = performance.now();
|
|
28132
|
+
try {
|
|
28133
|
+
const { tools } = await Promise.race([
|
|
28134
|
+
connection.client.listTools(),
|
|
28135
|
+
new Promise(
|
|
28136
|
+
(_, reject) => setTimeout(() => reject(new Error("Health check timeout")), 5e3)
|
|
28137
|
+
)
|
|
28138
|
+
]);
|
|
28139
|
+
const latencyMs = performance.now() - startTime;
|
|
28140
|
+
connection.healthy = true;
|
|
28141
|
+
connection.toolCount = tools.length;
|
|
28142
|
+
return {
|
|
28143
|
+
name,
|
|
28144
|
+
healthy: true,
|
|
28145
|
+
toolCount: tools.length,
|
|
28146
|
+
latencyMs
|
|
28147
|
+
};
|
|
28148
|
+
} catch (error) {
|
|
28149
|
+
const latencyMs = performance.now() - startTime;
|
|
28150
|
+
connection.healthy = false;
|
|
28151
|
+
return {
|
|
28152
|
+
name,
|
|
28153
|
+
healthy: false,
|
|
28154
|
+
toolCount: 0,
|
|
28155
|
+
latencyMs,
|
|
28156
|
+
error: error instanceof Error ? error.message : String(error)
|
|
28157
|
+
};
|
|
28158
|
+
}
|
|
28159
|
+
}
|
|
28160
|
+
/**
|
|
28161
|
+
* Start all servers from config list
|
|
28162
|
+
*/
|
|
28163
|
+
async startAll(configs) {
|
|
28164
|
+
const results = /* @__PURE__ */ new Map();
|
|
28165
|
+
for (const config of configs) {
|
|
28166
|
+
if (config.enabled === false) continue;
|
|
28167
|
+
try {
|
|
28168
|
+
const connection = await this.startServer(config);
|
|
28169
|
+
results.set(config.name, connection);
|
|
28170
|
+
} catch (error) {
|
|
28171
|
+
this.logger.error(
|
|
28172
|
+
`Failed to start server '${config.name}': ${error instanceof Error ? error.message : String(error)}`
|
|
28173
|
+
);
|
|
28174
|
+
}
|
|
28175
|
+
}
|
|
28176
|
+
return results;
|
|
28177
|
+
}
|
|
28178
|
+
/**
|
|
28179
|
+
* Stop all servers
|
|
28180
|
+
*/
|
|
28181
|
+
async stopAll() {
|
|
28182
|
+
const names = Array.from(this.connections.keys());
|
|
28183
|
+
for (const name of names) {
|
|
28184
|
+
await this.stopServer(name);
|
|
28185
|
+
}
|
|
28186
|
+
}
|
|
28187
|
+
/**
|
|
28188
|
+
* Get list of connected server names
|
|
28189
|
+
*/
|
|
28190
|
+
getConnectedServers() {
|
|
28191
|
+
return Array.from(this.connections.keys());
|
|
28192
|
+
}
|
|
28193
|
+
/**
|
|
28194
|
+
* Get a specific server connection
|
|
28195
|
+
*/
|
|
28196
|
+
getConnection(name) {
|
|
28197
|
+
return this.connections.get(name);
|
|
28198
|
+
}
|
|
28199
|
+
/**
|
|
28200
|
+
* Get all connections
|
|
28201
|
+
*/
|
|
28202
|
+
getAllConnections() {
|
|
28203
|
+
return Array.from(this.connections.values());
|
|
28204
|
+
}
|
|
28205
|
+
/**
|
|
28206
|
+
* Get the client for a server
|
|
28207
|
+
*/
|
|
28208
|
+
getClient(name) {
|
|
28209
|
+
return this.connections.get(name)?.client;
|
|
28210
|
+
}
|
|
28211
|
+
};
|
|
28212
|
+
var globalManager = null;
|
|
28213
|
+
function getMCPServerManager() {
|
|
28214
|
+
if (!globalManager) {
|
|
28215
|
+
globalManager = new MCPServerManager();
|
|
28216
|
+
}
|
|
28217
|
+
return globalManager;
|
|
28218
|
+
}
|
|
28219
|
+
|
|
28220
|
+
// src/tools/mcp.ts
|
|
28221
|
+
var mcpListServersTool = defineTool({
|
|
28222
|
+
name: "mcp_list_servers",
|
|
28223
|
+
description: `Inspect Coco's MCP configuration and current runtime connections.
|
|
28224
|
+
|
|
28225
|
+
Use this instead of bash_exec with "coco mcp ..." and instead of manually reading ~/.coco/mcp.json
|
|
28226
|
+
when you need to know which MCP servers are configured, connected, healthy, or which tools they expose.`,
|
|
28227
|
+
category: "config",
|
|
28228
|
+
parameters: z.object({
|
|
28229
|
+
includeDisabled: z.boolean().optional().default(false).describe("Include disabled MCP servers in the result"),
|
|
28230
|
+
includeTools: z.boolean().optional().default(false).describe("Include the list of exposed tool names for connected servers"),
|
|
28231
|
+
projectPath: z.string().optional().describe("Project path whose .mcp.json should be merged. Defaults to process.cwd()")
|
|
28232
|
+
}),
|
|
28233
|
+
async execute({ includeDisabled, includeTools, projectPath }) {
|
|
28234
|
+
const registry = new MCPRegistryImpl();
|
|
28235
|
+
await registry.load();
|
|
28236
|
+
const resolvedProjectPath = projectPath || process.cwd();
|
|
28237
|
+
const configuredServers = mergeMCPConfigs(
|
|
28238
|
+
registry.listServers(),
|
|
28239
|
+
await loadMCPServersFromCOCOConfig(),
|
|
28240
|
+
await loadProjectMCPFile(resolvedProjectPath)
|
|
28241
|
+
).filter((server) => includeDisabled || server.enabled !== false);
|
|
28242
|
+
const manager = getMCPServerManager();
|
|
28243
|
+
const servers = [];
|
|
28244
|
+
for (const server of configuredServers) {
|
|
28245
|
+
const connection = manager.getConnection(server.name);
|
|
28246
|
+
let tools;
|
|
28247
|
+
if (includeTools && connection) {
|
|
28248
|
+
try {
|
|
28249
|
+
const listed = await connection.client.listTools();
|
|
28250
|
+
tools = listed.tools.map((tool) => tool.name);
|
|
28251
|
+
} catch {
|
|
28252
|
+
tools = [];
|
|
28253
|
+
}
|
|
28254
|
+
}
|
|
28255
|
+
servers.push({
|
|
28256
|
+
name: server.name,
|
|
28257
|
+
transport: server.transport,
|
|
28258
|
+
enabled: server.enabled !== false,
|
|
28259
|
+
connected: connection !== void 0,
|
|
28260
|
+
healthy: connection?.healthy ?? false,
|
|
28261
|
+
toolCount: connection?.toolCount ?? 0,
|
|
28262
|
+
...includeTools ? { tools: tools ?? [] } : {}
|
|
28263
|
+
});
|
|
28264
|
+
}
|
|
28265
|
+
return {
|
|
28266
|
+
configuredCount: servers.length,
|
|
28267
|
+
connectedCount: servers.filter((server) => server.connected).length,
|
|
28268
|
+
servers
|
|
28269
|
+
};
|
|
28270
|
+
}
|
|
28271
|
+
});
|
|
28272
|
+
var mcpTools = [mcpListServersTool];
|
|
25785
28273
|
init_allowed_paths();
|
|
25786
28274
|
var BLOCKED_SYSTEM_PATHS = [
|
|
25787
28275
|
"/etc",
|
|
@@ -25915,6 +28403,7 @@ function registerAllTools(registry) {
|
|
|
25915
28403
|
...gitEnhancedTools,
|
|
25916
28404
|
...githubTools,
|
|
25917
28405
|
...openTools,
|
|
28406
|
+
...mcpTools,
|
|
25918
28407
|
...authorizePathTools
|
|
25919
28408
|
];
|
|
25920
28409
|
for (const tool of allTools) {
|
|
@@ -25929,6 +28418,7 @@ function createFullToolRegistry() {
|
|
|
25929
28418
|
|
|
25930
28419
|
// src/index.ts
|
|
25931
28420
|
init_errors();
|
|
28421
|
+
init_logger();
|
|
25932
28422
|
|
|
25933
28423
|
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
28424
|
//# sourceMappingURL=index.js.map
|