wave-agent-sdk 0.16.6 → 0.16.7
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/managers/mcpManager.d.ts +13 -0
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +111 -3
- package/dist/services/authService.d.ts +7 -0
- package/dist/services/authService.d.ts.map +1 -1
- package/dist/services/authService.js +25 -0
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +8 -0
- package/package.json +1 -1
- package/src/managers/mcpManager.ts +141 -3
- package/src/services/authService.ts +28 -0
- package/src/utils/containerSetup.ts +9 -0
|
@@ -46,6 +46,8 @@ export declare class McpManager {
|
|
|
46
46
|
private callbacks;
|
|
47
47
|
private mcpServers;
|
|
48
48
|
private resolverCtx;
|
|
49
|
+
private reconnectTimers;
|
|
50
|
+
private reconnectAttempts;
|
|
49
51
|
constructor(container: Container, options?: McpManagerOptions);
|
|
50
52
|
/**
|
|
51
53
|
* Initialize MCP manager with working directory and optionally auto-connect
|
|
@@ -61,6 +63,12 @@ export declare class McpManager {
|
|
|
61
63
|
addServer(name: string, config: McpServerConfig): boolean;
|
|
62
64
|
removeServer(name: string): boolean;
|
|
63
65
|
connectServer(name: string): Promise<boolean>;
|
|
66
|
+
/**
|
|
67
|
+
* Schedule auto-reconnect with exponential backoff.
|
|
68
|
+
* Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...
|
|
69
|
+
*/
|
|
70
|
+
private scheduleReconnect;
|
|
71
|
+
private cancelReconnect;
|
|
64
72
|
disconnectServer(name: string): Promise<boolean>;
|
|
65
73
|
getAllConnectedTools(): McpTool[];
|
|
66
74
|
executeMcpTool(toolName: string, args: Record<string, unknown>, context?: ToolContext): Promise<{
|
|
@@ -74,6 +82,11 @@ export declare class McpManager {
|
|
|
74
82
|
}>;
|
|
75
83
|
private executeToolOnConnection;
|
|
76
84
|
cleanup(): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Update credentials and reconnect MCP servers that use template variables.
|
|
87
|
+
* Called after SSO login to refresh ${WAVE_SSO_TOKEN} and ${WAVE_SERVER_URL}.
|
|
88
|
+
*/
|
|
89
|
+
refreshCredentials(serverUrl?: string, ssoToken?: string): Promise<void>;
|
|
77
90
|
/**
|
|
78
91
|
* Get all currently available MCP tools as plugins
|
|
79
92
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CAC3D;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUnD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,kBAAkB,GACtB,eAAe,CA2CjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,GAAG,CAAC,EAAE,kBAAkB,GACvB,SAAS,CA4CX;AAED,qBAAa,UAAU;
|
|
1
|
+
{"version":3,"file":"mcpManager.d.ts","sourceRoot":"","sources":["../../src/managers/mcpManager.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,0BAA0B,EAAE,MAAM,qBAAqB,CAAC;AAEjE,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,KAAK,EACV,MAAM,EACN,eAAe,EACf,SAAS,EACT,OAAO,EACP,eAAe,EAChB,MAAM,mBAAmB,CAAC;AAQ3B,MAAM,WAAW,mBAAmB;IAClC,kBAAkB,CAAC,EAAE,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CAC3D;AAID,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,mBAAmB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC7C,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUnD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,kBAAkB,GACtB,eAAe,CA2CjB;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,SAAS,EACjB,GAAG,CAAC,EAAE,kBAAkB,GACvB,SAAS,CA4CX;AAED,qBAAa,UAAU;IAcnB,OAAO,CAAC,SAAS;IAbnB,OAAO,CAAC,MAAM,CAA0B;IACxC,OAAO,CAAC,OAAO,CAA2C;IAC1D,OAAO,CAAC,WAAW,CAAyC;IAC5D,OAAO,CAAC,UAAU,CAAc;IAChC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,UAAU,CAA8C;IAEhE,OAAO,CAAC,WAAW,CAAiC;IACpD,OAAO,CAAC,eAAe,CAA0C;IACjE,OAAO,CAAC,iBAAiB,CAAkC;gBAGjD,SAAS,EAAE,SAAS,EAC5B,OAAO,GAAE,iBAAsB;IAUjC;;OAEG;IACG,UAAU,CACd,OAAO,EAAE,MAAM,EACf,WAAW,GAAE,OAAe,GAC3B,OAAO,CAAC,IAAI,CAAC;IAgDV,kBAAkB,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAO/C,UAAU,IAAI,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA0DvC,UAAU,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC;IAWrD,SAAS,IAAI,SAAS,GAAG,IAAI;IAI7B,aAAa,IAAI,eAAe,EAAE;IAIlC,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAIpD,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI;IASzE,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,OAAO;IA+BzD,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAgB7B,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAwMnD;;;OAGG;IACH,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,eAAe;IASjB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IA8BtD,oBAAoB,IAAI,OAAO,EAAE;IAW3B,cAAc,CAClB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,MAAM,CAAC,EAAE,KAAK,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KACtD,CAAC;YAsDY,uBAAuB;IA8D/B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAW9B;;;OAGG;IACG,kBAAkB,CACtB,SAAS,CAAC,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC;IAuEhB;;OAEG;IACH,iBAAiB,IAAI,UAAU,EAAE;IA6BjC;;OAEG;IACH,iBAAiB,IAAI,0BAA0B,EAAE;IAIjD;;OAEG;IACG,wBAAwB,CAC5B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC;IActB;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;CAcjC"}
|
|
@@ -114,6 +114,8 @@ export class McpManager {
|
|
|
114
114
|
this.connections = new Map();
|
|
115
115
|
this.configPath = "";
|
|
116
116
|
this.workdir = "";
|
|
117
|
+
this.reconnectTimers = new Map();
|
|
118
|
+
this.reconnectAttempts = new Map();
|
|
117
119
|
this.callbacks = options.callbacks || {};
|
|
118
120
|
this.mcpServers = options.mcpServers;
|
|
119
121
|
this.resolverCtx = {
|
|
@@ -250,19 +252,21 @@ export class McpManager {
|
|
|
250
252
|
if (this.servers.has(name)) {
|
|
251
253
|
return false;
|
|
252
254
|
}
|
|
255
|
+
// Resolve template variables before storing
|
|
256
|
+
const resolvedConfig = resolveMcpTemplates(config, this.resolverCtx ?? { serverUrl: undefined, ssoToken: undefined });
|
|
253
257
|
const newServer = {
|
|
254
258
|
name,
|
|
255
|
-
config,
|
|
259
|
+
config: resolvedConfig,
|
|
256
260
|
status: "disconnected",
|
|
257
261
|
};
|
|
258
262
|
this.servers.set(name, newServer);
|
|
259
263
|
// Update config
|
|
260
264
|
if (this.config) {
|
|
261
|
-
this.config.mcpServers[name] =
|
|
265
|
+
this.config.mcpServers[name] = resolvedConfig;
|
|
262
266
|
}
|
|
263
267
|
else {
|
|
264
268
|
this.config = {
|
|
265
|
-
mcpServers: { [name]:
|
|
269
|
+
mcpServers: { [name]: resolvedConfig },
|
|
266
270
|
};
|
|
267
271
|
}
|
|
268
272
|
return true;
|
|
@@ -413,6 +417,10 @@ export class McpManager {
|
|
|
413
417
|
tools: [],
|
|
414
418
|
toolCount: 0,
|
|
415
419
|
});
|
|
420
|
+
// Auto-reconnect with exponential backoff (not triggered by explicit disconnect)
|
|
421
|
+
if (!this.reconnectTimers.has(name)) {
|
|
422
|
+
this.scheduleReconnect(name);
|
|
423
|
+
}
|
|
416
424
|
};
|
|
417
425
|
// Store connection
|
|
418
426
|
this.connections.set(name, {
|
|
@@ -441,7 +449,46 @@ export class McpManager {
|
|
|
441
449
|
return false;
|
|
442
450
|
}
|
|
443
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Schedule auto-reconnect with exponential backoff.
|
|
454
|
+
* Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...
|
|
455
|
+
*/
|
|
456
|
+
scheduleReconnect(name) {
|
|
457
|
+
const attempts = this.reconnectAttempts.get(name) ?? 0;
|
|
458
|
+
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
|
|
459
|
+
this.reconnectAttempts.set(name, attempts + 1);
|
|
460
|
+
logger?.info(`Scheduling MCP server ${name} reconnect in ${delay}ms (attempt ${attempts + 1})`);
|
|
461
|
+
const timer = setTimeout(() => {
|
|
462
|
+
this.reconnectTimers.delete(name);
|
|
463
|
+
logger?.debug(`Auto-reconnecting MCP server: ${name}`);
|
|
464
|
+
this.connectServer(name)
|
|
465
|
+
.then((success) => {
|
|
466
|
+
if (success) {
|
|
467
|
+
logger?.info(`Auto-reconnected MCP server: ${name}`);
|
|
468
|
+
this.reconnectAttempts.delete(name);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
logger?.warn(`Auto-reconnect failed for MCP server: ${name}`);
|
|
472
|
+
// Will be retried via onclose handler
|
|
473
|
+
}
|
|
474
|
+
})
|
|
475
|
+
.catch((error) => {
|
|
476
|
+
logger?.error(`Auto-reconnect error for MCP server ${name}:`, error);
|
|
477
|
+
});
|
|
478
|
+
}, delay);
|
|
479
|
+
this.reconnectTimers.set(name, timer);
|
|
480
|
+
}
|
|
481
|
+
cancelReconnect(name) {
|
|
482
|
+
const timer = this.reconnectTimers.get(name);
|
|
483
|
+
if (timer) {
|
|
484
|
+
clearTimeout(timer);
|
|
485
|
+
this.reconnectTimers.delete(name);
|
|
486
|
+
}
|
|
487
|
+
this.reconnectAttempts.delete(name);
|
|
488
|
+
}
|
|
444
489
|
async disconnectServer(name) {
|
|
490
|
+
// Cancel any pending reconnect attempts
|
|
491
|
+
this.cancelReconnect(name);
|
|
445
492
|
const connection = this.connections.get(name);
|
|
446
493
|
if (!connection)
|
|
447
494
|
return false;
|
|
@@ -550,9 +597,70 @@ export class McpManager {
|
|
|
550
597
|
}
|
|
551
598
|
// Cleanup all connections
|
|
552
599
|
async cleanup() {
|
|
600
|
+
// Cancel all pending reconnect timers
|
|
601
|
+
for (const name of this.reconnectTimers.keys()) {
|
|
602
|
+
this.cancelReconnect(name);
|
|
603
|
+
}
|
|
553
604
|
const disconnectPromises = Array.from(this.connections.keys()).map((name) => this.disconnectServer(name));
|
|
554
605
|
await Promise.all(disconnectPromises);
|
|
555
606
|
}
|
|
607
|
+
/**
|
|
608
|
+
* Update credentials and reconnect MCP servers that use template variables.
|
|
609
|
+
* Called after SSO login to refresh ${WAVE_SSO_TOKEN} and ${WAVE_SERVER_URL}.
|
|
610
|
+
*/
|
|
611
|
+
async refreshCredentials(serverUrl, ssoToken) {
|
|
612
|
+
// Update resolver context
|
|
613
|
+
this.resolverCtx = {
|
|
614
|
+
serverUrl: serverUrl ?? this.resolverCtx?.serverUrl,
|
|
615
|
+
ssoToken: ssoToken ?? this.resolverCtx?.ssoToken,
|
|
616
|
+
};
|
|
617
|
+
logger?.info(`MCP refreshCredentials: serverUrl=${serverUrl}, hasToken=${!!ssoToken}`);
|
|
618
|
+
// Collect servers that need reconnection
|
|
619
|
+
const serversToReconnect = [];
|
|
620
|
+
for (const [name, server] of this.servers) {
|
|
621
|
+
// Re-resolve config with new credentials
|
|
622
|
+
const originalConfig = server.config;
|
|
623
|
+
const resolvedConfig = resolveMcpTemplates(originalConfig, this.resolverCtx);
|
|
624
|
+
// Update the stored config
|
|
625
|
+
this.servers.set(name, {
|
|
626
|
+
...server,
|
|
627
|
+
config: resolvedConfig,
|
|
628
|
+
});
|
|
629
|
+
if (this.config && this.config.mcpServers[name]) {
|
|
630
|
+
this.config.mcpServers[name] = resolvedConfig;
|
|
631
|
+
}
|
|
632
|
+
// Determine if reconnection is needed
|
|
633
|
+
const wasConnected = this.connections.has(name);
|
|
634
|
+
const wasDisconnected = server.status === "disconnected" || server.status === "error";
|
|
635
|
+
if (wasConnected) {
|
|
636
|
+
// Disconnect first, then reconnect with new resolved config
|
|
637
|
+
await this.disconnectServer(name);
|
|
638
|
+
serversToReconnect.push(name);
|
|
639
|
+
}
|
|
640
|
+
else if (wasDisconnected) {
|
|
641
|
+
// Was disconnected or errored — try to reconnect now that we have credentials
|
|
642
|
+
serversToReconnect.push(name);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// Reconnect servers
|
|
646
|
+
for (const name of serversToReconnect) {
|
|
647
|
+
logger?.debug(`Reconnecting MCP server after credential refresh: ${name}`);
|
|
648
|
+
this.connectServer(name)
|
|
649
|
+
.then((success) => {
|
|
650
|
+
if (success) {
|
|
651
|
+
logger?.info(`Successfully reconnected MCP server: ${name}`);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
logger?.warn(`Failed to reconnect MCP server: ${name}`);
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
.catch((error) => {
|
|
658
|
+
logger?.error(`Reconnection to MCP server ${name} failed:`, error);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
// Trigger state change callback
|
|
662
|
+
this.callbacks.onMcpServersChange?.(this.getAllServers());
|
|
663
|
+
}
|
|
556
664
|
// ========== Tools Registry Methods ==========
|
|
557
665
|
/**
|
|
558
666
|
* Get all currently available MCP tools as plugins
|
|
@@ -8,12 +8,19 @@ import type { AuthConfig, AuthUser } from "../types/auth.js";
|
|
|
8
8
|
export declare class AuthService {
|
|
9
9
|
private static instance;
|
|
10
10
|
private _serverUrl;
|
|
11
|
+
private onAuthChangeCallbacks;
|
|
11
12
|
static getInstance(): AuthService;
|
|
12
13
|
/**
|
|
13
14
|
* Set server URL programmatically (e.g. from AgentOptions.serverUrl).
|
|
14
15
|
* Takes priority over WAVE_SERVER_URL environment variable.
|
|
15
16
|
*/
|
|
16
17
|
setServerUrl(url: string): void;
|
|
18
|
+
/**
|
|
19
|
+
* Register a callback for auth state changes.
|
|
20
|
+
* Returns an unsubscribe function.
|
|
21
|
+
*/
|
|
22
|
+
onAuthChange(callback: (event: "login" | "logout") => void): () => void;
|
|
23
|
+
private notifyAuthChange;
|
|
17
24
|
getAuthPath(): string;
|
|
18
25
|
loadAuth(): AuthConfig;
|
|
19
26
|
saveAuth(config: AuthConfig): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authService.d.ts","sourceRoot":"","sources":["../../src/services/authService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI7D,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAc;IACrC,OAAO,CAAC,UAAU,CAAqB;
|
|
1
|
+
{"version":3,"file":"authService.d.ts","sourceRoot":"","sources":["../../src/services/authService.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgBH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAI7D,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAc;IACrC,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,qBAAqB,CACxB;IAEL,MAAM,CAAC,WAAW,IAAI,WAAW;IAOjC;;;OAGG;IACH,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAI/B;;;OAGG;IACH,YAAY,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,KAAK,IAAI,GAAG,MAAM,IAAI;IASvE,OAAO,CAAC,gBAAgB;IAUxB,WAAW,IAAI,MAAM;IAKrB,QAAQ,IAAI,UAAU;IAatB,QAAQ,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAUlC,SAAS,IAAI,IAAI;IAcjB,WAAW,IAAI,MAAM,GAAG,SAAS;IAKjC,YAAY,IAAI,MAAM;IAUhB,KAAK,CAAC,OAAO,CAAC,EAAE;QACpB,6DAA6D;QAC7D,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAClC,mGAAmG;QACnG,SAAS,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;QAClC,oFAAoF;QACpF,SAAS,CAAC,EAAE,MAAM,CAAC;KACpB,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBnB;;;OAGG;YACW,YAAY;IA0B1B,OAAO,CAAC,oBAAoB;YAoGd,WAAW;IAmBzB,kBAAkB,IAAI,OAAO;IAI7B,WAAW,IAAI,QAAQ,GAAG,SAAS;CAIpC;AAED,eAAO,MAAM,WAAW,aAA4B,CAAC"}
|
|
@@ -13,6 +13,9 @@ import { execFile } from "child_process";
|
|
|
13
13
|
import { promisify } from "util";
|
|
14
14
|
const execFileAsync = promisify(execFile);
|
|
15
15
|
export class AuthService {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.onAuthChangeCallbacks = [];
|
|
18
|
+
}
|
|
16
19
|
static getInstance() {
|
|
17
20
|
if (!AuthService.instance) {
|
|
18
21
|
AuthService.instance = new AuthService();
|
|
@@ -26,6 +29,26 @@ export class AuthService {
|
|
|
26
29
|
setServerUrl(url) {
|
|
27
30
|
this._serverUrl = url;
|
|
28
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Register a callback for auth state changes.
|
|
34
|
+
* Returns an unsubscribe function.
|
|
35
|
+
*/
|
|
36
|
+
onAuthChange(callback) {
|
|
37
|
+
this.onAuthChangeCallbacks.push(callback);
|
|
38
|
+
return () => {
|
|
39
|
+
this.onAuthChangeCallbacks = this.onAuthChangeCallbacks.filter((cb) => cb !== callback);
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
notifyAuthChange(event) {
|
|
43
|
+
for (const cb of this.onAuthChangeCallbacks) {
|
|
44
|
+
try {
|
|
45
|
+
cb(event);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Don't let callback errors break auth flow
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
29
52
|
getAuthPath() {
|
|
30
53
|
const homeDir = os.homedir();
|
|
31
54
|
return path.join(homeDir, ".wave", "auth.json");
|
|
@@ -64,6 +87,7 @@ export class AuthService {
|
|
|
64
87
|
else {
|
|
65
88
|
this.saveAuth(config);
|
|
66
89
|
}
|
|
90
|
+
this.notifyAuthChange("logout");
|
|
67
91
|
}
|
|
68
92
|
getSSOToken() {
|
|
69
93
|
const config = this.loadAuth();
|
|
@@ -88,6 +112,7 @@ export class AuthService {
|
|
|
88
112
|
// Save the token and user info (preserve existing keys)
|
|
89
113
|
const existing = this.loadAuth();
|
|
90
114
|
this.saveAuth({ ...existing, SSO_TOKEN: token, user });
|
|
115
|
+
this.notifyAuthChange("login");
|
|
91
116
|
return token;
|
|
92
117
|
}
|
|
93
118
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"containerSetup.d.ts","sourceRoot":"","sources":["../../src/utils/containerSetup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAwB3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAM3E,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EACV,cAAc,EACd,KAAK,EACL,IAAI,EACJ,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAK3B,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAGhB,uBAAuB,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC3D,aAAa,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACvC,sBAAsB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACvD,wBAAwB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACzD,iBAAiB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAClD,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,0BAA0B,GACvC,SAAS,
|
|
1
|
+
{"version":3,"file":"containerSetup.d.ts","sourceRoot":"","sources":["../../src/utils/containerSetup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAwB3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,qCAAqC,CAAC;AAM3E,OAAO,KAAK,EAAE,YAAY,EAAmB,MAAM,mBAAmB,CAAC;AACvE,OAAO,KAAK,EACV,cAAc,EACd,KAAK,EACL,IAAI,EACJ,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAK3B,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,OAAO,CAAC;IAGhB,uBAAuB,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,KAAK,IAAI,CAAC;IAC3D,aAAa,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;IACvC,sBAAsB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACvD,wBAAwB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IACzD,iBAAiB,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAC;IAClD,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACnD,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAED,wBAAgB,mBAAmB,CACjC,YAAY,EAAE,0BAA0B,GACvC,SAAS,CA6RX"}
|
|
@@ -101,6 +101,14 @@ export function setupAgentContainer(setupOptions) {
|
|
|
101
101
|
ssoToken,
|
|
102
102
|
});
|
|
103
103
|
container.register("McpManager", mcpManager);
|
|
104
|
+
// Wire up auth change callback to reconnect MCP servers after SSO login
|
|
105
|
+
authService.onAuthChange((event) => {
|
|
106
|
+
if (event === "login") {
|
|
107
|
+
const newServerUrl = options.serverUrl || process.env.WAVE_SERVER_URL;
|
|
108
|
+
const newToken = authService.getSSOToken();
|
|
109
|
+
mcpManager.refreshCredentials(newServerUrl, newToken);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
104
112
|
const lspManager = options.lspManager || new LspManager(container);
|
|
105
113
|
container.register("LspManager", lspManager);
|
|
106
114
|
const permissionManager = new PermissionManager(container, {
|
package/package.json
CHANGED
|
@@ -184,6 +184,8 @@ export class McpManager {
|
|
|
184
184
|
private mcpServers: Record<string, McpServerConfig> | undefined;
|
|
185
185
|
|
|
186
186
|
private resolverCtx: McpResolverContext | undefined;
|
|
187
|
+
private reconnectTimers: Map<string, NodeJS.Timeout> = new Map();
|
|
188
|
+
private reconnectAttempts: Map<string, number> = new Map();
|
|
187
189
|
|
|
188
190
|
constructor(
|
|
189
191
|
private container: Container,
|
|
@@ -353,9 +355,15 @@ export class McpManager {
|
|
|
353
355
|
return false;
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
// Resolve template variables before storing
|
|
359
|
+
const resolvedConfig = resolveMcpTemplates(
|
|
360
|
+
config,
|
|
361
|
+
this.resolverCtx ?? { serverUrl: undefined, ssoToken: undefined },
|
|
362
|
+
);
|
|
363
|
+
|
|
356
364
|
const newServer: McpServerStatus = {
|
|
357
365
|
name,
|
|
358
|
-
config,
|
|
366
|
+
config: resolvedConfig,
|
|
359
367
|
status: "disconnected",
|
|
360
368
|
};
|
|
361
369
|
|
|
@@ -363,10 +371,10 @@ export class McpManager {
|
|
|
363
371
|
|
|
364
372
|
// Update config
|
|
365
373
|
if (this.config) {
|
|
366
|
-
this.config.mcpServers[name] =
|
|
374
|
+
this.config.mcpServers[name] = resolvedConfig;
|
|
367
375
|
} else {
|
|
368
376
|
this.config = {
|
|
369
|
-
mcpServers: { [name]:
|
|
377
|
+
mcpServers: { [name]: resolvedConfig },
|
|
370
378
|
};
|
|
371
379
|
}
|
|
372
380
|
|
|
@@ -554,6 +562,10 @@ export class McpManager {
|
|
|
554
562
|
tools: [],
|
|
555
563
|
toolCount: 0,
|
|
556
564
|
});
|
|
565
|
+
// Auto-reconnect with exponential backoff (not triggered by explicit disconnect)
|
|
566
|
+
if (!this.reconnectTimers.has(name)) {
|
|
567
|
+
this.scheduleReconnect(name);
|
|
568
|
+
}
|
|
557
569
|
};
|
|
558
570
|
|
|
559
571
|
// Store connection
|
|
@@ -585,7 +597,53 @@ export class McpManager {
|
|
|
585
597
|
}
|
|
586
598
|
}
|
|
587
599
|
|
|
600
|
+
/**
|
|
601
|
+
* Schedule auto-reconnect with exponential backoff.
|
|
602
|
+
* Delays: 1s, 2s, 4s, 8s, 16s, 30s, 30s, ...
|
|
603
|
+
*/
|
|
604
|
+
private scheduleReconnect(name: string): void {
|
|
605
|
+
const attempts = this.reconnectAttempts.get(name) ?? 0;
|
|
606
|
+
const delay = Math.min(1000 * Math.pow(2, attempts), 30000);
|
|
607
|
+
this.reconnectAttempts.set(name, attempts + 1);
|
|
608
|
+
|
|
609
|
+
logger?.info(
|
|
610
|
+
`Scheduling MCP server ${name} reconnect in ${delay}ms (attempt ${attempts + 1})`,
|
|
611
|
+
);
|
|
612
|
+
|
|
613
|
+
const timer = setTimeout(() => {
|
|
614
|
+
this.reconnectTimers.delete(name);
|
|
615
|
+
logger?.debug(`Auto-reconnecting MCP server: ${name}`);
|
|
616
|
+
this.connectServer(name)
|
|
617
|
+
.then((success) => {
|
|
618
|
+
if (success) {
|
|
619
|
+
logger?.info(`Auto-reconnected MCP server: ${name}`);
|
|
620
|
+
this.reconnectAttempts.delete(name);
|
|
621
|
+
} else {
|
|
622
|
+
logger?.warn(`Auto-reconnect failed for MCP server: ${name}`);
|
|
623
|
+
// Will be retried via onclose handler
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
.catch((error) => {
|
|
627
|
+
logger?.error(`Auto-reconnect error for MCP server ${name}:`, error);
|
|
628
|
+
});
|
|
629
|
+
}, delay);
|
|
630
|
+
|
|
631
|
+
this.reconnectTimers.set(name, timer);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private cancelReconnect(name: string): void {
|
|
635
|
+
const timer = this.reconnectTimers.get(name);
|
|
636
|
+
if (timer) {
|
|
637
|
+
clearTimeout(timer);
|
|
638
|
+
this.reconnectTimers.delete(name);
|
|
639
|
+
}
|
|
640
|
+
this.reconnectAttempts.delete(name);
|
|
641
|
+
}
|
|
642
|
+
|
|
588
643
|
async disconnectServer(name: string): Promise<boolean> {
|
|
644
|
+
// Cancel any pending reconnect attempts
|
|
645
|
+
this.cancelReconnect(name);
|
|
646
|
+
|
|
589
647
|
const connection = this.connections.get(name);
|
|
590
648
|
if (!connection) return false;
|
|
591
649
|
|
|
@@ -749,12 +807,92 @@ export class McpManager {
|
|
|
749
807
|
|
|
750
808
|
// Cleanup all connections
|
|
751
809
|
async cleanup(): Promise<void> {
|
|
810
|
+
// Cancel all pending reconnect timers
|
|
811
|
+
for (const name of this.reconnectTimers.keys()) {
|
|
812
|
+
this.cancelReconnect(name);
|
|
813
|
+
}
|
|
752
814
|
const disconnectPromises = Array.from(this.connections.keys()).map((name) =>
|
|
753
815
|
this.disconnectServer(name),
|
|
754
816
|
);
|
|
755
817
|
await Promise.all(disconnectPromises);
|
|
756
818
|
}
|
|
757
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Update credentials and reconnect MCP servers that use template variables.
|
|
822
|
+
* Called after SSO login to refresh ${WAVE_SSO_TOKEN} and ${WAVE_SERVER_URL}.
|
|
823
|
+
*/
|
|
824
|
+
async refreshCredentials(
|
|
825
|
+
serverUrl?: string,
|
|
826
|
+
ssoToken?: string,
|
|
827
|
+
): Promise<void> {
|
|
828
|
+
// Update resolver context
|
|
829
|
+
this.resolverCtx = {
|
|
830
|
+
serverUrl: serverUrl ?? this.resolverCtx?.serverUrl,
|
|
831
|
+
ssoToken: ssoToken ?? this.resolverCtx?.ssoToken,
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
logger?.info(
|
|
835
|
+
`MCP refreshCredentials: serverUrl=${serverUrl}, hasToken=${!!ssoToken}`,
|
|
836
|
+
);
|
|
837
|
+
|
|
838
|
+
// Collect servers that need reconnection
|
|
839
|
+
const serversToReconnect: string[] = [];
|
|
840
|
+
|
|
841
|
+
for (const [name, server] of this.servers) {
|
|
842
|
+
// Re-resolve config with new credentials
|
|
843
|
+
const originalConfig = server.config;
|
|
844
|
+
const resolvedConfig = resolveMcpTemplates(
|
|
845
|
+
originalConfig,
|
|
846
|
+
this.resolverCtx,
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
// Update the stored config
|
|
850
|
+
this.servers.set(name, {
|
|
851
|
+
...server,
|
|
852
|
+
config: resolvedConfig,
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
if (this.config && this.config.mcpServers[name]) {
|
|
856
|
+
this.config.mcpServers[name] = resolvedConfig;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// Determine if reconnection is needed
|
|
860
|
+
const wasConnected = this.connections.has(name);
|
|
861
|
+
const wasDisconnected =
|
|
862
|
+
server.status === "disconnected" || server.status === "error";
|
|
863
|
+
|
|
864
|
+
if (wasConnected) {
|
|
865
|
+
// Disconnect first, then reconnect with new resolved config
|
|
866
|
+
await this.disconnectServer(name);
|
|
867
|
+
serversToReconnect.push(name);
|
|
868
|
+
} else if (wasDisconnected) {
|
|
869
|
+
// Was disconnected or errored — try to reconnect now that we have credentials
|
|
870
|
+
serversToReconnect.push(name);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Reconnect servers
|
|
875
|
+
for (const name of serversToReconnect) {
|
|
876
|
+
logger?.debug(
|
|
877
|
+
`Reconnecting MCP server after credential refresh: ${name}`,
|
|
878
|
+
);
|
|
879
|
+
this.connectServer(name)
|
|
880
|
+
.then((success) => {
|
|
881
|
+
if (success) {
|
|
882
|
+
logger?.info(`Successfully reconnected MCP server: ${name}`);
|
|
883
|
+
} else {
|
|
884
|
+
logger?.warn(`Failed to reconnect MCP server: ${name}`);
|
|
885
|
+
}
|
|
886
|
+
})
|
|
887
|
+
.catch((error) => {
|
|
888
|
+
logger?.error(`Reconnection to MCP server ${name} failed:`, error);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// Trigger state change callback
|
|
893
|
+
this.callbacks.onMcpServersChange?.(this.getAllServers());
|
|
894
|
+
}
|
|
895
|
+
|
|
758
896
|
// ========== Tools Registry Methods ==========
|
|
759
897
|
|
|
760
898
|
/**
|
|
@@ -26,6 +26,8 @@ const execFileAsync = promisify(execFile);
|
|
|
26
26
|
export class AuthService {
|
|
27
27
|
private static instance: AuthService;
|
|
28
28
|
private _serverUrl: string | undefined;
|
|
29
|
+
private onAuthChangeCallbacks: Array<(event: "login" | "logout") => void> =
|
|
30
|
+
[];
|
|
29
31
|
|
|
30
32
|
static getInstance(): AuthService {
|
|
31
33
|
if (!AuthService.instance) {
|
|
@@ -42,6 +44,29 @@ export class AuthService {
|
|
|
42
44
|
this._serverUrl = url;
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Register a callback for auth state changes.
|
|
49
|
+
* Returns an unsubscribe function.
|
|
50
|
+
*/
|
|
51
|
+
onAuthChange(callback: (event: "login" | "logout") => void): () => void {
|
|
52
|
+
this.onAuthChangeCallbacks.push(callback);
|
|
53
|
+
return () => {
|
|
54
|
+
this.onAuthChangeCallbacks = this.onAuthChangeCallbacks.filter(
|
|
55
|
+
(cb) => cb !== callback,
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private notifyAuthChange(event: "login" | "logout"): void {
|
|
61
|
+
for (const cb of this.onAuthChangeCallbacks) {
|
|
62
|
+
try {
|
|
63
|
+
cb(event);
|
|
64
|
+
} catch {
|
|
65
|
+
// Don't let callback errors break auth flow
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
45
70
|
getAuthPath(): string {
|
|
46
71
|
const homeDir = os.homedir();
|
|
47
72
|
return path.join(homeDir, ".wave", "auth.json");
|
|
@@ -81,6 +106,7 @@ export class AuthService {
|
|
|
81
106
|
} else {
|
|
82
107
|
this.saveAuth(config);
|
|
83
108
|
}
|
|
109
|
+
this.notifyAuthChange("logout");
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
getSSOToken(): string | undefined {
|
|
@@ -121,6 +147,8 @@ export class AuthService {
|
|
|
121
147
|
const existing = this.loadAuth();
|
|
122
148
|
this.saveAuth({ ...existing, SSO_TOKEN: token, user });
|
|
123
149
|
|
|
150
|
+
this.notifyAuthChange("login");
|
|
151
|
+
|
|
124
152
|
return token;
|
|
125
153
|
}
|
|
126
154
|
|
|
@@ -161,6 +161,15 @@ export function setupAgentContainer(
|
|
|
161
161
|
});
|
|
162
162
|
container.register("McpManager", mcpManager);
|
|
163
163
|
|
|
164
|
+
// Wire up auth change callback to reconnect MCP servers after SSO login
|
|
165
|
+
authService.onAuthChange((event) => {
|
|
166
|
+
if (event === "login") {
|
|
167
|
+
const newServerUrl = options.serverUrl || process.env.WAVE_SERVER_URL;
|
|
168
|
+
const newToken = authService.getSSOToken();
|
|
169
|
+
mcpManager.refreshCredentials(newServerUrl, newToken);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
164
173
|
const lspManager = options.lspManager || new LspManager(container);
|
|
165
174
|
container.register("LspManager", lspManager);
|
|
166
175
|
|