@mcp-ts/sdk 2.5.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -574
- package/dist/adapters/agui-adapter.d.mts +4 -4
- package/dist/adapters/agui-adapter.d.ts +4 -4
- package/dist/adapters/agui-middleware.d.mts +4 -4
- package/dist/adapters/agui-middleware.d.ts +4 -4
- package/dist/adapters/ai-adapter.d.mts +4 -4
- package/dist/adapters/ai-adapter.d.ts +4 -4
- package/dist/adapters/langchain-adapter.d.mts +4 -4
- package/dist/adapters/langchain-adapter.d.ts +4 -4
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -0
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +10 -18
- package/dist/client/react.d.ts +10 -18
- package/dist/client/react.js +17 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +17 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +3 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +3 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1056 -876
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -871
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
- package/dist/server/index.d.mts +338 -88
- package/dist/server/index.d.ts +338 -88
- package/dist/server/index.js +1053 -876
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1045 -871
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
- package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
- package/package.json +2 -1
- package/src/client/core/sse-client.ts +5 -0
- package/src/client/react/use-mcp.ts +27 -28
- package/src/server/handlers/sse-handler.ts +671 -633
- package/src/server/index.ts +2 -1
- package/src/server/mcp/multi-session-client.ts +34 -10
- package/src/server/mcp/oauth-client.ts +197 -205
- package/src/server/mcp/storage-oauth-provider.ts +148 -41
- package/src/server/mcp/tool-policy-gateway.ts +11 -1
- package/src/server/storage/file-backend.ts +25 -21
- package/src/server/storage/index.ts +71 -0
- package/src/server/storage/memory-backend.ts +24 -12
- package/src/server/storage/neon-backend.ts +70 -72
- package/src/server/storage/redis-backend.ts +26 -36
- package/src/server/storage/sqlite-backend.ts +25 -39
- package/src/server/storage/supabase-backend.ts +45 -54
- package/src/server/storage/types.ts +22 -2
- package/src/shared/types.ts +13 -3
package/dist/server/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
5
|
import { UnauthorizedError as UnauthorizedError$1 } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
6
|
import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
7
8
|
import * as fs2 from 'fs';
|
|
8
9
|
import { promises } from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
@@ -273,7 +274,6 @@ var RedisStorageBackend = class {
|
|
|
273
274
|
constructor(redis2) {
|
|
274
275
|
this.redis = redis2;
|
|
275
276
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
276
|
-
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
277
277
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
278
278
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
279
279
|
}
|
|
@@ -292,9 +292,6 @@ var RedisStorageBackend = class {
|
|
|
292
292
|
getSessionKey(userId, sessionId) {
|
|
293
293
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
294
294
|
}
|
|
295
|
-
getCredentialsKey(userId, sessionId) {
|
|
296
|
-
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
297
|
-
}
|
|
298
295
|
/**
|
|
299
296
|
* Generates Redis key for tracking all sessions for a user
|
|
300
297
|
* @private
|
|
@@ -384,48 +381,54 @@ var RedisStorageBackend = class {
|
|
|
384
381
|
}
|
|
385
382
|
async patchCredentials(userId, sessionId, data) {
|
|
386
383
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
if (!sessionExists) {
|
|
384
|
+
const currentStr = await this.redis.get(sessionKey);
|
|
385
|
+
if (!currentStr) {
|
|
390
386
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
391
387
|
}
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const effectiveTtl =
|
|
395
|
-
|
|
396
|
-
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
397
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
398
|
-
if (effectiveTtl > 0) {
|
|
399
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
400
|
-
} else {
|
|
401
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
402
|
-
}
|
|
388
|
+
const current = JSON.parse(currentStr);
|
|
389
|
+
const updated = { ...current, ...data, sessionId, userId };
|
|
390
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
391
|
+
await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
|
|
403
392
|
}
|
|
404
|
-
async get(userId, sessionId) {
|
|
393
|
+
async get(userId, sessionId, options) {
|
|
405
394
|
try {
|
|
406
395
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
407
396
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
408
|
-
if (!sessionDataStr)
|
|
409
|
-
|
|
397
|
+
if (!sessionDataStr) return null;
|
|
398
|
+
const session = normalizeStoredSession(JSON.parse(sessionDataStr));
|
|
399
|
+
if (!options?.includeCredentials) {
|
|
400
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
401
|
+
return sessionOnly;
|
|
410
402
|
}
|
|
411
|
-
|
|
412
|
-
return normalizeStoredSession(session);
|
|
403
|
+
return session;
|
|
413
404
|
} catch (error) {
|
|
414
405
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
415
406
|
return null;
|
|
416
407
|
}
|
|
417
408
|
}
|
|
418
409
|
async getCredentials(userId, sessionId) {
|
|
419
|
-
const session = await this.get(userId, sessionId);
|
|
410
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
420
411
|
if (!session) return null;
|
|
421
|
-
const
|
|
422
|
-
return
|
|
412
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
413
|
+
return {
|
|
414
|
+
sessionId,
|
|
415
|
+
userId,
|
|
416
|
+
clientInformation,
|
|
417
|
+
tokens,
|
|
418
|
+
codeVerifier,
|
|
419
|
+
codeVerifierChallenge,
|
|
420
|
+
codeVerifierNonce,
|
|
421
|
+
clientId,
|
|
422
|
+
oauthState
|
|
423
|
+
};
|
|
423
424
|
}
|
|
424
425
|
async clearCredentials(userId, sessionId) {
|
|
425
426
|
await this.patchCredentials(userId, sessionId, {
|
|
426
427
|
clientInformation: null,
|
|
427
428
|
tokens: null,
|
|
428
429
|
codeVerifier: null,
|
|
430
|
+
codeVerifierChallenge: null,
|
|
431
|
+
codeVerifierNonce: null,
|
|
429
432
|
clientId: null,
|
|
430
433
|
oauthState: null
|
|
431
434
|
});
|
|
@@ -458,10 +461,9 @@ var RedisStorageBackend = class {
|
|
|
458
461
|
async delete(userId, sessionId) {
|
|
459
462
|
try {
|
|
460
463
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
461
|
-
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
462
464
|
const userIdKey = this.getUserIdKey(userId);
|
|
463
465
|
await this.redis.srem(userIdKey, sessionId);
|
|
464
|
-
await this.redis.del(sessionKey
|
|
466
|
+
await this.redis.del(sessionKey);
|
|
465
467
|
} catch (error) {
|
|
466
468
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
467
469
|
}
|
|
@@ -492,9 +494,8 @@ var RedisStorageBackend = class {
|
|
|
492
494
|
async clearAll() {
|
|
493
495
|
try {
|
|
494
496
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
495
|
-
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
496
497
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
497
|
-
const allKeys = [...keys, ...
|
|
498
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
498
499
|
if (allKeys.length > 0) {
|
|
499
500
|
await this.redis.del(...allKeys);
|
|
500
501
|
}
|
|
@@ -518,7 +519,6 @@ var RedisStorageBackend = class {
|
|
|
518
519
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
519
520
|
if (staleSessionIds.length > 0) {
|
|
520
521
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
521
|
-
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
522
522
|
}
|
|
523
523
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
524
524
|
if (remainingCount === 0) {
|
|
@@ -543,7 +543,6 @@ var MemoryStorageBackend = class {
|
|
|
543
543
|
constructor() {
|
|
544
544
|
// Map<userId:sessionId, Session>
|
|
545
545
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
546
|
-
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
547
546
|
// Map<userId, Set<sessionId>>
|
|
548
547
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
549
548
|
}
|
|
@@ -581,26 +580,46 @@ var MemoryStorageBackend = class {
|
|
|
581
580
|
}
|
|
582
581
|
async patchCredentials(userId, sessionId, data) {
|
|
583
582
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
584
|
-
|
|
583
|
+
const current = this.sessions.get(sessionKey);
|
|
584
|
+
if (!current) {
|
|
585
585
|
throw new Error(`Session ${sessionId} not found`);
|
|
586
586
|
}
|
|
587
|
-
|
|
588
|
-
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
587
|
+
this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
589
588
|
}
|
|
590
|
-
async get(userId, sessionId) {
|
|
589
|
+
async get(userId, sessionId, options) {
|
|
591
590
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
592
|
-
|
|
591
|
+
const session = this.sessions.get(sessionKey) || null;
|
|
592
|
+
if (!session) return session;
|
|
593
|
+
if (!options?.includeCredentials) {
|
|
594
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
595
|
+
return sessionOnly;
|
|
596
|
+
}
|
|
597
|
+
return session;
|
|
593
598
|
}
|
|
594
599
|
async getCredentials(userId, sessionId) {
|
|
595
600
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
596
|
-
|
|
597
|
-
|
|
601
|
+
const session = this.sessions.get(sessionKey);
|
|
602
|
+
if (!session) return null;
|
|
603
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
604
|
+
return {
|
|
605
|
+
sessionId,
|
|
606
|
+
userId,
|
|
607
|
+
clientInformation,
|
|
608
|
+
tokens,
|
|
609
|
+
codeVerifier,
|
|
610
|
+
codeVerifierChallenge,
|
|
611
|
+
codeVerifierNonce,
|
|
612
|
+
clientId,
|
|
613
|
+
oauthState
|
|
614
|
+
};
|
|
598
615
|
}
|
|
599
616
|
async clearCredentials(userId, sessionId) {
|
|
600
617
|
await this.patchCredentials(userId, sessionId, {
|
|
601
618
|
clientInformation: null,
|
|
602
619
|
tokens: null,
|
|
603
620
|
codeVerifier: null,
|
|
621
|
+
codeVerifierChallenge: null,
|
|
622
|
+
codeVerifierNonce: null,
|
|
604
623
|
clientId: null,
|
|
605
624
|
oauthState: null
|
|
606
625
|
});
|
|
@@ -624,7 +643,6 @@ var MemoryStorageBackend = class {
|
|
|
624
643
|
async delete(userId, sessionId) {
|
|
625
644
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
626
645
|
this.sessions.delete(sessionKey);
|
|
627
|
-
this.credentials.delete(sessionKey);
|
|
628
646
|
const set = this.userIdSessions.get(userId);
|
|
629
647
|
if (set) {
|
|
630
648
|
set.delete(sessionId);
|
|
@@ -638,14 +656,12 @@ var MemoryStorageBackend = class {
|
|
|
638
656
|
}
|
|
639
657
|
async clearAll() {
|
|
640
658
|
this.sessions.clear();
|
|
641
|
-
this.credentials.clear();
|
|
642
659
|
this.userIdSessions.clear();
|
|
643
660
|
}
|
|
644
661
|
async cleanupExpired() {
|
|
645
662
|
for (const [key, session] of this.sessions.entries()) {
|
|
646
663
|
if (!isSessionExpired(session)) continue;
|
|
647
664
|
this.sessions.delete(key);
|
|
648
|
-
this.credentials.delete(key);
|
|
649
665
|
const set = this.userIdSessions.get(session.userId);
|
|
650
666
|
if (set) {
|
|
651
667
|
set.delete(session.sessionId);
|
|
@@ -665,7 +681,6 @@ var FileStorageBackend = class {
|
|
|
665
681
|
constructor(options = {}) {
|
|
666
682
|
__publicField(this, "filePath");
|
|
667
683
|
__publicField(this, "memoryCache", null);
|
|
668
|
-
__publicField(this, "credentialsCache", null);
|
|
669
684
|
__publicField(this, "initialized", false);
|
|
670
685
|
this.filePath = options.path || "./sessions.json";
|
|
671
686
|
}
|
|
@@ -680,22 +695,15 @@ var FileStorageBackend = class {
|
|
|
680
695
|
const data = await promises.readFile(this.filePath, "utf-8");
|
|
681
696
|
const json = JSON.parse(data);
|
|
682
697
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
683
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
684
698
|
if (Array.isArray(json.sessions)) {
|
|
685
699
|
json.sessions.forEach((s) => {
|
|
686
700
|
const session = normalizeStoredSession(s);
|
|
687
701
|
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
688
702
|
});
|
|
689
703
|
}
|
|
690
|
-
if (Array.isArray(json.credentials)) {
|
|
691
|
-
json.credentials.forEach((c) => {
|
|
692
|
-
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
704
|
} catch (error) {
|
|
696
705
|
if (error.code === "ENOENT") {
|
|
697
706
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
698
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
699
707
|
await this.flush();
|
|
700
708
|
} else {
|
|
701
709
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -709,10 +717,9 @@ var FileStorageBackend = class {
|
|
|
709
717
|
if (!this.initialized) await this.init();
|
|
710
718
|
}
|
|
711
719
|
async flush() {
|
|
712
|
-
if (!this.memoryCache
|
|
720
|
+
if (!this.memoryCache) return;
|
|
713
721
|
await promises.writeFile(this.filePath, JSON.stringify({
|
|
714
|
-
sessions: Array.from(this.memoryCache.values())
|
|
715
|
-
credentials: Array.from(this.credentialsCache.values())
|
|
722
|
+
sessions: Array.from(this.memoryCache.values())
|
|
716
723
|
}, null, 2), "utf-8");
|
|
717
724
|
}
|
|
718
725
|
getSessionKey(userId, sessionId) {
|
|
@@ -747,29 +754,49 @@ var FileStorageBackend = class {
|
|
|
747
754
|
async patchCredentials(userId, sessionId, data) {
|
|
748
755
|
await this.ensureInitialized();
|
|
749
756
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
750
|
-
|
|
757
|
+
const current = this.memoryCache.get(sessionKey);
|
|
758
|
+
if (!current) {
|
|
751
759
|
throw new Error(`Session ${sessionId} not found`);
|
|
752
760
|
}
|
|
753
|
-
|
|
754
|
-
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
761
|
+
this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
755
762
|
await this.flush();
|
|
756
763
|
}
|
|
757
|
-
async get(userId, sessionId) {
|
|
764
|
+
async get(userId, sessionId, options) {
|
|
758
765
|
await this.ensureInitialized();
|
|
759
766
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
760
|
-
|
|
767
|
+
const session = this.memoryCache.get(sessionKey) || null;
|
|
768
|
+
if (!session) return null;
|
|
769
|
+
if (!options?.includeCredentials) {
|
|
770
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
771
|
+
return sessionOnly;
|
|
772
|
+
}
|
|
773
|
+
return session;
|
|
761
774
|
}
|
|
762
775
|
async getCredentials(userId, sessionId) {
|
|
763
776
|
await this.ensureInitialized();
|
|
764
777
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
765
|
-
|
|
766
|
-
|
|
778
|
+
const session = this.memoryCache.get(sessionKey);
|
|
779
|
+
if (!session) return null;
|
|
780
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
781
|
+
return {
|
|
782
|
+
sessionId,
|
|
783
|
+
userId,
|
|
784
|
+
clientInformation,
|
|
785
|
+
tokens,
|
|
786
|
+
codeVerifier,
|
|
787
|
+
codeVerifierChallenge,
|
|
788
|
+
codeVerifierNonce,
|
|
789
|
+
clientId,
|
|
790
|
+
oauthState
|
|
791
|
+
};
|
|
767
792
|
}
|
|
768
793
|
async clearCredentials(userId, sessionId) {
|
|
769
794
|
await this.patchCredentials(userId, sessionId, {
|
|
770
795
|
clientInformation: null,
|
|
771
796
|
tokens: null,
|
|
772
797
|
codeVerifier: null,
|
|
798
|
+
codeVerifierChallenge: null,
|
|
799
|
+
codeVerifierNonce: null,
|
|
773
800
|
clientId: null,
|
|
774
801
|
oauthState: null
|
|
775
802
|
});
|
|
@@ -786,7 +813,6 @@ var FileStorageBackend = class {
|
|
|
786
813
|
await this.ensureInitialized();
|
|
787
814
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
788
815
|
const deleted = this.memoryCache.delete(sessionKey);
|
|
789
|
-
this.credentialsCache.delete(sessionKey);
|
|
790
816
|
if (deleted) {
|
|
791
817
|
await this.flush();
|
|
792
818
|
}
|
|
@@ -798,7 +824,6 @@ var FileStorageBackend = class {
|
|
|
798
824
|
async clearAll() {
|
|
799
825
|
await this.ensureInitialized();
|
|
800
826
|
this.memoryCache.clear();
|
|
801
|
-
this.credentialsCache.clear();
|
|
802
827
|
await this.flush();
|
|
803
828
|
}
|
|
804
829
|
async cleanupExpired() {
|
|
@@ -807,7 +832,6 @@ var FileStorageBackend = class {
|
|
|
807
832
|
for (const [key, session] of this.memoryCache.entries()) {
|
|
808
833
|
if (!isSessionExpired(session)) continue;
|
|
809
834
|
this.memoryCache.delete(key);
|
|
810
|
-
this.credentialsCache.delete(key);
|
|
811
835
|
changed = true;
|
|
812
836
|
}
|
|
813
837
|
if (changed) {
|
|
@@ -821,12 +845,10 @@ var SqliteStorage = class {
|
|
|
821
845
|
constructor(options = {}) {
|
|
822
846
|
__publicField(this, "db", null);
|
|
823
847
|
__publicField(this, "table");
|
|
824
|
-
__publicField(this, "credentialsTable");
|
|
825
848
|
__publicField(this, "initialized", false);
|
|
826
849
|
__publicField(this, "dbPath");
|
|
827
850
|
this.dbPath = options.path || "./sessions.db";
|
|
828
851
|
this.table = options.table || "mcp_sessions";
|
|
829
|
-
this.credentialsTable = `${this.table}_credentials`;
|
|
830
852
|
}
|
|
831
853
|
async init() {
|
|
832
854
|
if (this.initialized) return;
|
|
@@ -846,13 +868,6 @@ var SqliteStorage = class {
|
|
|
846
868
|
expiresAt INTEGER
|
|
847
869
|
);
|
|
848
870
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
849
|
-
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
850
|
-
sessionId TEXT NOT NULL,
|
|
851
|
-
userId TEXT NOT NULL,
|
|
852
|
-
data TEXT NOT NULL,
|
|
853
|
-
PRIMARY KEY (userId, sessionId),
|
|
854
|
-
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
855
|
-
);
|
|
856
871
|
`);
|
|
857
872
|
this.initialized = true;
|
|
858
873
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -909,41 +924,54 @@ var SqliteStorage = class {
|
|
|
909
924
|
}
|
|
910
925
|
async patchCredentials(userId, sessionId, data) {
|
|
911
926
|
this.ensureInitialized();
|
|
912
|
-
|
|
927
|
+
const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
|
|
928
|
+
if (!currentSession) {
|
|
913
929
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
914
930
|
}
|
|
915
|
-
const
|
|
916
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
931
|
+
const updated = { ...currentSession, ...data, sessionId, userId };
|
|
917
932
|
const stmt = this.db.prepare(
|
|
918
|
-
`
|
|
919
|
-
VALUES (?, ?, ?)
|
|
920
|
-
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
933
|
+
`UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
|
|
921
934
|
);
|
|
922
|
-
stmt.run(
|
|
935
|
+
stmt.run(JSON.stringify(updated), sessionId, userId);
|
|
923
936
|
}
|
|
924
|
-
async get(userId, sessionId) {
|
|
937
|
+
async get(userId, sessionId, options) {
|
|
925
938
|
this.ensureInitialized();
|
|
926
939
|
const stmt = this.db.prepare(
|
|
927
940
|
`SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
928
941
|
);
|
|
929
942
|
const row = stmt.get(sessionId, userId);
|
|
930
943
|
if (!row) return null;
|
|
931
|
-
|
|
944
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
945
|
+
if (!options?.includeCredentials) {
|
|
946
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
947
|
+
return sessionOnly;
|
|
948
|
+
}
|
|
949
|
+
return session;
|
|
932
950
|
}
|
|
933
951
|
async getCredentials(userId, sessionId) {
|
|
934
952
|
this.ensureInitialized();
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
953
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
954
|
+
if (!session) return null;
|
|
955
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
956
|
+
return {
|
|
957
|
+
sessionId,
|
|
958
|
+
userId,
|
|
959
|
+
clientInformation,
|
|
960
|
+
tokens,
|
|
961
|
+
codeVerifier,
|
|
962
|
+
codeVerifierChallenge,
|
|
963
|
+
codeVerifierNonce,
|
|
964
|
+
clientId,
|
|
965
|
+
oauthState
|
|
966
|
+
};
|
|
941
967
|
}
|
|
942
968
|
async clearCredentials(userId, sessionId) {
|
|
943
969
|
await this.patchCredentials(userId, sessionId, {
|
|
944
970
|
clientInformation: null,
|
|
945
971
|
tokens: null,
|
|
946
972
|
codeVerifier: null,
|
|
973
|
+
codeVerifierChallenge: null,
|
|
974
|
+
codeVerifierNonce: null,
|
|
947
975
|
clientId: null,
|
|
948
976
|
oauthState: null
|
|
949
977
|
});
|
|
@@ -969,9 +997,6 @@ var SqliteStorage = class {
|
|
|
969
997
|
const stmt = this.db.prepare(
|
|
970
998
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
971
999
|
);
|
|
972
|
-
this.db.prepare(
|
|
973
|
-
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
974
|
-
).run(sessionId, userId);
|
|
975
1000
|
stmt.run(sessionId, userId);
|
|
976
1001
|
}
|
|
977
1002
|
async listAllIds() {
|
|
@@ -982,7 +1007,6 @@ var SqliteStorage = class {
|
|
|
982
1007
|
}
|
|
983
1008
|
async clearAll() {
|
|
984
1009
|
this.ensureInitialized();
|
|
985
|
-
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
986
1010
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
987
1011
|
stmt.run();
|
|
988
1012
|
}
|
|
@@ -996,14 +1020,6 @@ var SqliteStorage = class {
|
|
|
996
1020
|
deleteStmt.run(row.sessionId, row.userId);
|
|
997
1021
|
}
|
|
998
1022
|
}
|
|
999
|
-
this.db.prepare(
|
|
1000
|
-
`DELETE FROM ${this.credentialsTable}
|
|
1001
|
-
WHERE NOT EXISTS (
|
|
1002
|
-
SELECT 1 FROM ${this.table}
|
|
1003
|
-
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1004
|
-
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1005
|
-
)`
|
|
1006
|
-
).run();
|
|
1007
1023
|
}
|
|
1008
1024
|
async disconnect() {
|
|
1009
1025
|
if (this.db) {
|
|
@@ -1085,7 +1101,6 @@ var SupabaseStorageBackend = class {
|
|
|
1085
1101
|
}
|
|
1086
1102
|
async init() {
|
|
1087
1103
|
await this.assertTable("mcp_sessions", "session_id");
|
|
1088
|
-
await this.assertTable("mcp_credentials", "session_id");
|
|
1089
1104
|
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1090
1105
|
}
|
|
1091
1106
|
async assertTable(table, column) {
|
|
@@ -1116,18 +1131,13 @@ var SupabaseStorageBackend = class {
|
|
|
1116
1131
|
headers: decryptObject(row.headers),
|
|
1117
1132
|
authUrl: row.auth_url,
|
|
1118
1133
|
status: row.status ?? "pending",
|
|
1119
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
clientInformation: decryptObject(row?.client_information),
|
|
1127
|
-
tokens: decryptObject(row?.tokens),
|
|
1128
|
-
codeVerifier: decryptObject(row?.code_verifier),
|
|
1129
|
-
clientId: row?.client_id,
|
|
1130
|
-
oauthState: row?.oauth_state
|
|
1134
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1135
|
+
clientInformation: decryptObject(row.client_information),
|
|
1136
|
+
tokens: decryptObject(row.tokens),
|
|
1137
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1138
|
+
clientId: row.client_id,
|
|
1139
|
+
oauthState: row.oauth_state,
|
|
1140
|
+
enabled: row.enabled ?? true
|
|
1131
1141
|
};
|
|
1132
1142
|
}
|
|
1133
1143
|
hasCredentialData(data) {
|
|
@@ -1185,6 +1195,7 @@ var SupabaseStorageBackend = class {
|
|
|
1185
1195
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
1186
1196
|
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1187
1197
|
if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
|
|
1198
|
+
if ("enabled" in data) updateData.enabled = data.enabled;
|
|
1188
1199
|
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1189
1200
|
let updatedRows = null;
|
|
1190
1201
|
if (shouldUpdateSession) {
|
|
@@ -1206,23 +1217,22 @@ var SupabaseStorageBackend = class {
|
|
|
1206
1217
|
}
|
|
1207
1218
|
async patchCredentials(userId, sessionId, data) {
|
|
1208
1219
|
if (!this.hasCredentialData(data)) return;
|
|
1209
|
-
const
|
|
1210
|
-
user_id: userId,
|
|
1211
|
-
session_id: sessionId,
|
|
1220
|
+
const updateData = {
|
|
1212
1221
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1213
1222
|
};
|
|
1214
|
-
if ("clientInformation" in data)
|
|
1215
|
-
if ("tokens" in data)
|
|
1216
|
-
if ("codeVerifier" in data)
|
|
1217
|
-
if ("clientId" in data)
|
|
1218
|
-
if ("oauthState" in data)
|
|
1219
|
-
const { error } = await this.supabase.from("
|
|
1223
|
+
if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1224
|
+
if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1225
|
+
if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1226
|
+
if ("clientId" in data) updateData.client_id = data.clientId ?? null;
|
|
1227
|
+
if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
|
|
1228
|
+
const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
|
|
1220
1229
|
if (error) {
|
|
1221
1230
|
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1222
1231
|
}
|
|
1223
1232
|
}
|
|
1224
|
-
async get(userId, sessionId) {
|
|
1225
|
-
const
|
|
1233
|
+
async get(userId, sessionId, options) {
|
|
1234
|
+
const selection = options?.includeCredentials ? "*" : "session_id, user_id, server_id, server_name, server_url, transport_type, callback_url, created_at, updated_at, expires_at, headers, auth_url, status, tool_policy, enabled";
|
|
1235
|
+
const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1226
1236
|
if (error) {
|
|
1227
1237
|
console.error("[SupabaseStorage] Failed to get session:", error);
|
|
1228
1238
|
return null;
|
|
@@ -1231,19 +1241,21 @@ var SupabaseStorageBackend = class {
|
|
|
1231
1241
|
return this.mapRowToSessionData(data);
|
|
1232
1242
|
}
|
|
1233
1243
|
async getCredentials(userId, sessionId) {
|
|
1234
|
-
const { data, error } = await this.supabase.from("
|
|
1244
|
+
const { data, error } = await this.supabase.from("mcp_sessions").select("client_information, tokens, code_verifier, client_id, oauth_state").eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1235
1245
|
if (error) {
|
|
1236
1246
|
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1237
1247
|
return null;
|
|
1238
1248
|
}
|
|
1239
|
-
if (data)
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1249
|
+
if (!data) return null;
|
|
1250
|
+
return {
|
|
1251
|
+
sessionId,
|
|
1252
|
+
userId,
|
|
1253
|
+
clientInformation: decryptObject(data.client_information),
|
|
1254
|
+
tokens: decryptObject(data.tokens),
|
|
1255
|
+
codeVerifier: decryptObject(data.code_verifier),
|
|
1256
|
+
clientId: data.client_id,
|
|
1257
|
+
oauthState: data.oauth_state
|
|
1258
|
+
};
|
|
1247
1259
|
}
|
|
1248
1260
|
async list(userId) {
|
|
1249
1261
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
@@ -1254,7 +1266,14 @@ var SupabaseStorageBackend = class {
|
|
|
1254
1266
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
1255
1267
|
}
|
|
1256
1268
|
async clearCredentials(userId, sessionId) {
|
|
1257
|
-
const { error } = await this.supabase.from("
|
|
1269
|
+
const { error } = await this.supabase.from("mcp_sessions").update({
|
|
1270
|
+
client_information: null,
|
|
1271
|
+
tokens: null,
|
|
1272
|
+
code_verifier: null,
|
|
1273
|
+
client_id: null,
|
|
1274
|
+
oauth_state: null,
|
|
1275
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1276
|
+
}).eq("user_id", userId).eq("session_id", sessionId);
|
|
1258
1277
|
if (error) {
|
|
1259
1278
|
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1260
1279
|
}
|
|
@@ -1282,10 +1301,6 @@ var SupabaseStorageBackend = class {
|
|
|
1282
1301
|
return data.map((row) => row.session_id);
|
|
1283
1302
|
}
|
|
1284
1303
|
async clearAll() {
|
|
1285
|
-
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1286
|
-
if (credentialsError) {
|
|
1287
|
-
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1288
|
-
}
|
|
1289
1304
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
1290
1305
|
if (error) {
|
|
1291
1306
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
@@ -1311,16 +1326,12 @@ var NeonStorageBackend = class {
|
|
|
1311
1326
|
constructor(sql, options = {}) {
|
|
1312
1327
|
this.sql = sql;
|
|
1313
1328
|
__publicField(this, "tableName");
|
|
1314
|
-
__publicField(this, "credentialsTableName");
|
|
1315
1329
|
const schema = options.schema || "public";
|
|
1316
1330
|
const table = options.table || "mcp_sessions";
|
|
1317
|
-
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
1318
1331
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1319
|
-
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
1320
1332
|
}
|
|
1321
1333
|
async init() {
|
|
1322
1334
|
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1323
|
-
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1324
1335
|
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1325
1336
|
}
|
|
1326
1337
|
async assertTable(qualifiedName, displayName) {
|
|
@@ -1358,18 +1369,13 @@ var NeonStorageBackend = class {
|
|
|
1358
1369
|
headers: decryptObject(row.headers),
|
|
1359
1370
|
authUrl: row.auth_url ?? void 0,
|
|
1360
1371
|
status: row.status ?? "pending",
|
|
1361
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1362
|
-
};
|
|
1363
|
-
}
|
|
1364
|
-
mapRowToCredentials(row, userId, sessionId) {
|
|
1365
|
-
return {
|
|
1366
|
-
sessionId,
|
|
1367
|
-
userId,
|
|
1372
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1368
1373
|
clientInformation: decryptObject(row.client_information),
|
|
1369
1374
|
tokens: decryptObject(row.tokens),
|
|
1370
1375
|
codeVerifier: decryptObject(row.code_verifier),
|
|
1371
1376
|
clientId: row.client_id ?? void 0,
|
|
1372
|
-
oauthState: row.oauth_state
|
|
1377
|
+
oauthState: row.oauth_state,
|
|
1378
|
+
enabled: row.enabled ?? true
|
|
1373
1379
|
};
|
|
1374
1380
|
}
|
|
1375
1381
|
hasCredentialData(data) {
|
|
@@ -1439,7 +1445,7 @@ var NeonStorageBackend = class {
|
|
|
1439
1445
|
const updatedSession = { ...currentSession, ...data };
|
|
1440
1446
|
const status = updatedSession.status ?? "pending";
|
|
1441
1447
|
const expiresAt = resolveSessionExpiresAt(status);
|
|
1442
|
-
const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data || "toolPolicy" in data;
|
|
1448
|
+
const shouldUpdateSession = "serverId" in data || "serverName" in data || "serverUrl" in data || "transportType" in data || "callbackUrl" in data || "status" in data || "headers" in data || "authUrl" in data || "toolPolicy" in data || "enabled" in data;
|
|
1443
1449
|
if (shouldUpdateSession) {
|
|
1444
1450
|
const setClauses = [];
|
|
1445
1451
|
const values = [];
|
|
@@ -1462,6 +1468,9 @@ var NeonStorageBackend = class {
|
|
|
1462
1468
|
const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
|
|
1463
1469
|
addSet("tool_policy", toolPolicy);
|
|
1464
1470
|
}
|
|
1471
|
+
if ("enabled" in data) {
|
|
1472
|
+
addSet("enabled", updatedSession.enabled);
|
|
1473
|
+
}
|
|
1465
1474
|
setClauses.push("updated_at = now()");
|
|
1466
1475
|
const updatedRows = await this.sql.query(
|
|
1467
1476
|
`UPDATE ${this.tableName}
|
|
@@ -1477,45 +1486,45 @@ var NeonStorageBackend = class {
|
|
|
1477
1486
|
}
|
|
1478
1487
|
async patchCredentials(userId, sessionId, data) {
|
|
1479
1488
|
if (!this.hasCredentialData(data)) return;
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
"codeVerifier" in data,
|
|
1510
|
-
"clientId" in data,
|
|
1511
|
-
"oauthState" in data
|
|
1512
|
-
]
|
|
1489
|
+
const setClauses = [];
|
|
1490
|
+
const values = [];
|
|
1491
|
+
let paramIndex = 1;
|
|
1492
|
+
const addSet = (column, value) => {
|
|
1493
|
+
setClauses.push(`${column} = $${paramIndex++}`);
|
|
1494
|
+
values.push(value);
|
|
1495
|
+
};
|
|
1496
|
+
if ("clientInformation" in data) {
|
|
1497
|
+
addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
|
|
1498
|
+
}
|
|
1499
|
+
if ("tokens" in data) {
|
|
1500
|
+
addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
|
|
1501
|
+
}
|
|
1502
|
+
if ("codeVerifier" in data) {
|
|
1503
|
+
addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
|
|
1504
|
+
}
|
|
1505
|
+
if ("clientId" in data) {
|
|
1506
|
+
addSet("client_id", data.clientId ?? null);
|
|
1507
|
+
}
|
|
1508
|
+
if ("oauthState" in data) {
|
|
1509
|
+
addSet("oauth_state", data.oauthState ?? null);
|
|
1510
|
+
}
|
|
1511
|
+
setClauses.push("updated_at = now()");
|
|
1512
|
+
const updatedRows = await this.sql.query(
|
|
1513
|
+
`UPDATE ${this.tableName}
|
|
1514
|
+
SET ${setClauses.join(", ")}
|
|
1515
|
+
WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
|
|
1516
|
+
RETURNING id`,
|
|
1517
|
+
[...values, userId, sessionId]
|
|
1513
1518
|
);
|
|
1519
|
+
if (updatedRows.length === 0) {
|
|
1520
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1521
|
+
}
|
|
1514
1522
|
}
|
|
1515
|
-
async get(userId, sessionId) {
|
|
1523
|
+
async get(userId, sessionId, options) {
|
|
1516
1524
|
try {
|
|
1525
|
+
const selection = options?.includeCredentials ? "*" : "session_id, user_id, server_id, server_name, server_url, transport_type, callback_url, created_at, updated_at, expires_at, headers, auth_url, status, tool_policy, enabled";
|
|
1517
1526
|
const rows = await this.sql.query(
|
|
1518
|
-
`SELECT
|
|
1527
|
+
`SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1519
1528
|
[userId, sessionId]
|
|
1520
1529
|
);
|
|
1521
1530
|
if (!rows[0]) return null;
|
|
@@ -1527,18 +1536,22 @@ var NeonStorageBackend = class {
|
|
|
1527
1536
|
}
|
|
1528
1537
|
async getCredentials(userId, sessionId) {
|
|
1529
1538
|
try {
|
|
1530
|
-
const
|
|
1531
|
-
`SELECT
|
|
1532
|
-
|
|
1533
|
-
);
|
|
1534
|
-
if (credentialRows[0]) {
|
|
1535
|
-
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1536
|
-
}
|
|
1537
|
-
const sessionRows = await this.sql.query(
|
|
1538
|
-
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1539
|
+
const rows = await this.sql.query(
|
|
1540
|
+
`SELECT client_information, tokens, code_verifier, client_id, oauth_state
|
|
1541
|
+
FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1539
1542
|
[userId, sessionId]
|
|
1540
1543
|
);
|
|
1541
|
-
|
|
1544
|
+
if (!rows[0]) return null;
|
|
1545
|
+
const row = rows[0];
|
|
1546
|
+
return {
|
|
1547
|
+
sessionId,
|
|
1548
|
+
userId,
|
|
1549
|
+
clientInformation: decryptObject(row.client_information),
|
|
1550
|
+
tokens: decryptObject(row.tokens),
|
|
1551
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1552
|
+
clientId: row.client_id ?? void 0,
|
|
1553
|
+
oauthState: row.oauth_state
|
|
1554
|
+
};
|
|
1542
1555
|
} catch (error) {
|
|
1543
1556
|
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1544
1557
|
return null;
|
|
@@ -1559,7 +1572,9 @@ var NeonStorageBackend = class {
|
|
|
1559
1572
|
async clearCredentials(userId, sessionId) {
|
|
1560
1573
|
try {
|
|
1561
1574
|
await this.sql.query(
|
|
1562
|
-
`
|
|
1575
|
+
`UPDATE ${this.tableName}
|
|
1576
|
+
SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
|
|
1577
|
+
WHERE user_id = $1 AND session_id = $2`,
|
|
1563
1578
|
[userId, sessionId]
|
|
1564
1579
|
);
|
|
1565
1580
|
} catch (error) {
|
|
@@ -1601,7 +1616,6 @@ var NeonStorageBackend = class {
|
|
|
1601
1616
|
}
|
|
1602
1617
|
async clearAll() {
|
|
1603
1618
|
try {
|
|
1604
|
-
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1605
1619
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1606
1620
|
} catch (error) {
|
|
1607
1621
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1843,6 +1857,60 @@ function onSessionMutation(listener) {
|
|
|
1843
1857
|
sessionMutationListeners.delete(listener);
|
|
1844
1858
|
};
|
|
1845
1859
|
}
|
|
1860
|
+
function withDbObservability(store, emit) {
|
|
1861
|
+
const readMethods = /* @__PURE__ */ new Set([
|
|
1862
|
+
"get",
|
|
1863
|
+
"getCredentials",
|
|
1864
|
+
"list",
|
|
1865
|
+
"listIds",
|
|
1866
|
+
"listAllIds"
|
|
1867
|
+
]);
|
|
1868
|
+
return new Proxy(store, {
|
|
1869
|
+
get(target, prop, receiver) {
|
|
1870
|
+
const original = Reflect.get(target, prop, receiver);
|
|
1871
|
+
if (typeof original !== "function") return original;
|
|
1872
|
+
return (...args) => {
|
|
1873
|
+
const method = prop;
|
|
1874
|
+
const isRead = readMethods.has(prop);
|
|
1875
|
+
const start = performance.now();
|
|
1876
|
+
const emitEvent = (error) => {
|
|
1877
|
+
emit({
|
|
1878
|
+
type: isRead ? "db:read" : "db:write",
|
|
1879
|
+
level: error ? "error" : "debug",
|
|
1880
|
+
message: `${method}(${args.map(
|
|
1881
|
+
(a) => typeof a === "string" ? a.slice(0, 24) : typeof a
|
|
1882
|
+
).join(", ")})`,
|
|
1883
|
+
sessionId: typeof args[1] === "string" ? args[1] : void 0,
|
|
1884
|
+
payload: {
|
|
1885
|
+
method,
|
|
1886
|
+
argTypes: args.map((a) => typeof a),
|
|
1887
|
+
durationMs: performance.now() - start,
|
|
1888
|
+
...error ? { error } : {}
|
|
1889
|
+
},
|
|
1890
|
+
timestamp: Date.now()
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1893
|
+
try {
|
|
1894
|
+
const result = original.apply(target, args);
|
|
1895
|
+
if (result instanceof Promise) {
|
|
1896
|
+
return result.then((r) => {
|
|
1897
|
+
emitEvent();
|
|
1898
|
+
return r;
|
|
1899
|
+
}).catch((e) => {
|
|
1900
|
+
emitEvent(String(e));
|
|
1901
|
+
throw e;
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
emitEvent();
|
|
1905
|
+
return result;
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
emitEvent(String(e));
|
|
1908
|
+
throw e;
|
|
1909
|
+
}
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1846
1914
|
var sessions = new Proxy({}, {
|
|
1847
1915
|
get(_target, prop) {
|
|
1848
1916
|
return async (...args) => {
|
|
@@ -1862,6 +1930,24 @@ var sessions = new Proxy({}, {
|
|
|
1862
1930
|
});
|
|
1863
1931
|
|
|
1864
1932
|
// src/server/mcp/storage-oauth-provider.ts
|
|
1933
|
+
var codeVerifierContext = new AsyncLocalStorage();
|
|
1934
|
+
function runWithCodeVerifierState(verifier, method, fn) {
|
|
1935
|
+
return codeVerifierContext.run({ verifier, method }, fn);
|
|
1936
|
+
}
|
|
1937
|
+
function base64UrlEncode(bytes) {
|
|
1938
|
+
let binary = "";
|
|
1939
|
+
for (const byte of bytes) {
|
|
1940
|
+
binary += String.fromCharCode(byte);
|
|
1941
|
+
}
|
|
1942
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1943
|
+
}
|
|
1944
|
+
async function createCodeChallenge(verifier) {
|
|
1945
|
+
const digest = await crypto.subtle.digest(
|
|
1946
|
+
"SHA-256",
|
|
1947
|
+
new TextEncoder().encode(verifier)
|
|
1948
|
+
);
|
|
1949
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
1950
|
+
}
|
|
1865
1951
|
var StorageOAuthClientProvider = class {
|
|
1866
1952
|
/**
|
|
1867
1953
|
* Creates a new session-backed OAuth provider
|
|
@@ -1877,8 +1963,12 @@ var StorageOAuthClientProvider = class {
|
|
|
1877
1963
|
__publicField(this, "logoUri");
|
|
1878
1964
|
__publicField(this, "policyUri");
|
|
1879
1965
|
__publicField(this, "clientSecret");
|
|
1966
|
+
__publicField(this, "_store");
|
|
1880
1967
|
__publicField(this, "_authUrl");
|
|
1881
1968
|
__publicField(this, "_clientId");
|
|
1969
|
+
__publicField(this, "_cachedCodeVerifier");
|
|
1970
|
+
__publicField(this, "_hasCodeVerifier", false);
|
|
1971
|
+
__publicField(this, "_cachedTokens");
|
|
1882
1972
|
__publicField(this, "onRedirectCallback");
|
|
1883
1973
|
this.userId = options.userId;
|
|
1884
1974
|
this.serverId = options.serverId;
|
|
@@ -1890,6 +1980,8 @@ var StorageOAuthClientProvider = class {
|
|
|
1890
1980
|
this.policyUri = options.policyUri;
|
|
1891
1981
|
this._clientId = options.clientId;
|
|
1892
1982
|
this.clientSecret = options.clientSecret;
|
|
1983
|
+
this._cachedTokens = options.cachedTokens;
|
|
1984
|
+
this._store = options.sessionStore ?? sessions;
|
|
1893
1985
|
this.onRedirectCallback = options.onRedirect;
|
|
1894
1986
|
}
|
|
1895
1987
|
get clientMetadata() {
|
|
@@ -1917,7 +2009,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1917
2009
|
* @private
|
|
1918
2010
|
*/
|
|
1919
2011
|
async getCredentials() {
|
|
1920
|
-
const data = await
|
|
2012
|
+
const data = await this._store.getCredentials(this.userId, this.sessionId);
|
|
1921
2013
|
if (!data) {
|
|
1922
2014
|
return { userId: this.userId, sessionId: this.sessionId };
|
|
1923
2015
|
}
|
|
@@ -1930,35 +2022,33 @@ var StorageOAuthClientProvider = class {
|
|
|
1930
2022
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1931
2023
|
*/
|
|
1932
2024
|
async patchCredentials(data) {
|
|
1933
|
-
await
|
|
2025
|
+
await this._store.patchCredentials(this.userId, this.sessionId, data);
|
|
1934
2026
|
}
|
|
1935
2027
|
/**
|
|
1936
|
-
* Retrieves stored OAuth client information
|
|
2028
|
+
* Retrieves stored OAuth client information.
|
|
1937
2029
|
*/
|
|
1938
2030
|
async clientInformation() {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
await this.patchCredentials({
|
|
1945
|
-
clientId: this._clientId,
|
|
1946
|
-
clientInformation: {
|
|
1947
|
-
client_id: this._clientId,
|
|
1948
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
1949
|
-
}
|
|
1950
|
-
});
|
|
2031
|
+
if (this._clientId) {
|
|
2032
|
+
return {
|
|
2033
|
+
client_id: this._clientId,
|
|
2034
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2035
|
+
};
|
|
1951
2036
|
}
|
|
1952
|
-
|
|
1953
|
-
|
|
2037
|
+
const data = await this.getCredentials();
|
|
2038
|
+
if (this._cachedTokens === void 0) {
|
|
2039
|
+
this._cachedTokens = data.tokens ?? null;
|
|
1954
2040
|
}
|
|
1955
|
-
if (
|
|
1956
|
-
|
|
2041
|
+
if (data.clientId) {
|
|
2042
|
+
this._clientId = data.clientId;
|
|
2043
|
+
if (data.clientInformation) {
|
|
2044
|
+
return data.clientInformation;
|
|
2045
|
+
}
|
|
2046
|
+
return {
|
|
2047
|
+
client_id: data.clientId,
|
|
2048
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2049
|
+
};
|
|
1957
2050
|
}
|
|
1958
|
-
return
|
|
1959
|
-
client_id: this._clientId,
|
|
1960
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
1961
|
-
};
|
|
2051
|
+
return void 0;
|
|
1962
2052
|
}
|
|
1963
2053
|
/**
|
|
1964
2054
|
* Stores OAuth client information
|
|
@@ -1975,11 +2065,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1975
2065
|
*/
|
|
1976
2066
|
async saveTokens(tokens) {
|
|
1977
2067
|
await this.patchCredentials({ tokens });
|
|
2068
|
+
this._cachedTokens = tokens;
|
|
2069
|
+
}
|
|
2070
|
+
/**
|
|
2071
|
+
* Persists static client credentials to DB immediately on creation.
|
|
2072
|
+
* Ensures getOrCreateClient() finds them on rehydration, even when the
|
|
2073
|
+
* server allows anonymous connect and never triggers DCR.
|
|
2074
|
+
*/
|
|
2075
|
+
async initializeCredentials() {
|
|
2076
|
+
if (this._clientId) {
|
|
2077
|
+
await this.patchCredentials({
|
|
2078
|
+
clientId: this._clientId,
|
|
2079
|
+
clientInformation: {
|
|
2080
|
+
client_id: this._clientId,
|
|
2081
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2082
|
+
}
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
1978
2085
|
}
|
|
1979
2086
|
get authUrl() {
|
|
1980
2087
|
return this._authUrl;
|
|
1981
2088
|
}
|
|
1982
2089
|
async state() {
|
|
2090
|
+
this._cachedCodeVerifier = void 0;
|
|
2091
|
+
this._hasCodeVerifier = false;
|
|
1983
2092
|
const nonce = nanoid(32);
|
|
1984
2093
|
await this.patchCredentials({
|
|
1985
2094
|
oauthState: {
|
|
@@ -2000,7 +2109,7 @@ var StorageOAuthClientProvider = class {
|
|
|
2000
2109
|
if (parsed.sessionId !== this.sessionId) {
|
|
2001
2110
|
return { valid: false, error: "OAuth state mismatch" };
|
|
2002
2111
|
}
|
|
2003
|
-
const data = await
|
|
2112
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
2004
2113
|
if (!data) {
|
|
2005
2114
|
return { valid: false, error: "Session not found" };
|
|
2006
2115
|
}
|
|
@@ -2025,49 +2134,77 @@ var StorageOAuthClientProvider = class {
|
|
|
2025
2134
|
}
|
|
2026
2135
|
async redirectToAuthorization(authUrl) {
|
|
2027
2136
|
this._authUrl = authUrl.toString();
|
|
2028
|
-
|
|
2137
|
+
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
|
2138
|
+
const state = authUrl.searchParams.get("state");
|
|
2139
|
+
if (this._cachedCodeVerifier && codeChallenge && state) {
|
|
2140
|
+
const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
|
|
2141
|
+
if (expectedChallenge === codeChallenge) {
|
|
2142
|
+
const parsed = parseOAuthState(state);
|
|
2143
|
+
if (parsed) {
|
|
2144
|
+
await this.patchCredentials({
|
|
2145
|
+
codeVerifier: this._cachedCodeVerifier
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2029
2150
|
if (this.onRedirectCallback) {
|
|
2030
2151
|
this.onRedirectCallback(authUrl.toString());
|
|
2031
2152
|
}
|
|
2032
2153
|
}
|
|
2033
2154
|
async invalidateCredentials(scope) {
|
|
2034
2155
|
if (scope === "all") {
|
|
2035
|
-
|
|
2156
|
+
this._cachedTokens = void 0;
|
|
2157
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
2036
2158
|
} else {
|
|
2037
2159
|
const updates = {};
|
|
2038
2160
|
if (scope === "client") {
|
|
2039
2161
|
updates.clientInformation = null;
|
|
2040
2162
|
updates.clientId = null;
|
|
2041
2163
|
} else if (scope === "tokens") {
|
|
2164
|
+
this._cachedTokens = void 0;
|
|
2042
2165
|
updates.tokens = null;
|
|
2043
2166
|
} else if (scope === "verifier") {
|
|
2167
|
+
this._cachedCodeVerifier = void 0;
|
|
2168
|
+
this._hasCodeVerifier = false;
|
|
2044
2169
|
updates.codeVerifier = null;
|
|
2045
2170
|
}
|
|
2046
2171
|
await this.patchCredentials(updates);
|
|
2047
2172
|
}
|
|
2048
2173
|
}
|
|
2049
2174
|
async saveCodeVerifier(verifier) {
|
|
2050
|
-
|
|
2051
|
-
if (data.codeVerifier) {
|
|
2175
|
+
if (this._hasCodeVerifier) {
|
|
2052
2176
|
return;
|
|
2053
2177
|
}
|
|
2054
|
-
|
|
2178
|
+
this._cachedCodeVerifier = verifier;
|
|
2179
|
+
this._hasCodeVerifier = true;
|
|
2055
2180
|
}
|
|
2056
2181
|
async codeVerifier() {
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
this._clientId = data.clientId;
|
|
2182
|
+
if (this._cachedCodeVerifier) {
|
|
2183
|
+
return this._cachedCodeVerifier;
|
|
2060
2184
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2185
|
+
const ctx = codeVerifierContext.getStore();
|
|
2186
|
+
if (ctx?.verifier) {
|
|
2187
|
+
return ctx.verifier;
|
|
2188
|
+
}
|
|
2189
|
+
const data = await this.getCredentials();
|
|
2190
|
+
if (data.codeVerifier) {
|
|
2191
|
+
return data.codeVerifier;
|
|
2063
2192
|
}
|
|
2064
|
-
|
|
2193
|
+
throw new Error("No code verifier found");
|
|
2065
2194
|
}
|
|
2066
2195
|
async deleteCodeVerifier() {
|
|
2067
|
-
|
|
2196
|
+
this._cachedCodeVerifier = void 0;
|
|
2197
|
+
this._hasCodeVerifier = false;
|
|
2198
|
+
await this.patchCredentials({
|
|
2199
|
+
codeVerifier: null
|
|
2200
|
+
});
|
|
2068
2201
|
}
|
|
2069
2202
|
async tokens() {
|
|
2203
|
+
if (this._cachedTokens !== void 0) {
|
|
2204
|
+
return this._cachedTokens ?? void 0;
|
|
2205
|
+
}
|
|
2070
2206
|
const data = await this.getCredentials();
|
|
2207
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2071
2208
|
if (data.clientId && !this._clientId) {
|
|
2072
2209
|
this._clientId = data.clientId;
|
|
2073
2210
|
}
|
|
@@ -2160,23 +2297,10 @@ var MCPClient = class {
|
|
|
2160
2297
|
__publicField(this, "client");
|
|
2161
2298
|
__publicField(this, "oauthProvider", null);
|
|
2162
2299
|
__publicField(this, "transport", null);
|
|
2163
|
-
__publicField(this, "
|
|
2164
|
-
__publicField(this, "serverId");
|
|
2165
|
-
__publicField(this, "sessionId");
|
|
2166
|
-
__publicField(this, "serverName");
|
|
2167
|
-
__publicField(this, "transportType");
|
|
2168
|
-
__publicField(this, "serverUrl");
|
|
2169
|
-
__publicField(this, "callbackUrl");
|
|
2170
|
-
__publicField(this, "onRedirect");
|
|
2171
|
-
__publicField(this, "clientId");
|
|
2172
|
-
__publicField(this, "clientSecret");
|
|
2173
|
-
__publicField(this, "headers");
|
|
2174
|
-
/** OAuth Client Metadata */
|
|
2175
|
-
__publicField(this, "clientName");
|
|
2176
|
-
__publicField(this, "clientUri");
|
|
2177
|
-
__publicField(this, "logoUri");
|
|
2178
|
-
__publicField(this, "policyUri");
|
|
2300
|
+
__publicField(this, "config");
|
|
2179
2301
|
__publicField(this, "createdAt");
|
|
2302
|
+
__publicField(this, "_serverInfo");
|
|
2303
|
+
__publicField(this, "_store");
|
|
2180
2304
|
/** Event emitters for connection lifecycle */
|
|
2181
2305
|
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
2182
2306
|
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
@@ -2192,21 +2316,8 @@ var MCPClient = class {
|
|
|
2192
2316
|
* the memory when the client is no longer needed.
|
|
2193
2317
|
*/
|
|
2194
2318
|
__publicField(this, "cachedTools", null);
|
|
2195
|
-
this.
|
|
2196
|
-
this.
|
|
2197
|
-
this.callbackUrl = options.callbackUrl;
|
|
2198
|
-
this.onRedirect = options.onRedirect;
|
|
2199
|
-
this.userId = options.userId;
|
|
2200
|
-
this.serverId = options.serverId;
|
|
2201
|
-
this.sessionId = options.sessionId;
|
|
2202
|
-
this.transportType = options.transportType;
|
|
2203
|
-
this.clientId = options.clientId;
|
|
2204
|
-
this.clientSecret = options.clientSecret;
|
|
2205
|
-
this.headers = options.headers;
|
|
2206
|
-
this.clientName = options.clientName;
|
|
2207
|
-
this.clientUri = options.clientUri;
|
|
2208
|
-
this.logoUri = options.logoUri;
|
|
2209
|
-
this.policyUri = options.policyUri;
|
|
2319
|
+
this.config = { ...options };
|
|
2320
|
+
this._store = options.sessionStore ?? sessions;
|
|
2210
2321
|
this.client = new Client(
|
|
2211
2322
|
{
|
|
2212
2323
|
name: MCP_CLIENT_NAME,
|
|
@@ -2223,6 +2334,21 @@ var MCPClient = class {
|
|
|
2223
2334
|
}
|
|
2224
2335
|
);
|
|
2225
2336
|
}
|
|
2337
|
+
/** Shared session-shaped data for ensureSession and saveSession */
|
|
2338
|
+
get session() {
|
|
2339
|
+
return {
|
|
2340
|
+
sessionId: this.config.sessionId,
|
|
2341
|
+
userId: this.config.userId,
|
|
2342
|
+
serverId: this.config.serverId,
|
|
2343
|
+
serverName: this.config.serverName,
|
|
2344
|
+
serverUrl: this.config.serverUrl,
|
|
2345
|
+
callbackUrl: this.config.callbackUrl,
|
|
2346
|
+
transportType: this.config.transportType || "streamable-http",
|
|
2347
|
+
headers: this.config.headers,
|
|
2348
|
+
createdAt: this.createdAt ?? Date.now(),
|
|
2349
|
+
updatedAt: Date.now()
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2226
2352
|
/**
|
|
2227
2353
|
* Emit a connection state change event
|
|
2228
2354
|
* @private
|
|
@@ -2230,13 +2356,13 @@ var MCPClient = class {
|
|
|
2230
2356
|
emitStateChange(newState) {
|
|
2231
2357
|
const previousState = this.currentState;
|
|
2232
2358
|
this.currentState = newState;
|
|
2233
|
-
if (!this.serverId) return;
|
|
2359
|
+
if (!this.config.serverId) return;
|
|
2234
2360
|
this._onConnectionEvent.fire({
|
|
2235
2361
|
type: "state_changed",
|
|
2236
|
-
sessionId: this.sessionId,
|
|
2237
|
-
serverId: this.serverId,
|
|
2238
|
-
serverName: this.serverName || this.serverId,
|
|
2239
|
-
serverUrl: this.serverUrl || "",
|
|
2362
|
+
sessionId: this.config.sessionId,
|
|
2363
|
+
serverId: this.config.serverId,
|
|
2364
|
+
serverName: this.config.serverName || this.config.serverId,
|
|
2365
|
+
serverUrl: this.config.serverUrl || "",
|
|
2240
2366
|
createdAt: this.createdAt,
|
|
2241
2367
|
state: newState,
|
|
2242
2368
|
previousState,
|
|
@@ -2247,8 +2373,8 @@ var MCPClient = class {
|
|
|
2247
2373
|
level: "info",
|
|
2248
2374
|
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
2249
2375
|
displayMessage: `State changed to ${newState}`,
|
|
2250
|
-
sessionId: this.sessionId,
|
|
2251
|
-
serverId: this.serverId,
|
|
2376
|
+
sessionId: this.config.sessionId,
|
|
2377
|
+
serverId: this.config.serverId,
|
|
2252
2378
|
payload: { previousState, newState },
|
|
2253
2379
|
timestamp: Date.now(),
|
|
2254
2380
|
id: nanoid()
|
|
@@ -2259,11 +2385,11 @@ var MCPClient = class {
|
|
|
2259
2385
|
* @private
|
|
2260
2386
|
*/
|
|
2261
2387
|
emitError(error, errorType = "unknown") {
|
|
2262
|
-
if (!this.serverId) return;
|
|
2388
|
+
if (!this.config.serverId) return;
|
|
2263
2389
|
this._onConnectionEvent.fire({
|
|
2264
2390
|
type: "error",
|
|
2265
|
-
sessionId: this.sessionId,
|
|
2266
|
-
serverId: this.serverId,
|
|
2391
|
+
sessionId: this.config.sessionId,
|
|
2392
|
+
serverId: this.config.serverId,
|
|
2267
2393
|
error,
|
|
2268
2394
|
errorType,
|
|
2269
2395
|
timestamp: Date.now()
|
|
@@ -2273,8 +2399,8 @@ var MCPClient = class {
|
|
|
2273
2399
|
level: "error",
|
|
2274
2400
|
message: error,
|
|
2275
2401
|
displayMessage: error,
|
|
2276
|
-
sessionId: this.sessionId,
|
|
2277
|
-
serverId: this.serverId,
|
|
2402
|
+
sessionId: this.config.sessionId,
|
|
2403
|
+
serverId: this.config.serverId,
|
|
2278
2404
|
payload: { errorType, error },
|
|
2279
2405
|
timestamp: Date.now(),
|
|
2280
2406
|
id: nanoid()
|
|
@@ -2285,11 +2411,11 @@ var MCPClient = class {
|
|
|
2285
2411
|
* @private
|
|
2286
2412
|
*/
|
|
2287
2413
|
emitProgress(message) {
|
|
2288
|
-
if (!this.serverId) return;
|
|
2414
|
+
if (!this.config.serverId) return;
|
|
2289
2415
|
this._onConnectionEvent.fire({
|
|
2290
2416
|
type: "progress",
|
|
2291
|
-
sessionId: this.sessionId,
|
|
2292
|
-
serverId: this.serverId,
|
|
2417
|
+
sessionId: this.config.sessionId,
|
|
2418
|
+
serverId: this.config.serverId,
|
|
2293
2419
|
message,
|
|
2294
2420
|
timestamp: Date.now()
|
|
2295
2421
|
});
|
|
@@ -2307,14 +2433,14 @@ var MCPClient = class {
|
|
|
2307
2433
|
* @private
|
|
2308
2434
|
*/
|
|
2309
2435
|
getTransport(type) {
|
|
2310
|
-
if (!this.serverUrl) {
|
|
2436
|
+
if (!this.config.serverUrl) {
|
|
2311
2437
|
throw new Error("Server URL is required to create transport");
|
|
2312
2438
|
}
|
|
2313
|
-
const baseUrl = new URL(this.serverUrl);
|
|
2314
|
-
const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2439
|
+
const baseUrl = new URL(this.config.serverUrl);
|
|
2440
|
+
const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2315
2441
|
const transportOptions = {
|
|
2316
2442
|
...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
|
|
2317
|
-
...this.headers && { requestInit: { headers: this.headers } },
|
|
2443
|
+
...this.config.headers && { requestInit: { headers: this.config.headers } },
|
|
2318
2444
|
/**
|
|
2319
2445
|
* Custom fetch implementation to handle connection timeouts.
|
|
2320
2446
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
@@ -2352,78 +2478,74 @@ var MCPClient = class {
|
|
|
2352
2478
|
* @private
|
|
2353
2479
|
*/
|
|
2354
2480
|
async ensureSession() {
|
|
2355
|
-
if (this.oauthProvider)
|
|
2356
|
-
return;
|
|
2357
|
-
}
|
|
2481
|
+
if (this.oauthProvider) return;
|
|
2358
2482
|
this.emitStateChange("INITIALIZING");
|
|
2359
2483
|
this.emitProgress("Loading session configuration...");
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
throw new Error(`Session not found: ${this.sessionId}`);
|
|
2484
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2485
|
+
const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2486
|
+
if (!existingSession2) {
|
|
2487
|
+
throw new Error(`Session not found: ${this.config.sessionId}`);
|
|
2365
2488
|
}
|
|
2366
|
-
this.serverUrl = this.serverUrl ||
|
|
2367
|
-
this.callbackUrl = this.callbackUrl ||
|
|
2368
|
-
this.serverName = this.serverName ||
|
|
2369
|
-
this.serverId = this.serverId ||
|
|
2370
|
-
this.headers = this.headers ||
|
|
2371
|
-
this.createdAt =
|
|
2372
|
-
}
|
|
2373
|
-
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
2489
|
+
this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
|
|
2490
|
+
this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
|
|
2491
|
+
this.config.serverName = this.config.serverName || existingSession2.serverName;
|
|
2492
|
+
this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
|
|
2493
|
+
this.config.headers = this.config.headers || existingSession2.headers;
|
|
2494
|
+
this.createdAt = existingSession2.createdAt;
|
|
2495
|
+
}
|
|
2496
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2374
2497
|
throw new Error("Missing required connection metadata");
|
|
2375
2498
|
}
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
this.
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
this.
|
|
2394
|
-
|
|
2499
|
+
this.oauthProvider = new StorageOAuthClientProvider({
|
|
2500
|
+
userId: this.config.userId,
|
|
2501
|
+
serverId: this.config.serverId,
|
|
2502
|
+
sessionId: this.config.sessionId,
|
|
2503
|
+
redirectUrl: this.config.callbackUrl,
|
|
2504
|
+
clientName: this.config.clientName,
|
|
2505
|
+
clientUri: this.config.clientUri,
|
|
2506
|
+
logoUri: this.config.logoUri,
|
|
2507
|
+
policyUri: this.config.policyUri,
|
|
2508
|
+
clientId: this.config.clientId,
|
|
2509
|
+
clientSecret: this.config.clientSecret,
|
|
2510
|
+
cachedTokens: this.config.cachedCredentials?.tokens,
|
|
2511
|
+
sessionStore: this._store,
|
|
2512
|
+
onRedirect: (redirectUrl) => {
|
|
2513
|
+
if (this.config.serverId) {
|
|
2514
|
+
this._onConnectionEvent.fire({
|
|
2515
|
+
type: "auth_required",
|
|
2516
|
+
sessionId: this.config.sessionId,
|
|
2517
|
+
serverId: this.config.serverId,
|
|
2518
|
+
authUrl: redirectUrl,
|
|
2519
|
+
timestamp: Date.now()
|
|
2520
|
+
});
|
|
2395
2521
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
}
|
|
2401
|
-
|
|
2522
|
+
if (this.config.onRedirect) {
|
|
2523
|
+
this.config.onRedirect(redirectUrl);
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
});
|
|
2527
|
+
const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
|
|
2528
|
+
if (!existingSession) {
|
|
2402
2529
|
this.createdAt = Date.now();
|
|
2403
2530
|
const updatedAt = this.createdAt;
|
|
2404
2531
|
this._onObservabilityEvent.fire({
|
|
2405
2532
|
type: "mcp:client:session_created",
|
|
2406
2533
|
level: "info",
|
|
2407
|
-
message: `Creating pending session ${this.sessionId} for connection setup`,
|
|
2408
|
-
sessionId: this.sessionId,
|
|
2409
|
-
serverId: this.serverId,
|
|
2534
|
+
message: `Creating pending session ${this.config.sessionId} for connection setup`,
|
|
2535
|
+
sessionId: this.config.sessionId,
|
|
2536
|
+
serverId: this.config.serverId,
|
|
2410
2537
|
timestamp: Date.now(),
|
|
2411
2538
|
id: nanoid()
|
|
2412
2539
|
});
|
|
2413
|
-
await
|
|
2414
|
-
|
|
2415
|
-
userId: this.userId,
|
|
2416
|
-
serverId: this.serverId,
|
|
2417
|
-
serverName: this.serverName,
|
|
2418
|
-
serverUrl: this.serverUrl,
|
|
2419
|
-
callbackUrl: this.callbackUrl,
|
|
2420
|
-
transportType: this.transportType || "streamable-http",
|
|
2421
|
-
headers: this.headers,
|
|
2422
|
-
createdAt: this.createdAt,
|
|
2540
|
+
await this._store.create({
|
|
2541
|
+
...this.session,
|
|
2423
2542
|
updatedAt,
|
|
2424
2543
|
status: "pending"
|
|
2425
2544
|
});
|
|
2426
2545
|
}
|
|
2546
|
+
if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
|
|
2547
|
+
await this.oauthProvider.initializeCredentials();
|
|
2548
|
+
}
|
|
2427
2549
|
}
|
|
2428
2550
|
/**
|
|
2429
2551
|
* Saves current session state to the session store
|
|
@@ -2431,34 +2553,14 @@ var MCPClient = class {
|
|
|
2431
2553
|
* @param status - Session lifecycle status used by storage cleanup
|
|
2432
2554
|
* @private
|
|
2433
2555
|
*/
|
|
2434
|
-
async saveSession(status = "active"
|
|
2435
|
-
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2556
|
+
async saveSession(status = "active") {
|
|
2557
|
+
if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
|
|
2436
2558
|
return;
|
|
2437
2559
|
}
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
userId: this.userId,
|
|
2441
|
-
serverId: this.serverId,
|
|
2442
|
-
serverName: this.serverName,
|
|
2443
|
-
serverUrl: this.serverUrl,
|
|
2444
|
-
callbackUrl: this.callbackUrl,
|
|
2445
|
-
transportType: this.transportType || "streamable-http",
|
|
2446
|
-
headers: this.headers,
|
|
2447
|
-
createdAt: this.createdAt || Date.now(),
|
|
2448
|
-
updatedAt: Date.now(),
|
|
2560
|
+
await this._store.update(this.config.userId, this.config.sessionId, {
|
|
2561
|
+
...this.session,
|
|
2449
2562
|
status
|
|
2450
|
-
};
|
|
2451
|
-
if (status === "active") {
|
|
2452
|
-
sessionData.authUrl = null;
|
|
2453
|
-
}
|
|
2454
|
-
if (existingSession === void 0) {
|
|
2455
|
-
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2456
|
-
}
|
|
2457
|
-
if (existingSession) {
|
|
2458
|
-
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2459
|
-
} else {
|
|
2460
|
-
await sessions.create(sessionData);
|
|
2461
|
-
}
|
|
2563
|
+
});
|
|
2462
2564
|
}
|
|
2463
2565
|
/**
|
|
2464
2566
|
* Removes transient setup/auth sessions without masking the original error.
|
|
@@ -2466,7 +2568,7 @@ var MCPClient = class {
|
|
|
2466
2568
|
*/
|
|
2467
2569
|
async deleteTransientSession() {
|
|
2468
2570
|
try {
|
|
2469
|
-
await
|
|
2571
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2470
2572
|
} catch {
|
|
2471
2573
|
}
|
|
2472
2574
|
}
|
|
@@ -2476,7 +2578,7 @@ var MCPClient = class {
|
|
|
2476
2578
|
* @private
|
|
2477
2579
|
*/
|
|
2478
2580
|
async tryConnect() {
|
|
2479
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2581
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2480
2582
|
let lastError;
|
|
2481
2583
|
for (const currentType of transportsToTry) {
|
|
2482
2584
|
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
@@ -2484,6 +2586,7 @@ var MCPClient = class {
|
|
|
2484
2586
|
const transport = this.getTransport(currentType);
|
|
2485
2587
|
this.transport = transport;
|
|
2486
2588
|
await this.client.connect(transport);
|
|
2589
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2487
2590
|
return { transportType: currentType };
|
|
2488
2591
|
} catch (error) {
|
|
2489
2592
|
lastError = error;
|
|
@@ -2499,8 +2602,8 @@ var MCPClient = class {
|
|
|
2499
2602
|
this._onObservabilityEvent.fire({
|
|
2500
2603
|
level: "warn",
|
|
2501
2604
|
message: `Transport ${currentType} failed, falling back`,
|
|
2502
|
-
sessionId: this.sessionId,
|
|
2503
|
-
serverId: this.serverId,
|
|
2605
|
+
sessionId: this.config.sessionId,
|
|
2606
|
+
serverId: this.config.serverId,
|
|
2504
2607
|
metadata: {
|
|
2505
2608
|
failedTransport: currentType,
|
|
2506
2609
|
error: errorMessage
|
|
@@ -2543,15 +2646,15 @@ var MCPClient = class {
|
|
|
2543
2646
|
try {
|
|
2544
2647
|
this.emitStateChange("CONNECTING");
|
|
2545
2648
|
const { transportType } = await this.tryConnect();
|
|
2546
|
-
this.transportType = transportType;
|
|
2649
|
+
this.config.transportType = transportType;
|
|
2547
2650
|
this.emitStateChange("CONNECTED");
|
|
2548
2651
|
this.emitProgress("Connected successfully");
|
|
2549
2652
|
this._onObservabilityEvent.fire({
|
|
2550
2653
|
type: "mcp:client:session_saved",
|
|
2551
2654
|
level: "info",
|
|
2552
|
-
message: `Saving active session ${this.sessionId} (connect success)`,
|
|
2553
|
-
sessionId: this.sessionId,
|
|
2554
|
-
serverId: this.serverId,
|
|
2655
|
+
message: `Saving active session ${this.config.sessionId} (connect success)`,
|
|
2656
|
+
sessionId: this.config.sessionId,
|
|
2657
|
+
serverId: this.config.serverId,
|
|
2555
2658
|
timestamp: Date.now(),
|
|
2556
2659
|
id: nanoid()
|
|
2557
2660
|
});
|
|
@@ -2574,23 +2677,23 @@ var MCPClient = class {
|
|
|
2574
2677
|
this._onObservabilityEvent.fire({
|
|
2575
2678
|
type: "mcp:client:session_saved",
|
|
2576
2679
|
level: "info",
|
|
2577
|
-
message: `Saving pending OAuth session ${this.sessionId}`,
|
|
2578
|
-
sessionId: this.sessionId,
|
|
2579
|
-
serverId: this.serverId,
|
|
2680
|
+
message: `Saving pending OAuth session ${this.config.sessionId}`,
|
|
2681
|
+
sessionId: this.config.sessionId,
|
|
2682
|
+
serverId: this.config.serverId,
|
|
2580
2683
|
timestamp: Date.now(),
|
|
2581
2684
|
id: nanoid()
|
|
2582
2685
|
});
|
|
2583
2686
|
await this.saveSession("pending");
|
|
2584
|
-
if (this.serverId) {
|
|
2687
|
+
if (this.config.serverId) {
|
|
2585
2688
|
this._onConnectionEvent.fire({
|
|
2586
2689
|
type: "auth_required",
|
|
2587
|
-
sessionId: this.sessionId,
|
|
2588
|
-
serverId: this.serverId,
|
|
2690
|
+
sessionId: this.config.sessionId,
|
|
2691
|
+
serverId: this.config.serverId,
|
|
2589
2692
|
authUrl,
|
|
2590
2693
|
timestamp: Date.now()
|
|
2591
2694
|
});
|
|
2592
|
-
if (authUrl && this.onRedirect) {
|
|
2593
|
-
this.onRedirect(authUrl);
|
|
2695
|
+
if (authUrl && this.config.onRedirect) {
|
|
2696
|
+
this.config.onRedirect(authUrl);
|
|
2594
2697
|
}
|
|
2595
2698
|
}
|
|
2596
2699
|
throw new UnauthorizedError("OAuth authorization required");
|
|
@@ -2599,9 +2702,9 @@ var MCPClient = class {
|
|
|
2599
2702
|
this.emitError(errorMessage, "connection");
|
|
2600
2703
|
this.emitStateChange("FAILED");
|
|
2601
2704
|
try {
|
|
2602
|
-
const existingSession = await
|
|
2705
|
+
const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2603
2706
|
if (!existingSession || existingSession.status !== "active") {
|
|
2604
|
-
await
|
|
2707
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2605
2708
|
}
|
|
2606
2709
|
} catch {
|
|
2607
2710
|
}
|
|
@@ -2626,16 +2729,16 @@ var MCPClient = class {
|
|
|
2626
2729
|
throw new Error(error);
|
|
2627
2730
|
}
|
|
2628
2731
|
if (state) {
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2732
|
+
try {
|
|
2733
|
+
await this.oauthProvider.consumeState(state);
|
|
2734
|
+
} catch (error) {
|
|
2735
|
+
const msg = error instanceof Error ? error.message : "Invalid OAuth state";
|
|
2736
|
+
this.emitError(msg, "auth");
|
|
2633
2737
|
this.emitStateChange("FAILED");
|
|
2634
|
-
throw
|
|
2738
|
+
throw error;
|
|
2635
2739
|
}
|
|
2636
|
-
await this.oauthProvider.consumeState(state);
|
|
2637
2740
|
}
|
|
2638
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2741
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2639
2742
|
let lastError;
|
|
2640
2743
|
let tokensExchanged = false;
|
|
2641
2744
|
let authenticatedStateEmitted = false;
|
|
@@ -2662,14 +2765,15 @@ var MCPClient = class {
|
|
|
2662
2765
|
}
|
|
2663
2766
|
}
|
|
2664
2767
|
await this.client.connect(this.transport);
|
|
2665
|
-
this.
|
|
2768
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2769
|
+
this.config.transportType = currentType;
|
|
2666
2770
|
this.emitStateChange("CONNECTED");
|
|
2667
2771
|
this._onObservabilityEvent.fire({
|
|
2668
2772
|
type: "mcp:client:session_saved",
|
|
2669
2773
|
level: "info",
|
|
2670
|
-
message: `Saving active session ${this.sessionId} (OAuth complete)`,
|
|
2671
|
-
sessionId: this.sessionId,
|
|
2672
|
-
serverId: this.serverId,
|
|
2774
|
+
message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
|
|
2775
|
+
sessionId: this.config.sessionId,
|
|
2776
|
+
serverId: this.config.serverId,
|
|
2673
2777
|
timestamp: Date.now(),
|
|
2674
2778
|
id: nanoid()
|
|
2675
2779
|
});
|
|
@@ -2742,11 +2846,11 @@ var MCPClient = class {
|
|
|
2742
2846
|
this.emitStateChange("DISCOVERING");
|
|
2743
2847
|
try {
|
|
2744
2848
|
const result = await this.fetchTools();
|
|
2745
|
-
if (this.serverId) {
|
|
2849
|
+
if (this.config.serverId) {
|
|
2746
2850
|
this._onConnectionEvent.fire({
|
|
2747
2851
|
type: "tools_discovered",
|
|
2748
|
-
sessionId: this.sessionId,
|
|
2749
|
-
serverId: this.serverId,
|
|
2852
|
+
sessionId: this.config.sessionId,
|
|
2853
|
+
serverId: this.config.serverId,
|
|
2750
2854
|
toolCount: result.tools.length,
|
|
2751
2855
|
tools: result.tools,
|
|
2752
2856
|
timestamp: Date.now()
|
|
@@ -2786,8 +2890,8 @@ var MCPClient = class {
|
|
|
2786
2890
|
level: "info",
|
|
2787
2891
|
message: `Tool ${toolName} called successfully`,
|
|
2788
2892
|
displayMessage: `Called tool ${toolName}`,
|
|
2789
|
-
sessionId: this.sessionId,
|
|
2790
|
-
serverId: this.serverId,
|
|
2893
|
+
sessionId: this.config.sessionId,
|
|
2894
|
+
serverId: this.config.serverId,
|
|
2791
2895
|
payload: {
|
|
2792
2896
|
toolName,
|
|
2793
2897
|
args: toolArgs
|
|
@@ -2803,8 +2907,8 @@ var MCPClient = class {
|
|
|
2803
2907
|
level: "error",
|
|
2804
2908
|
message: errorMessage,
|
|
2805
2909
|
displayMessage: `Failed to call tool ${toolName}`,
|
|
2806
|
-
sessionId: this.sessionId,
|
|
2807
|
-
serverId: this.serverId,
|
|
2910
|
+
sessionId: this.config.sessionId,
|
|
2911
|
+
serverId: this.config.serverId,
|
|
2808
2912
|
payload: {
|
|
2809
2913
|
errorType: "tool_execution",
|
|
2810
2914
|
error: errorMessage,
|
|
@@ -2952,8 +3056,8 @@ var MCPClient = class {
|
|
|
2952
3056
|
type: "mcp:client:error",
|
|
2953
3057
|
level: "warn",
|
|
2954
3058
|
message: "Initialization failed during clearSession",
|
|
2955
|
-
sessionId: this.sessionId,
|
|
2956
|
-
serverId: this.serverId,
|
|
3059
|
+
sessionId: this.config.sessionId,
|
|
3060
|
+
serverId: this.config.serverId,
|
|
2957
3061
|
payload: { error: String(error) },
|
|
2958
3062
|
timestamp: Date.now(),
|
|
2959
3063
|
id: nanoid()
|
|
@@ -2962,7 +3066,7 @@ var MCPClient = class {
|
|
|
2962
3066
|
if (this.oauthProvider) {
|
|
2963
3067
|
await this.oauthProvider.invalidateCredentials("all");
|
|
2964
3068
|
}
|
|
2965
|
-
await
|
|
3069
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2966
3070
|
await this.disconnect();
|
|
2967
3071
|
}
|
|
2968
3072
|
/**
|
|
@@ -2996,19 +3100,19 @@ var MCPClient = class {
|
|
|
2996
3100
|
}
|
|
2997
3101
|
this.oauthProvider = null;
|
|
2998
3102
|
this.transport = null;
|
|
2999
|
-
if (this.serverId) {
|
|
3103
|
+
if (this.config.serverId) {
|
|
3000
3104
|
this._onConnectionEvent.fire({
|
|
3001
3105
|
type: "disconnected",
|
|
3002
|
-
sessionId: this.sessionId,
|
|
3003
|
-
serverId: this.serverId,
|
|
3106
|
+
sessionId: this.config.sessionId,
|
|
3107
|
+
serverId: this.config.serverId,
|
|
3004
3108
|
timestamp: Date.now()
|
|
3005
3109
|
});
|
|
3006
3110
|
this._onObservabilityEvent.fire({
|
|
3007
3111
|
type: "mcp:client:disconnect",
|
|
3008
3112
|
level: "info",
|
|
3009
|
-
message: `Disconnected from ${this.serverId}`,
|
|
3010
|
-
sessionId: this.sessionId,
|
|
3011
|
-
serverId: this.serverId,
|
|
3113
|
+
message: `Disconnected from ${this.config.serverId}`,
|
|
3114
|
+
sessionId: this.config.sessionId,
|
|
3115
|
+
serverId: this.config.serverId,
|
|
3012
3116
|
payload: {},
|
|
3013
3117
|
timestamp: Date.now(),
|
|
3014
3118
|
id: nanoid()
|
|
@@ -3033,42 +3137,52 @@ var MCPClient = class {
|
|
|
3033
3137
|
* @returns Server URL or empty string if not set
|
|
3034
3138
|
*/
|
|
3035
3139
|
getServerUrl() {
|
|
3036
|
-
return this.serverUrl || "";
|
|
3140
|
+
return this.config.serverUrl || "";
|
|
3037
3141
|
}
|
|
3038
3142
|
/**
|
|
3039
3143
|
* Gets the OAuth callback URL
|
|
3040
3144
|
* @returns Callback URL or empty string if not set
|
|
3041
3145
|
*/
|
|
3042
3146
|
getCallbackUrl() {
|
|
3043
|
-
return this.callbackUrl || "";
|
|
3147
|
+
return this.config.callbackUrl || "";
|
|
3044
3148
|
}
|
|
3045
3149
|
/**
|
|
3046
3150
|
* Gets the transport type being used
|
|
3047
3151
|
* @returns Transport type (defaults to 'streamable-http')
|
|
3048
3152
|
*/
|
|
3049
3153
|
getTransportType() {
|
|
3050
|
-
return this.transportType || "streamable-http";
|
|
3154
|
+
return this.config.transportType || "streamable-http";
|
|
3155
|
+
}
|
|
3156
|
+
/**
|
|
3157
|
+
* Gets the full server metadata from the MCP initialize response.
|
|
3158
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3159
|
+
* Returns undefined if the client hasn't connected yet.
|
|
3160
|
+
*/
|
|
3161
|
+
getServerInfo() {
|
|
3162
|
+
return this._serverInfo;
|
|
3051
3163
|
}
|
|
3052
3164
|
/**
|
|
3053
|
-
* Gets the human-readable server name
|
|
3165
|
+
* Gets the human-readable server name.
|
|
3166
|
+
* Prefers the server's reported title/name from the initialize response,
|
|
3167
|
+
* falling back to the name provided at construction or session metadata.
|
|
3054
3168
|
* @returns Server name or undefined
|
|
3055
3169
|
*/
|
|
3056
3170
|
getServerName() {
|
|
3057
|
-
return this.serverName;
|
|
3171
|
+
return this.config.serverName;
|
|
3058
3172
|
}
|
|
3059
3173
|
/**
|
|
3060
3174
|
* Gets the server ID
|
|
3061
3175
|
* @returns Server ID or undefined
|
|
3062
3176
|
*/
|
|
3063
3177
|
getServerId() {
|
|
3064
|
-
return this.serverId;
|
|
3178
|
+
return this.config.serverId;
|
|
3065
3179
|
}
|
|
3066
3180
|
/**
|
|
3067
3181
|
* Gets the session ID
|
|
3068
3182
|
* @returns Session ID
|
|
3069
3183
|
*/
|
|
3070
3184
|
getSessionId() {
|
|
3071
|
-
return this.sessionId;
|
|
3185
|
+
return this.config.sessionId;
|
|
3072
3186
|
}
|
|
3073
3187
|
};
|
|
3074
3188
|
|
|
@@ -3091,6 +3205,14 @@ var ToolPolicyGateway = class {
|
|
|
3091
3205
|
getServerId() {
|
|
3092
3206
|
return this.client.getServerId?.();
|
|
3093
3207
|
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Returns the full server metadata from the underlying client, if available.
|
|
3210
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3211
|
+
* Available only after the client has connected and completed initialization.
|
|
3212
|
+
*/
|
|
3213
|
+
getServerInfo() {
|
|
3214
|
+
return this.client.getServerInfo?.();
|
|
3215
|
+
}
|
|
3094
3216
|
/**
|
|
3095
3217
|
* Returns the human-readable server name from the underlying client, if available.
|
|
3096
3218
|
*/
|
|
@@ -3219,6 +3341,7 @@ var MultiSessionClient = class {
|
|
|
3219
3341
|
__publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
|
|
3220
3342
|
__publicField(this, "userId");
|
|
3221
3343
|
__publicField(this, "options");
|
|
3344
|
+
__publicField(this, "_store");
|
|
3222
3345
|
this.userId = userId;
|
|
3223
3346
|
this.options = {
|
|
3224
3347
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
@@ -3226,6 +3349,7 @@ var MultiSessionClient = class {
|
|
|
3226
3349
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
3227
3350
|
...options
|
|
3228
3351
|
};
|
|
3352
|
+
this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
|
|
3229
3353
|
}
|
|
3230
3354
|
// -----------------------------------------------------------------------
|
|
3231
3355
|
// Public API
|
|
@@ -3309,9 +3433,9 @@ var MultiSessionClient = class {
|
|
|
3309
3433
|
* to querying the storage backend via `sessions.list(userId)`.
|
|
3310
3434
|
*/
|
|
3311
3435
|
async fetchActiveSessions() {
|
|
3312
|
-
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await
|
|
3436
|
+
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
|
|
3313
3437
|
return sessionList.filter(
|
|
3314
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
3438
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
|
|
3315
3439
|
);
|
|
3316
3440
|
}
|
|
3317
3441
|
/**
|
|
@@ -3384,8 +3508,13 @@ var MultiSessionClient = class {
|
|
|
3384
3508
|
callbackUrl: session.callbackUrl,
|
|
3385
3509
|
serverName: session.serverName,
|
|
3386
3510
|
transportType: session.transportType,
|
|
3387
|
-
headers: session.headers
|
|
3511
|
+
headers: session.headers,
|
|
3512
|
+
hasSession: true,
|
|
3513
|
+
sessionStore: this._store
|
|
3388
3514
|
});
|
|
3515
|
+
if (this.options.onObservabilityEvent) {
|
|
3516
|
+
client.onObservabilityEvent(this.options.onObservabilityEvent);
|
|
3517
|
+
}
|
|
3389
3518
|
const timeoutMs = this.options.timeout;
|
|
3390
3519
|
let timeoutTimer;
|
|
3391
3520
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3440,130 +3569,188 @@ function isConnectionEvent(event) {
|
|
|
3440
3569
|
}
|
|
3441
3570
|
|
|
3442
3571
|
// src/server/handlers/sse-handler.ts
|
|
3443
|
-
var
|
|
3572
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
3444
3573
|
function normalizeHeaders(headers) {
|
|
3445
3574
|
if (!headers || typeof headers !== "object") return void 0;
|
|
3446
|
-
const entries = Object.entries(headers).map(([
|
|
3575
|
+
const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
|
|
3447
3576
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3448
3577
|
}
|
|
3578
|
+
function validatingStateEvent(sessionId, session) {
|
|
3579
|
+
return {
|
|
3580
|
+
type: "state_changed",
|
|
3581
|
+
sessionId,
|
|
3582
|
+
serverId: session.serverId ?? "unknown",
|
|
3583
|
+
serverName: session.serverName ?? "Unknown",
|
|
3584
|
+
serverUrl: session.serverUrl,
|
|
3585
|
+
state: "VALIDATING",
|
|
3586
|
+
previousState: "DISCONNECTED",
|
|
3587
|
+
timestamp: Date.now()
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
function connectionErrorEvent(sessionId, serverId, error, errorType) {
|
|
3591
|
+
return {
|
|
3592
|
+
type: "error",
|
|
3593
|
+
sessionId,
|
|
3594
|
+
serverId: serverId ?? "unknown",
|
|
3595
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
3596
|
+
errorType,
|
|
3597
|
+
timestamp: Date.now()
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3449
3600
|
var SSEConnectionManager = class {
|
|
3601
|
+
// -----------------------------------------------------------------------
|
|
3602
|
+
// Constructor
|
|
3603
|
+
// -----------------------------------------------------------------------
|
|
3604
|
+
/**
|
|
3605
|
+
* @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
|
|
3606
|
+
* @param sendEvent - Callback that writes a typed event onto the SSE stream.
|
|
3607
|
+
*/
|
|
3450
3608
|
constructor(options, sendEvent) {
|
|
3451
3609
|
this.options = options;
|
|
3452
3610
|
this.sendEvent = sendEvent;
|
|
3611
|
+
// -----------------------------------------------------------------------
|
|
3612
|
+
// State
|
|
3613
|
+
// -----------------------------------------------------------------------
|
|
3453
3614
|
__publicField(this, "userId");
|
|
3615
|
+
/** Active MCP transports keyed by sessionId. */
|
|
3454
3616
|
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
3617
|
+
/** Instrumented session store — always wraps `sessions` with DB observability. */
|
|
3618
|
+
__publicField(this, "observedStore");
|
|
3455
3619
|
__publicField(this, "heartbeatTimer");
|
|
3456
|
-
__publicField(this, "
|
|
3620
|
+
__publicField(this, "active", true);
|
|
3457
3621
|
this.userId = options.userId;
|
|
3622
|
+
this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
|
|
3458
3623
|
this.startHeartbeat();
|
|
3459
3624
|
}
|
|
3625
|
+
// -----------------------------------------------------------------------
|
|
3626
|
+
// Public API
|
|
3627
|
+
// -----------------------------------------------------------------------
|
|
3460
3628
|
/**
|
|
3461
|
-
*
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
}
|
|
3472
|
-
return metadata;
|
|
3473
|
-
}
|
|
3474
|
-
/**
|
|
3475
|
-
* Start heartbeat to keep connection alive
|
|
3476
|
-
*/
|
|
3477
|
-
startHeartbeat() {
|
|
3478
|
-
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
3479
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3480
|
-
if (this.isActive) {
|
|
3481
|
-
this.sendEvent({
|
|
3482
|
-
level: "debug",
|
|
3483
|
-
message: "heartbeat",
|
|
3484
|
-
timestamp: Date.now()
|
|
3485
|
-
});
|
|
3486
|
-
}
|
|
3487
|
-
}, interval);
|
|
3488
|
-
}
|
|
3489
|
-
/**
|
|
3490
|
-
* Handle incoming RPC requests
|
|
3491
|
-
* Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
|
|
3629
|
+
* Dispatches an incoming RPC request, emits timing observability,
|
|
3630
|
+
* and returns the response.
|
|
3631
|
+
*
|
|
3632
|
+
* Emits `rpc:start` before dispatch and `rpc:end` on completion
|
|
3633
|
+
* (success or error), each carrying the method name, sessionId, and
|
|
3634
|
+
* duration. All events flow through the unified `onObservability`
|
|
3635
|
+
* and the SSE stream.
|
|
3636
|
+
*
|
|
3637
|
+
* @param request - The deserialized RPC envelope.
|
|
3638
|
+
* @returns The RPC response (success or error).
|
|
3492
3639
|
*/
|
|
3493
3640
|
async handleRequest(request) {
|
|
3641
|
+
const method = request.method;
|
|
3642
|
+
const sessionId = request.params?.sessionId;
|
|
3643
|
+
const t0 = performance.now();
|
|
3644
|
+
this.sendEvent({
|
|
3645
|
+
type: "rpc:start",
|
|
3646
|
+
level: "debug",
|
|
3647
|
+
message: method,
|
|
3648
|
+
sessionId,
|
|
3649
|
+
timestamp: Date.now()
|
|
3650
|
+
});
|
|
3494
3651
|
try {
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
break;
|
|
3506
|
-
case "disconnect":
|
|
3507
|
-
result = await this.disconnect(request.params);
|
|
3508
|
-
break;
|
|
3509
|
-
case "listTools":
|
|
3510
|
-
result = await this.listTools(request.params);
|
|
3511
|
-
break;
|
|
3512
|
-
case "setToolPolicy":
|
|
3513
|
-
result = await this.setToolPolicy(request.params);
|
|
3514
|
-
break;
|
|
3515
|
-
case "getToolPolicy":
|
|
3516
|
-
result = await this.getToolPolicy(request.params);
|
|
3517
|
-
break;
|
|
3518
|
-
case "callTool":
|
|
3519
|
-
result = await this.callTool(request.params);
|
|
3520
|
-
break;
|
|
3521
|
-
case "getSession":
|
|
3522
|
-
result = await this.getSession(request.params);
|
|
3523
|
-
break;
|
|
3524
|
-
case "finishAuth":
|
|
3525
|
-
result = await this.finishAuth(request.params);
|
|
3526
|
-
break;
|
|
3527
|
-
case "listPrompts":
|
|
3528
|
-
result = await this.listPrompts(request.params);
|
|
3529
|
-
break;
|
|
3530
|
-
case "getPrompt":
|
|
3531
|
-
result = await this.getPrompt(request.params);
|
|
3532
|
-
break;
|
|
3533
|
-
case "listResources":
|
|
3534
|
-
result = await this.listResources(request.params);
|
|
3535
|
-
break;
|
|
3536
|
-
case "readResource":
|
|
3537
|
-
result = await this.readResource(request.params);
|
|
3538
|
-
break;
|
|
3539
|
-
default:
|
|
3540
|
-
throw new Error(`Unknown method: ${request.method}`);
|
|
3541
|
-
}
|
|
3542
|
-
const response = {
|
|
3543
|
-
id: request.id,
|
|
3544
|
-
result
|
|
3545
|
-
};
|
|
3652
|
+
const result = await this.dispatchImpl(request);
|
|
3653
|
+
this.sendEvent({
|
|
3654
|
+
type: "rpc:end",
|
|
3655
|
+
level: "debug",
|
|
3656
|
+
message: method,
|
|
3657
|
+
sessionId,
|
|
3658
|
+
payload: { durationMs: performance.now() - t0 },
|
|
3659
|
+
timestamp: Date.now()
|
|
3660
|
+
});
|
|
3661
|
+
const response = { id: request.id, result };
|
|
3546
3662
|
this.sendEvent(response);
|
|
3547
3663
|
return response;
|
|
3548
3664
|
} catch (error) {
|
|
3549
|
-
|
|
3665
|
+
this.sendEvent({
|
|
3666
|
+
type: "rpc:end",
|
|
3667
|
+
level: "error",
|
|
3668
|
+
message: method,
|
|
3669
|
+
sessionId,
|
|
3670
|
+
payload: { durationMs: performance.now() - t0, error: String(error) },
|
|
3671
|
+
timestamp: Date.now()
|
|
3672
|
+
});
|
|
3673
|
+
const response = {
|
|
3550
3674
|
id: request.id,
|
|
3551
3675
|
error: {
|
|
3552
3676
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
3553
3677
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
3554
3678
|
}
|
|
3555
3679
|
};
|
|
3556
|
-
this.sendEvent(
|
|
3557
|
-
return
|
|
3680
|
+
this.sendEvent(response);
|
|
3681
|
+
return response;
|
|
3682
|
+
}
|
|
3683
|
+
}
|
|
3684
|
+
/**
|
|
3685
|
+
* Tears down all active MCP transports and stops the heartbeat timer.
|
|
3686
|
+
*
|
|
3687
|
+
* Disconnects are issued in parallel across all sessions. After calling
|
|
3688
|
+
* this method the manager instance should be discarded.
|
|
3689
|
+
*/
|
|
3690
|
+
async dispose() {
|
|
3691
|
+
this.active = false;
|
|
3692
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
3693
|
+
await Promise.all(
|
|
3694
|
+
Array.from(this.clients.values()).map((c) => c.disconnect())
|
|
3695
|
+
);
|
|
3696
|
+
this.clients.clear();
|
|
3697
|
+
}
|
|
3698
|
+
// -----------------------------------------------------------------------
|
|
3699
|
+
// RPC Dispatch (raw — called by handleRequest which adds timing)
|
|
3700
|
+
// -----------------------------------------------------------------------
|
|
3701
|
+
/**
|
|
3702
|
+
* Routes an RPC method name to the appropriate private handler.
|
|
3703
|
+
*
|
|
3704
|
+
* @throws {Error} When the method name is unrecognized.
|
|
3705
|
+
*/
|
|
3706
|
+
async dispatchImpl(request) {
|
|
3707
|
+
switch (request.method) {
|
|
3708
|
+
case "listSessions":
|
|
3709
|
+
return this.listSessions();
|
|
3710
|
+
case "connect":
|
|
3711
|
+
return this.connect(request.params);
|
|
3712
|
+
case "reconnect":
|
|
3713
|
+
return this.reconnect(request.params);
|
|
3714
|
+
case "disconnect":
|
|
3715
|
+
return this.disconnect(request.params);
|
|
3716
|
+
case "listTools":
|
|
3717
|
+
return this.listTools(request.params);
|
|
3718
|
+
case "setToolPolicy":
|
|
3719
|
+
return this.setToolPolicy(request.params);
|
|
3720
|
+
case "getToolPolicy":
|
|
3721
|
+
return this.getToolPolicy(request.params);
|
|
3722
|
+
case "updateSession":
|
|
3723
|
+
return this.updateSession(request.params);
|
|
3724
|
+
case "callTool":
|
|
3725
|
+
return this.callTool(request.params);
|
|
3726
|
+
case "getSession":
|
|
3727
|
+
return this.getSession(request.params);
|
|
3728
|
+
case "finishAuth":
|
|
3729
|
+
return this.finishAuth(request.params);
|
|
3730
|
+
case "listPrompts":
|
|
3731
|
+
return this.listPrompts(request.params);
|
|
3732
|
+
case "getPrompt":
|
|
3733
|
+
return this.getPrompt(request.params);
|
|
3734
|
+
case "listResources":
|
|
3735
|
+
return this.listResources(request.params);
|
|
3736
|
+
case "readResource":
|
|
3737
|
+
return this.readResource(request.params);
|
|
3738
|
+
default:
|
|
3739
|
+
throw new Error(`Unknown RPC method: ${request.method}`);
|
|
3558
3740
|
}
|
|
3559
3741
|
}
|
|
3742
|
+
// -----------------------------------------------------------------------
|
|
3743
|
+
// Session Query
|
|
3744
|
+
// -----------------------------------------------------------------------
|
|
3560
3745
|
/**
|
|
3561
|
-
*
|
|
3746
|
+
* Lists all sessions owned by the current user.
|
|
3747
|
+
*
|
|
3748
|
+
* Returns a lightweight view — session metadata only, no credential fields.
|
|
3562
3749
|
*/
|
|
3563
3750
|
async listSessions() {
|
|
3564
|
-
const
|
|
3751
|
+
const all = await sessions.list(this.userId);
|
|
3565
3752
|
return {
|
|
3566
|
-
sessions:
|
|
3753
|
+
sessions: all.map((s) => ({
|
|
3567
3754
|
sessionId: s.sessionId,
|
|
3568
3755
|
serverId: s.serverId,
|
|
3569
3756
|
serverName: s.serverName,
|
|
@@ -3572,215 +3759,203 @@ var SSEConnectionManager = class {
|
|
|
3572
3759
|
createdAt: s.createdAt,
|
|
3573
3760
|
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3574
3761
|
status: s.status ?? "pending",
|
|
3575
|
-
toolPolicy: s.toolPolicy
|
|
3762
|
+
toolPolicy: s.toolPolicy,
|
|
3763
|
+
enabled: s.enabled ?? true
|
|
3576
3764
|
}))
|
|
3577
3765
|
};
|
|
3578
3766
|
}
|
|
3579
3767
|
/**
|
|
3580
|
-
*
|
|
3768
|
+
* Restores and validates a previously persisted session.
|
|
3769
|
+
*
|
|
3770
|
+
* Loads the full session row (including credentials) from storage, creates
|
|
3771
|
+
* a new MCP transport, connects to the remote server, and emits the
|
|
3772
|
+
* discovered (policy-filtered) tool list via SSE.
|
|
3581
3773
|
*/
|
|
3582
|
-
async
|
|
3583
|
-
const
|
|
3584
|
-
|
|
3585
|
-
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
|
|
3586
|
-
const existingSessions = await sessions.list(this.userId);
|
|
3587
|
-
const duplicate = existingSessions.find(
|
|
3588
|
-
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
3589
|
-
);
|
|
3590
|
-
if (duplicate) {
|
|
3591
|
-
if (duplicate.status === "pending") {
|
|
3592
|
-
await this.getSession({ sessionId: duplicate.sessionId });
|
|
3593
|
-
return {
|
|
3594
|
-
sessionId: duplicate.sessionId,
|
|
3595
|
-
success: true
|
|
3596
|
-
};
|
|
3597
|
-
}
|
|
3598
|
-
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
3599
|
-
}
|
|
3600
|
-
const sessionId = await sessions.generateSessionId();
|
|
3774
|
+
async getSession(params) {
|
|
3775
|
+
const session = await this.requireSession(params.sessionId);
|
|
3776
|
+
this.sendEvent(validatingStateEvent(params.sessionId, session));
|
|
3601
3777
|
try {
|
|
3602
|
-
const
|
|
3603
|
-
|
|
3604
|
-
userId: this.userId,
|
|
3605
|
-
sessionId,
|
|
3606
|
-
serverId,
|
|
3607
|
-
serverName,
|
|
3608
|
-
serverUrl,
|
|
3609
|
-
callbackUrl,
|
|
3610
|
-
transportType,
|
|
3611
|
-
headers,
|
|
3612
|
-
clientId,
|
|
3613
|
-
clientSecret,
|
|
3614
|
-
...clientMetadata
|
|
3615
|
-
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
3616
|
-
});
|
|
3617
|
-
this.clients.set(sessionId, client);
|
|
3618
|
-
client.onConnectionEvent((event) => {
|
|
3619
|
-
this.emitConnectionEvent(event);
|
|
3620
|
-
});
|
|
3621
|
-
client.onObservabilityEvent((event) => {
|
|
3622
|
-
this.sendEvent(event);
|
|
3623
|
-
});
|
|
3778
|
+
const client = this.restoreClient(session);
|
|
3779
|
+
this.attachClientEvents(client);
|
|
3624
3780
|
await client.connect();
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
success: true
|
|
3629
|
-
};
|
|
3781
|
+
this.clients.set(params.sessionId, client);
|
|
3782
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3783
|
+
return { success: true, toolCount: result.tools.length };
|
|
3630
3784
|
} catch (error) {
|
|
3631
|
-
|
|
3632
|
-
this.clients.delete(sessionId);
|
|
3633
|
-
return {
|
|
3634
|
-
sessionId,
|
|
3635
|
-
success: true
|
|
3636
|
-
};
|
|
3637
|
-
}
|
|
3638
|
-
this.emitConnectionEvent({
|
|
3639
|
-
type: "error",
|
|
3640
|
-
sessionId,
|
|
3641
|
-
serverId,
|
|
3642
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3643
|
-
errorType: "connection",
|
|
3644
|
-
timestamp: Date.now()
|
|
3645
|
-
});
|
|
3646
|
-
this.clients.delete(sessionId);
|
|
3785
|
+
this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
|
|
3647
3786
|
throw error;
|
|
3648
3787
|
}
|
|
3649
3788
|
}
|
|
3789
|
+
// -----------------------------------------------------------------------
|
|
3790
|
+
// Connection Lifecycle
|
|
3791
|
+
// -----------------------------------------------------------------------
|
|
3650
3792
|
/**
|
|
3651
|
-
*
|
|
3652
|
-
*
|
|
3793
|
+
* Initiates a connection to a new MCP server.
|
|
3794
|
+
*
|
|
3795
|
+
* If a session for the same `serverId` or `serverUrl` already exists and
|
|
3796
|
+
* is still in a `pending` (OAuth) state, the existing session is resumed
|
|
3797
|
+
* instead of creating a duplicate.
|
|
3798
|
+
*
|
|
3799
|
+
* `UnauthorizedError` is treated as a pending-auth state and returned as
|
|
3800
|
+
* a successful result (the client will then redirect to the auth URL).
|
|
3653
3801
|
*/
|
|
3654
|
-
async
|
|
3655
|
-
const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
|
|
3802
|
+
async connect(params) {
|
|
3656
3803
|
const headers = normalizeHeaders(params.headers);
|
|
3657
|
-
const serverId =
|
|
3658
|
-
const
|
|
3659
|
-
|
|
3660
|
-
(
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
if (existingClient) {
|
|
3666
|
-
await existingClient.disconnect();
|
|
3667
|
-
this.clients.delete(duplicate.sessionId);
|
|
3804
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3805
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3806
|
+
if (existing) {
|
|
3807
|
+
if (existing.status === "pending") {
|
|
3808
|
+
return this.getSession({ sessionId: existing.sessionId }).then(() => ({
|
|
3809
|
+
sessionId: existing.sessionId,
|
|
3810
|
+
success: true
|
|
3811
|
+
}));
|
|
3668
3812
|
}
|
|
3813
|
+
throw new Error(
|
|
3814
|
+
`Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
|
|
3815
|
+
);
|
|
3669
3816
|
}
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3817
|
+
const sessionId = await sessions.generateSessionId();
|
|
3818
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3819
|
+
const client = new MCPClient({
|
|
3820
|
+
userId: this.userId,
|
|
3821
|
+
sessionId,
|
|
3822
|
+
serverId,
|
|
3823
|
+
serverName: params.serverName,
|
|
3824
|
+
serverUrl: params.serverUrl,
|
|
3825
|
+
callbackUrl: params.callbackUrl,
|
|
3826
|
+
transportType: params.transportType,
|
|
3827
|
+
headers,
|
|
3828
|
+
sessionStore: this.observedStore,
|
|
3829
|
+
...metadata
|
|
3830
|
+
});
|
|
3831
|
+
this.cacheClient(sessionId, client);
|
|
3832
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3833
|
+
}
|
|
3834
|
+
/**
|
|
3835
|
+
* Reconnects to an MCP server by tearing down the active transport if one
|
|
3836
|
+
* exists and instantiating a fresh connection, reusing the existing session
|
|
3837
|
+
* (and its stored credentials / DCR client info) from the database.
|
|
3838
|
+
*/
|
|
3839
|
+
async reconnect(params) {
|
|
3840
|
+
const headers = normalizeHeaders(params.headers);
|
|
3841
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3842
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3843
|
+
const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
|
|
3844
|
+
if (existing) {
|
|
3845
|
+
const staleClient = this.clients.get(existing.sessionId);
|
|
3846
|
+
if (staleClient) {
|
|
3847
|
+
await staleClient.disconnect();
|
|
3848
|
+
this.clients.delete(existing.sessionId);
|
|
3699
3849
|
}
|
|
3700
|
-
this.emitConnectionEvent({
|
|
3701
|
-
type: "error",
|
|
3702
|
-
sessionId,
|
|
3703
|
-
serverId,
|
|
3704
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3705
|
-
errorType: "connection",
|
|
3706
|
-
timestamp: Date.now()
|
|
3707
|
-
});
|
|
3708
|
-
this.clients.delete(sessionId);
|
|
3709
|
-
throw error;
|
|
3710
3850
|
}
|
|
3851
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3852
|
+
const client = new MCPClient({
|
|
3853
|
+
userId: this.userId,
|
|
3854
|
+
sessionId,
|
|
3855
|
+
serverId,
|
|
3856
|
+
serverName: params.serverName,
|
|
3857
|
+
serverUrl: params.serverUrl,
|
|
3858
|
+
callbackUrl: params.callbackUrl,
|
|
3859
|
+
transportType: params.transportType,
|
|
3860
|
+
headers,
|
|
3861
|
+
sessionStore: this.observedStore,
|
|
3862
|
+
...metadata
|
|
3863
|
+
});
|
|
3864
|
+
this.cacheClient(sessionId, client);
|
|
3865
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3711
3866
|
}
|
|
3712
3867
|
/**
|
|
3713
|
-
*
|
|
3868
|
+
* Disconnects from an MCP server.
|
|
3869
|
+
*
|
|
3870
|
+
* If an active in-memory transport exists it delegates to `clearSession()`
|
|
3871
|
+
* (which sends the server an HTTP DELETE per the Streamable spec). If no
|
|
3872
|
+
* active transport is available (orphaned session from a failed OAuth flow),
|
|
3873
|
+
* the session row is removed directly from storage.
|
|
3714
3874
|
*/
|
|
3715
3875
|
async disconnect(params) {
|
|
3716
|
-
const
|
|
3717
|
-
const client = this.clients.get(sessionId);
|
|
3876
|
+
const client = this.clients.get(params.sessionId);
|
|
3718
3877
|
if (client) {
|
|
3719
3878
|
await client.clearSession();
|
|
3720
|
-
this.clients.delete(sessionId);
|
|
3879
|
+
this.clients.delete(params.sessionId);
|
|
3721
3880
|
} else {
|
|
3722
|
-
await sessions.delete(this.userId, sessionId);
|
|
3881
|
+
await sessions.delete(this.userId, params.sessionId);
|
|
3723
3882
|
}
|
|
3724
3883
|
return { success: true };
|
|
3725
3884
|
}
|
|
3885
|
+
// -----------------------------------------------------------------------
|
|
3886
|
+
// OAuth
|
|
3887
|
+
// -----------------------------------------------------------------------
|
|
3726
3888
|
/**
|
|
3727
|
-
*
|
|
3889
|
+
* Completes the OAuth 2.1 authorization code flow for a pending session.
|
|
3890
|
+
*
|
|
3891
|
+
* Loads the stored session (with credentials), creates a fresh `MCPClient`,
|
|
3892
|
+
* and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
|
|
3893
|
+
* so the PKCE code verifier is available without a DB read.
|
|
3894
|
+
*
|
|
3895
|
+
* The session's stored `transportType` is intentionally **omitted** from the
|
|
3896
|
+
* client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
|
|
3897
|
+
* first, fall back to SSE), which is required for servers like Neon that
|
|
3898
|
+
* only support SSE transport.
|
|
3728
3899
|
*/
|
|
3729
|
-
async
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3900
|
+
async finishAuth(params) {
|
|
3901
|
+
const parsed = parseOAuthState(params.state);
|
|
3902
|
+
const sessionId = parsed?.sessionId ?? params.state;
|
|
3903
|
+
const session = await this.requireSession(sessionId);
|
|
3904
|
+
try {
|
|
3905
|
+
const clientId = session.clientId ?? void 0;
|
|
3906
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
3907
|
+
const client = new MCPClient({
|
|
3908
|
+
userId: this.userId,
|
|
3909
|
+
sessionId,
|
|
3910
|
+
serverId: session.serverId,
|
|
3911
|
+
serverName: session.serverName,
|
|
3912
|
+
serverUrl: session.serverUrl,
|
|
3913
|
+
callbackUrl: session.callbackUrl,
|
|
3914
|
+
headers: session.headers,
|
|
3915
|
+
clientId,
|
|
3916
|
+
clientSecret,
|
|
3917
|
+
hasSession: true,
|
|
3918
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
3919
|
+
sessionStore: this.observedStore
|
|
3920
|
+
});
|
|
3921
|
+
this.attachClientEvents(client);
|
|
3922
|
+
await runWithCodeVerifierState(
|
|
3923
|
+
session.codeVerifier ?? "",
|
|
3924
|
+
"S256",
|
|
3925
|
+
() => client.finishAuth(params.code, params.state)
|
|
3926
|
+
);
|
|
3927
|
+
this.clients.set(sessionId, client);
|
|
3928
|
+
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
3929
|
+
return { success: true, toolCount: result.tools.length };
|
|
3930
|
+
} catch (error) {
|
|
3931
|
+
this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
|
|
3932
|
+
throw error;
|
|
3737
3933
|
}
|
|
3738
|
-
const client = new MCPClient({
|
|
3739
|
-
userId: this.userId,
|
|
3740
|
-
sessionId,
|
|
3741
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3742
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3743
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3744
|
-
// reloading during initialize().
|
|
3745
|
-
serverId: session.serverId,
|
|
3746
|
-
serverName: session.serverName,
|
|
3747
|
-
serverUrl: session.serverUrl,
|
|
3748
|
-
callbackUrl: session.callbackUrl,
|
|
3749
|
-
transportType: session.transportType,
|
|
3750
|
-
headers: session.headers
|
|
3751
|
-
});
|
|
3752
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3753
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3754
|
-
await client.connect();
|
|
3755
|
-
this.clients.set(sessionId, client);
|
|
3756
|
-
return client;
|
|
3757
3934
|
}
|
|
3935
|
+
// -----------------------------------------------------------------------
|
|
3936
|
+
// Tool Discovery & Policy
|
|
3937
|
+
// -----------------------------------------------------------------------
|
|
3758
3938
|
/**
|
|
3759
|
-
* Fetches
|
|
3939
|
+
* Fetches the complete tool list from the remote MCP server and emits a
|
|
3940
|
+
* `tools_discovered` SSE event with two lists:
|
|
3760
3941
|
*
|
|
3761
|
-
*
|
|
3762
|
-
* - `
|
|
3763
|
-
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
3764
|
-
* that blocked tools still appear as checkboxes in the dialog.
|
|
3942
|
+
* - `tools` — policy-filtered (what agents are allowed to call)
|
|
3943
|
+
* - `allTools` — complete, unfiltered (used by the management UI)
|
|
3765
3944
|
*
|
|
3766
|
-
* `fetchTools()`
|
|
3767
|
-
* `gateway.listTools()`
|
|
3768
|
-
* network round-trip is made regardless of how many callers follow.
|
|
3945
|
+
* Only a single network round-trip is made: `fetchTools()` populates the
|
|
3946
|
+
* in-memory cache, then `gateway.listTools()` reuses that cache.
|
|
3769
3947
|
*
|
|
3770
|
-
* @param sessionId - The session
|
|
3771
|
-
* @returns The session record and the
|
|
3772
|
-
* @throws {Error}
|
|
3948
|
+
* @param sessionId - The session to discover tools for.
|
|
3949
|
+
* @returns The session record and the filtered tool result.
|
|
3950
|
+
* @throws {Error} If the session does not exist in storage.
|
|
3773
3951
|
*/
|
|
3774
3952
|
async listPolicyFilteredTools(sessionId) {
|
|
3775
|
-
const session = await
|
|
3776
|
-
if (!session) {
|
|
3777
|
-
throw new Error("Session not found");
|
|
3778
|
-
}
|
|
3953
|
+
const session = await this.requireSession(sessionId);
|
|
3779
3954
|
const client = await this.getOrCreateClient(sessionId);
|
|
3780
3955
|
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
3781
3956
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
3782
3957
|
const result = await gateway.listTools();
|
|
3783
|
-
this.
|
|
3958
|
+
this.sendEvent({
|
|
3784
3959
|
type: "tools_discovered",
|
|
3785
3960
|
sessionId,
|
|
3786
3961
|
serverId: session.serverId ?? "unknown",
|
|
@@ -3792,254 +3967,254 @@ var SSEConnectionManager = class {
|
|
|
3792
3967
|
return { session, result };
|
|
3793
3968
|
}
|
|
3794
3969
|
/**
|
|
3795
|
-
* Returns the policy-filtered tool list for a session
|
|
3796
|
-
*
|
|
3797
|
-
*
|
|
3970
|
+
* Returns the policy-filtered tool list for a session.
|
|
3971
|
+
*
|
|
3972
|
+
* Delegates to {@link listPolicyFilteredTools}, which also emits a
|
|
3973
|
+
* `tools_discovered` SSE event to keep the client state current.
|
|
3798
3974
|
*/
|
|
3799
3975
|
async listTools(params) {
|
|
3800
|
-
const {
|
|
3801
|
-
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
3976
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3802
3977
|
return { tools: result.tools };
|
|
3803
3978
|
}
|
|
3804
3979
|
/**
|
|
3805
|
-
*
|
|
3980
|
+
* Returns all raw tools annotated with their effective policy state
|
|
3981
|
+
* for display in the management UI.
|
|
3806
3982
|
*/
|
|
3807
3983
|
async getToolPolicy(params) {
|
|
3808
|
-
const
|
|
3809
|
-
const
|
|
3810
|
-
if (!session) {
|
|
3811
|
-
throw new Error("Session not found");
|
|
3812
|
-
}
|
|
3813
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
3984
|
+
const session = await this.requireSession(params.sessionId);
|
|
3985
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3814
3986
|
const allTools = await client.fetchTools();
|
|
3815
|
-
const
|
|
3987
|
+
const policy = session.toolPolicy ?? {
|
|
3816
3988
|
mode: "all",
|
|
3817
3989
|
toolIds: [],
|
|
3818
3990
|
updatedAt: session.updatedAt ?? session.createdAt
|
|
3819
3991
|
};
|
|
3820
3992
|
const serverId = session.serverId ?? "unknown";
|
|
3821
|
-
const tools = allTools.tools.map((
|
|
3822
|
-
...
|
|
3823
|
-
toolId: createToolId(serverId,
|
|
3824
|
-
allowed: isToolAllowed(
|
|
3993
|
+
const tools = allTools.tools.map((t) => ({
|
|
3994
|
+
...t,
|
|
3995
|
+
toolId: createToolId(serverId, t.name),
|
|
3996
|
+
allowed: isToolAllowed(policy, t.name, session.serverId)
|
|
3825
3997
|
}));
|
|
3826
3998
|
return {
|
|
3827
|
-
toolPolicy,
|
|
3999
|
+
toolPolicy: policy,
|
|
3828
4000
|
tools,
|
|
3829
4001
|
toolCount: tools.length,
|
|
3830
|
-
allowedToolCount: tools.filter((
|
|
4002
|
+
allowedToolCount: tools.filter((t) => t.allowed).length
|
|
3831
4003
|
};
|
|
3832
4004
|
}
|
|
3833
4005
|
/**
|
|
3834
|
-
* Persists a new tool access policy
|
|
3835
|
-
*
|
|
4006
|
+
* Persists a new tool access policy and broadcasts the updated filtered
|
|
4007
|
+
* tool list via a `tools_discovered` SSE event.
|
|
3836
4008
|
*
|
|
3837
|
-
*
|
|
3838
|
-
*
|
|
3839
|
-
* an additional round-trip to the server.
|
|
3840
|
-
*
|
|
3841
|
-
* @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
|
|
3842
|
-
* @throws {Error} When the session does not exist or the policy references unknown tool IDs.
|
|
4009
|
+
* @throws {Error} If the session does not exist or the policy references
|
|
4010
|
+
* tool IDs that don't match any known tool.
|
|
3843
4011
|
*/
|
|
3844
4012
|
async setToolPolicy(params) {
|
|
3845
|
-
const
|
|
3846
|
-
const
|
|
3847
|
-
if (!session) {
|
|
3848
|
-
throw new Error("Session not found");
|
|
3849
|
-
}
|
|
3850
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4013
|
+
const session = await this.requireSession(params.sessionId);
|
|
4014
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3851
4015
|
const allTools = await client.fetchTools();
|
|
3852
|
-
const
|
|
3853
|
-
validateToolPolicyAgainstTools(
|
|
3854
|
-
await sessions.update(this.userId, sessionId, { toolPolicy });
|
|
3855
|
-
const
|
|
3856
|
-
|
|
4016
|
+
const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
|
|
4017
|
+
validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
|
|
4018
|
+
await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
|
|
4019
|
+
const filtered = createToolPolicyGateway(
|
|
4020
|
+
this.userId,
|
|
4021
|
+
params.sessionId,
|
|
4022
|
+
client
|
|
4023
|
+
).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
|
|
4024
|
+
this.sendEvent({
|
|
3857
4025
|
type: "tools_discovered",
|
|
3858
|
-
sessionId,
|
|
4026
|
+
sessionId: params.sessionId,
|
|
3859
4027
|
serverId: session.serverId ?? "unknown",
|
|
3860
|
-
toolCount:
|
|
3861
|
-
tools:
|
|
4028
|
+
toolCount: filtered.length,
|
|
4029
|
+
tools: filtered,
|
|
3862
4030
|
allTools: allTools.tools,
|
|
3863
4031
|
timestamp: Date.now()
|
|
3864
4032
|
});
|
|
3865
|
-
return {
|
|
3866
|
-
success: true,
|
|
3867
|
-
toolPolicy,
|
|
3868
|
-
tools: filteredTools,
|
|
3869
|
-
toolCount: filteredTools.length
|
|
3870
|
-
};
|
|
3871
|
-
}
|
|
3872
|
-
/**
|
|
3873
|
-
* Proxies a tool invocation to the remote MCP server.
|
|
3874
|
-
* Resolves the client for the given session and delegates to the tool router.
|
|
3875
|
-
*/
|
|
3876
|
-
async callTool(params) {
|
|
3877
|
-
const { sessionId, toolName, toolArgs } = params;
|
|
3878
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
3879
|
-
const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
|
|
3880
|
-
const meta = result._meta || {};
|
|
3881
|
-
return {
|
|
3882
|
-
...result,
|
|
3883
|
-
_meta: {
|
|
3884
|
-
...meta,
|
|
3885
|
-
sessionId
|
|
3886
|
-
}
|
|
3887
|
-
};
|
|
4033
|
+
return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
|
|
3888
4034
|
}
|
|
3889
4035
|
/**
|
|
3890
|
-
*
|
|
4036
|
+
* Enables or disables a session for agent tool discovery.
|
|
4037
|
+
*
|
|
4038
|
+
* Disabled sessions retain their OAuth tokens and connection metadata
|
|
4039
|
+
* but are hidden from `MultiSessionClient.connect()` and blocked from
|
|
4040
|
+
* RPC tool access. Re-enabling does not require re-authentication.
|
|
4041
|
+
*
|
|
4042
|
+
* @param params - `{ sessionId, enabled: boolean }`
|
|
4043
|
+
* @returns `{ success: true }`
|
|
4044
|
+
* @throws {Error} If the session does not exist.
|
|
3891
4045
|
*/
|
|
3892
|
-
async
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
throw new Error("Session not found");
|
|
3897
|
-
}
|
|
3898
|
-
this.emitConnectionEvent({
|
|
3899
|
-
type: "state_changed",
|
|
3900
|
-
sessionId,
|
|
3901
|
-
serverId: session.serverId ?? "unknown",
|
|
3902
|
-
serverName: session.serverName ?? "Unknown",
|
|
3903
|
-
serverUrl: session.serverUrl,
|
|
3904
|
-
state: "VALIDATING",
|
|
3905
|
-
previousState: "DISCONNECTED",
|
|
3906
|
-
timestamp: Date.now()
|
|
3907
|
-
});
|
|
3908
|
-
try {
|
|
3909
|
-
const clientMetadata = await this.getResolvedClientMetadata();
|
|
3910
|
-
const client = new MCPClient({
|
|
3911
|
-
userId: this.userId,
|
|
3912
|
-
sessionId,
|
|
3913
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3914
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3915
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3916
|
-
// reloading during initialize().
|
|
3917
|
-
serverId: session.serverId,
|
|
3918
|
-
serverName: session.serverName,
|
|
3919
|
-
serverUrl: session.serverUrl,
|
|
3920
|
-
callbackUrl: session.callbackUrl,
|
|
3921
|
-
transportType: session.transportType,
|
|
3922
|
-
headers: session.headers,
|
|
3923
|
-
...clientMetadata
|
|
3924
|
-
});
|
|
3925
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3926
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3927
|
-
await client.connect();
|
|
3928
|
-
this.clients.set(sessionId, client);
|
|
3929
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
3930
|
-
return { success: true, toolCount: tools.tools.length };
|
|
3931
|
-
} catch (error) {
|
|
3932
|
-
this.emitConnectionEvent({
|
|
3933
|
-
type: "error",
|
|
3934
|
-
sessionId,
|
|
3935
|
-
serverId: session.serverId ?? "unknown",
|
|
3936
|
-
error: error instanceof Error ? error.message : "Validation failed",
|
|
3937
|
-
errorType: "validation",
|
|
3938
|
-
timestamp: Date.now()
|
|
3939
|
-
});
|
|
3940
|
-
throw error;
|
|
3941
|
-
}
|
|
4046
|
+
async updateSession(params) {
|
|
4047
|
+
await this.requireSession(params.sessionId);
|
|
4048
|
+
await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
|
|
4049
|
+
return { success: true };
|
|
3942
4050
|
}
|
|
4051
|
+
// -----------------------------------------------------------------------
|
|
4052
|
+
// Tool Execution
|
|
4053
|
+
// -----------------------------------------------------------------------
|
|
3943
4054
|
/**
|
|
3944
|
-
*
|
|
4055
|
+
* Proxies a tool invocation to the remote MCP server.
|
|
4056
|
+
*
|
|
4057
|
+
* Resolves (or creates) the transport for the given session, runs the call
|
|
4058
|
+
* through the tool-policy gateway, and injects `sessionId` into the result
|
|
4059
|
+
* metadata so the client can route the response without scanning all sessions.
|
|
3945
4060
|
*/
|
|
3946
|
-
async
|
|
3947
|
-
const
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
}
|
|
3955
|
-
try {
|
|
3956
|
-
const client = new MCPClient({
|
|
3957
|
-
userId: this.userId,
|
|
3958
|
-
sessionId,
|
|
3959
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3960
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3961
|
-
// original connection metadata instead of relying on lazy
|
|
3962
|
-
// reloading during initialize().
|
|
3963
|
-
serverId: session.serverId,
|
|
3964
|
-
serverName: session.serverName,
|
|
3965
|
-
serverUrl: session.serverUrl,
|
|
3966
|
-
callbackUrl: session.callbackUrl,
|
|
3967
|
-
// NOTE: transportType is intentionally omitted here.
|
|
3968
|
-
// The session's stored transportType is a placeholder ('streamable-http')
|
|
3969
|
-
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
3970
|
-
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
3971
|
-
// Neon that only support SSE transport.
|
|
3972
|
-
headers: session.headers
|
|
3973
|
-
});
|
|
3974
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3975
|
-
await client.finishAuth(code, oauthState);
|
|
3976
|
-
this.clients.set(sessionId, client);
|
|
3977
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
3978
|
-
return { success: true, toolCount: tools.tools.length };
|
|
3979
|
-
} catch (error) {
|
|
3980
|
-
this.emitConnectionEvent({
|
|
3981
|
-
type: "error",
|
|
3982
|
-
sessionId,
|
|
3983
|
-
serverId: session.serverId ?? "unknown",
|
|
3984
|
-
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
3985
|
-
errorType: "auth",
|
|
3986
|
-
timestamp: Date.now()
|
|
3987
|
-
});
|
|
3988
|
-
throw error;
|
|
3989
|
-
}
|
|
4061
|
+
async callTool(params) {
|
|
4062
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4063
|
+
const result = await createToolPolicyGateway(
|
|
4064
|
+
this.userId,
|
|
4065
|
+
params.sessionId,
|
|
4066
|
+
client
|
|
4067
|
+
).callTool(params.toolName, params.toolArgs);
|
|
4068
|
+
return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
|
|
3990
4069
|
}
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
4070
|
+
// -----------------------------------------------------------------------
|
|
4071
|
+
// Prompts
|
|
4072
|
+
// -----------------------------------------------------------------------
|
|
4073
|
+
/** Lists all prompts exposed by the remote MCP server. */
|
|
3994
4074
|
async listPrompts(params) {
|
|
3995
|
-
const
|
|
3996
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4075
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3997
4076
|
const result = await client.listPrompts();
|
|
3998
4077
|
return { prompts: result.prompts };
|
|
3999
4078
|
}
|
|
4000
|
-
/**
|
|
4001
|
-
* Get a specific prompt
|
|
4002
|
-
*/
|
|
4079
|
+
/** Retrieves a specific prompt by name with optional arguments. */
|
|
4003
4080
|
async getPrompt(params) {
|
|
4004
|
-
const
|
|
4005
|
-
|
|
4006
|
-
return await client.getPrompt(name, args);
|
|
4081
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4082
|
+
return client.getPrompt(params.name, params.args);
|
|
4007
4083
|
}
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4084
|
+
// -----------------------------------------------------------------------
|
|
4085
|
+
// Resources
|
|
4086
|
+
// -----------------------------------------------------------------------
|
|
4087
|
+
/** Lists all resources exposed by the remote MCP server. */
|
|
4011
4088
|
async listResources(params) {
|
|
4012
|
-
const
|
|
4013
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4089
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4014
4090
|
const result = await client.listResources();
|
|
4015
4091
|
return { resources: result.resources };
|
|
4016
4092
|
}
|
|
4093
|
+
/** Reads a specific resource identified by URI. */
|
|
4094
|
+
async readResource(params) {
|
|
4095
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4096
|
+
return client.readResource(params.uri);
|
|
4097
|
+
}
|
|
4098
|
+
// -----------------------------------------------------------------------
|
|
4099
|
+
// Internal Helpers
|
|
4100
|
+
// -----------------------------------------------------------------------
|
|
4101
|
+
/** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
|
|
4102
|
+
async getResolvedClientMetadata(request) {
|
|
4103
|
+
let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
|
|
4104
|
+
if (this.options.getClientMetadata) {
|
|
4105
|
+
metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
|
|
4106
|
+
}
|
|
4107
|
+
return metadata;
|
|
4108
|
+
}
|
|
4109
|
+
/** Ensures the given session exists in storage and throws otherwise. */
|
|
4110
|
+
async requireSession(sessionId) {
|
|
4111
|
+
const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
|
|
4112
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
4113
|
+
return session;
|
|
4114
|
+
}
|
|
4115
|
+
/** Finds an existing session matching the given serverId or serverUrl. */
|
|
4116
|
+
async findExistingSession(serverId, serverUrl) {
|
|
4117
|
+
const all = await sessions.list(this.userId);
|
|
4118
|
+
return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
|
|
4119
|
+
}
|
|
4120
|
+
/** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
|
|
4121
|
+
normalizeServerId(raw) {
|
|
4122
|
+
return raw && raw.length <= 12 ? raw : generateServerId();
|
|
4123
|
+
}
|
|
4017
4124
|
/**
|
|
4018
|
-
*
|
|
4125
|
+
* Returns the cached in-memory transport for `sessionId`, or creates one
|
|
4126
|
+
* from the persisted session row (with credentials) and connects it.
|
|
4019
4127
|
*/
|
|
4020
|
-
async
|
|
4021
|
-
const
|
|
4022
|
-
|
|
4023
|
-
|
|
4128
|
+
async getOrCreateClient(sessionId) {
|
|
4129
|
+
const existing = this.clients.get(sessionId);
|
|
4130
|
+
if (existing) return existing;
|
|
4131
|
+
const session = await this.requireSession(sessionId);
|
|
4132
|
+
if (session.enabled === false) {
|
|
4133
|
+
throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
|
|
4134
|
+
}
|
|
4135
|
+
const client = this.restoreClient(session);
|
|
4136
|
+
this.attachClientEvents(client);
|
|
4137
|
+
await client.connect();
|
|
4138
|
+
this.clients.set(sessionId, client);
|
|
4139
|
+
return client;
|
|
4140
|
+
}
|
|
4141
|
+
/**
|
|
4142
|
+
* Builds an `MCPClient` from a stored session row.
|
|
4143
|
+
*
|
|
4144
|
+
* Extracts `clientId` and `clientSecret` from the session's credential
|
|
4145
|
+
* fields and passes `hasSession: true` + `cachedCredentials` so the client
|
|
4146
|
+
* can skip redundant existence checks and credential reads.
|
|
4147
|
+
*/
|
|
4148
|
+
restoreClient(session) {
|
|
4149
|
+
const clientId = session.clientId ?? void 0;
|
|
4150
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
4151
|
+
return new MCPClient({
|
|
4152
|
+
userId: this.userId,
|
|
4153
|
+
sessionId: session.sessionId,
|
|
4154
|
+
serverId: session.serverId,
|
|
4155
|
+
serverName: session.serverName,
|
|
4156
|
+
serverUrl: session.serverUrl,
|
|
4157
|
+
callbackUrl: session.callbackUrl,
|
|
4158
|
+
transportType: session.transportType,
|
|
4159
|
+
headers: session.headers,
|
|
4160
|
+
clientId,
|
|
4161
|
+
clientSecret,
|
|
4162
|
+
hasSession: true,
|
|
4163
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
4164
|
+
sessionStore: this.observedStore
|
|
4165
|
+
});
|
|
4024
4166
|
}
|
|
4025
4167
|
/**
|
|
4026
|
-
*
|
|
4168
|
+
* Wires connection and observability events from the client into the
|
|
4169
|
+
* unified observability channel. Connection events go to `sendEvent`
|
|
4170
|
+
* (SSE stream); observability events go to `emitObs` (user callback
|
|
4171
|
+
* + SSE stream).
|
|
4027
4172
|
*/
|
|
4028
|
-
|
|
4029
|
-
this.sendEvent(
|
|
4173
|
+
attachClientEvents(client) {
|
|
4174
|
+
client.onConnectionEvent((e) => this.sendEvent(e));
|
|
4175
|
+
client.onObservabilityEvent((e) => this.sendEvent(e));
|
|
4030
4176
|
}
|
|
4031
4177
|
/**
|
|
4032
|
-
*
|
|
4178
|
+
* Registers the client in the in-memory cache and attaches its event
|
|
4179
|
+
* listeners to the unified observability channel.
|
|
4033
4180
|
*/
|
|
4034
|
-
|
|
4035
|
-
this.
|
|
4036
|
-
|
|
4037
|
-
|
|
4181
|
+
cacheClient(sessionId, client) {
|
|
4182
|
+
this.attachClientEvents(client);
|
|
4183
|
+
this.clients.set(sessionId, client);
|
|
4184
|
+
}
|
|
4185
|
+
/**
|
|
4186
|
+
* Attempts a `client.connect()` and, on success, discovers tools.
|
|
4187
|
+
*
|
|
4188
|
+
* If the server responds with `UnauthorizedError`, the session is
|
|
4189
|
+
* treated as pending OAuth — a successful result is returned with
|
|
4190
|
+
* the sessionId so the browser client can redirect to the auth URL.
|
|
4191
|
+
*
|
|
4192
|
+
* For any other error an `error` connection event is emitted and the
|
|
4193
|
+
* client is removed from the in-memory cache before re-throwing.
|
|
4194
|
+
*/
|
|
4195
|
+
async connectAndDiscover(client, sessionId, serverId) {
|
|
4196
|
+
try {
|
|
4197
|
+
await client.connect();
|
|
4198
|
+
await this.listPolicyFilteredTools(sessionId);
|
|
4199
|
+
return { sessionId, success: true };
|
|
4200
|
+
} catch (error) {
|
|
4201
|
+
if (error instanceof UnauthorizedError) {
|
|
4202
|
+
this.clients.delete(sessionId);
|
|
4203
|
+
return { sessionId, success: true };
|
|
4204
|
+
}
|
|
4205
|
+
this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
|
|
4206
|
+
this.clients.delete(sessionId);
|
|
4207
|
+
throw error;
|
|
4038
4208
|
}
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
this.
|
|
4209
|
+
}
|
|
4210
|
+
/** Starts the periodic heartbeat timer. */
|
|
4211
|
+
startHeartbeat() {
|
|
4212
|
+
const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
4213
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4214
|
+
if (this.active) {
|
|
4215
|
+
this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
|
|
4216
|
+
}
|
|
4217
|
+
}, ms);
|
|
4043
4218
|
}
|
|
4044
4219
|
};
|
|
4045
4220
|
function createSSEHandler(options) {
|
|
@@ -4068,8 +4243,7 @@ function createSSEHandler(options) {
|
|
|
4068
4243
|
});
|
|
4069
4244
|
req.on("end", async () => {
|
|
4070
4245
|
try {
|
|
4071
|
-
|
|
4072
|
-
await manager.handleRequest(request);
|
|
4246
|
+
await manager.handleRequest(JSON.parse(body));
|
|
4073
4247
|
} catch {
|
|
4074
4248
|
}
|
|
4075
4249
|
});
|
|
@@ -4236,6 +4410,6 @@ data: ${JSON.stringify(data)}
|
|
|
4236
4410
|
return { GET, POST };
|
|
4237
4411
|
}
|
|
4238
4412
|
|
|
4239
|
-
export { MCPClient, MultiSessionClient, SSEConnectionManager, StorageOAuthClientProvider, UnauthorizedError, createNextMcpHandler, createSSEHandler, onSessionMutation, sanitizeServerLabel, sessions };
|
|
4413
|
+
export { MCPClient, MultiSessionClient, SSEConnectionManager, StorageOAuthClientProvider, UnauthorizedError, createNextMcpHandler, createSSEHandler, decryptObject, encryptObject, onSessionMutation, sanitizeServerLabel, sessions, withDbObservability };
|
|
4240
4414
|
//# sourceMappingURL=index.mjs.map
|
|
4241
4415
|
//# sourceMappingURL=index.mjs.map
|