@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.js
CHANGED
|
@@ -6,9 +6,10 @@ var streamableHttp_js = require('@modelcontextprotocol/sdk/client/streamableHttp
|
|
|
6
6
|
var sse_js = require('@modelcontextprotocol/sdk/client/sse.js');
|
|
7
7
|
var auth_js = require('@modelcontextprotocol/sdk/client/auth.js');
|
|
8
8
|
var types_js = require('@modelcontextprotocol/sdk/types.js');
|
|
9
|
+
var async_hooks = require('async_hooks');
|
|
9
10
|
var fs2 = require('fs');
|
|
10
11
|
var path = require('path');
|
|
11
|
-
var crypto = require('crypto');
|
|
12
|
+
var crypto$1 = require('crypto');
|
|
12
13
|
|
|
13
14
|
function _interopNamespace(e) {
|
|
14
15
|
if (e && e.__esModule) return e;
|
|
@@ -43,9 +44,9 @@ var __export = (target, all) => {
|
|
|
43
44
|
};
|
|
44
45
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
45
46
|
|
|
46
|
-
// node_modules/tsup/assets/cjs_shims.js
|
|
47
|
+
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
47
48
|
var init_cjs_shims = __esm({
|
|
48
|
-
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
49
|
+
"../../node_modules/tsup/assets/cjs_shims.js"() {
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
52
|
|
|
@@ -325,7 +326,6 @@ var RedisStorageBackend = class {
|
|
|
325
326
|
constructor(redis2) {
|
|
326
327
|
this.redis = redis2;
|
|
327
328
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
328
|
-
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
329
329
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
330
330
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
331
331
|
}
|
|
@@ -344,9 +344,6 @@ var RedisStorageBackend = class {
|
|
|
344
344
|
getSessionKey(userId, sessionId) {
|
|
345
345
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
346
346
|
}
|
|
347
|
-
getCredentialsKey(userId, sessionId) {
|
|
348
|
-
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
349
|
-
}
|
|
350
347
|
/**
|
|
351
348
|
* Generates Redis key for tracking all sessions for a user
|
|
352
349
|
* @private
|
|
@@ -436,48 +433,54 @@ var RedisStorageBackend = class {
|
|
|
436
433
|
}
|
|
437
434
|
async patchCredentials(userId, sessionId, data) {
|
|
438
435
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
439
|
-
const
|
|
440
|
-
|
|
441
|
-
if (!sessionExists) {
|
|
436
|
+
const currentStr = await this.redis.get(sessionKey);
|
|
437
|
+
if (!currentStr) {
|
|
442
438
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
443
439
|
}
|
|
444
|
-
const
|
|
445
|
-
const
|
|
446
|
-
const effectiveTtl =
|
|
447
|
-
|
|
448
|
-
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
449
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
450
|
-
if (effectiveTtl > 0) {
|
|
451
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
452
|
-
} else {
|
|
453
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
454
|
-
}
|
|
440
|
+
const current = JSON.parse(currentStr);
|
|
441
|
+
const updated = { ...current, ...data, sessionId, userId };
|
|
442
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
443
|
+
await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
|
|
455
444
|
}
|
|
456
|
-
async get(userId, sessionId) {
|
|
445
|
+
async get(userId, sessionId, options) {
|
|
457
446
|
try {
|
|
458
447
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
459
448
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
460
|
-
if (!sessionDataStr)
|
|
461
|
-
|
|
449
|
+
if (!sessionDataStr) return null;
|
|
450
|
+
const session = normalizeStoredSession(JSON.parse(sessionDataStr));
|
|
451
|
+
if (!options?.includeCredentials) {
|
|
452
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
453
|
+
return sessionOnly;
|
|
462
454
|
}
|
|
463
|
-
|
|
464
|
-
return normalizeStoredSession(session);
|
|
455
|
+
return session;
|
|
465
456
|
} catch (error) {
|
|
466
457
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
467
458
|
return null;
|
|
468
459
|
}
|
|
469
460
|
}
|
|
470
461
|
async getCredentials(userId, sessionId) {
|
|
471
|
-
const session = await this.get(userId, sessionId);
|
|
462
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
472
463
|
if (!session) return null;
|
|
473
|
-
const
|
|
474
|
-
return
|
|
464
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
465
|
+
return {
|
|
466
|
+
sessionId,
|
|
467
|
+
userId,
|
|
468
|
+
clientInformation,
|
|
469
|
+
tokens,
|
|
470
|
+
codeVerifier,
|
|
471
|
+
codeVerifierChallenge,
|
|
472
|
+
codeVerifierNonce,
|
|
473
|
+
clientId,
|
|
474
|
+
oauthState
|
|
475
|
+
};
|
|
475
476
|
}
|
|
476
477
|
async clearCredentials(userId, sessionId) {
|
|
477
478
|
await this.patchCredentials(userId, sessionId, {
|
|
478
479
|
clientInformation: null,
|
|
479
480
|
tokens: null,
|
|
480
481
|
codeVerifier: null,
|
|
482
|
+
codeVerifierChallenge: null,
|
|
483
|
+
codeVerifierNonce: null,
|
|
481
484
|
clientId: null,
|
|
482
485
|
oauthState: null
|
|
483
486
|
});
|
|
@@ -510,10 +513,9 @@ var RedisStorageBackend = class {
|
|
|
510
513
|
async delete(userId, sessionId) {
|
|
511
514
|
try {
|
|
512
515
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
513
|
-
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
514
516
|
const userIdKey = this.getUserIdKey(userId);
|
|
515
517
|
await this.redis.srem(userIdKey, sessionId);
|
|
516
|
-
await this.redis.del(sessionKey
|
|
518
|
+
await this.redis.del(sessionKey);
|
|
517
519
|
} catch (error) {
|
|
518
520
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
519
521
|
}
|
|
@@ -544,9 +546,8 @@ var RedisStorageBackend = class {
|
|
|
544
546
|
async clearAll() {
|
|
545
547
|
try {
|
|
546
548
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
547
|
-
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
548
549
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
549
|
-
const allKeys = [...keys, ...
|
|
550
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
550
551
|
if (allKeys.length > 0) {
|
|
551
552
|
await this.redis.del(...allKeys);
|
|
552
553
|
}
|
|
@@ -570,7 +571,6 @@ var RedisStorageBackend = class {
|
|
|
570
571
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
571
572
|
if (staleSessionIds.length > 0) {
|
|
572
573
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
573
|
-
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
574
574
|
}
|
|
575
575
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
576
576
|
if (remainingCount === 0) {
|
|
@@ -596,7 +596,6 @@ var MemoryStorageBackend = class {
|
|
|
596
596
|
constructor() {
|
|
597
597
|
// Map<userId:sessionId, Session>
|
|
598
598
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
599
|
-
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
600
599
|
// Map<userId, Set<sessionId>>
|
|
601
600
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
602
601
|
}
|
|
@@ -634,26 +633,46 @@ var MemoryStorageBackend = class {
|
|
|
634
633
|
}
|
|
635
634
|
async patchCredentials(userId, sessionId, data) {
|
|
636
635
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
637
|
-
|
|
636
|
+
const current = this.sessions.get(sessionKey);
|
|
637
|
+
if (!current) {
|
|
638
638
|
throw new Error(`Session ${sessionId} not found`);
|
|
639
639
|
}
|
|
640
|
-
|
|
641
|
-
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
640
|
+
this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
642
641
|
}
|
|
643
|
-
async get(userId, sessionId) {
|
|
642
|
+
async get(userId, sessionId, options) {
|
|
644
643
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
645
|
-
|
|
644
|
+
const session = this.sessions.get(sessionKey) || null;
|
|
645
|
+
if (!session) return session;
|
|
646
|
+
if (!options?.includeCredentials) {
|
|
647
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
648
|
+
return sessionOnly;
|
|
649
|
+
}
|
|
650
|
+
return session;
|
|
646
651
|
}
|
|
647
652
|
async getCredentials(userId, sessionId) {
|
|
648
653
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
649
|
-
|
|
650
|
-
|
|
654
|
+
const session = this.sessions.get(sessionKey);
|
|
655
|
+
if (!session) return null;
|
|
656
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
657
|
+
return {
|
|
658
|
+
sessionId,
|
|
659
|
+
userId,
|
|
660
|
+
clientInformation,
|
|
661
|
+
tokens,
|
|
662
|
+
codeVerifier,
|
|
663
|
+
codeVerifierChallenge,
|
|
664
|
+
codeVerifierNonce,
|
|
665
|
+
clientId,
|
|
666
|
+
oauthState
|
|
667
|
+
};
|
|
651
668
|
}
|
|
652
669
|
async clearCredentials(userId, sessionId) {
|
|
653
670
|
await this.patchCredentials(userId, sessionId, {
|
|
654
671
|
clientInformation: null,
|
|
655
672
|
tokens: null,
|
|
656
673
|
codeVerifier: null,
|
|
674
|
+
codeVerifierChallenge: null,
|
|
675
|
+
codeVerifierNonce: null,
|
|
657
676
|
clientId: null,
|
|
658
677
|
oauthState: null
|
|
659
678
|
});
|
|
@@ -677,7 +696,6 @@ var MemoryStorageBackend = class {
|
|
|
677
696
|
async delete(userId, sessionId) {
|
|
678
697
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
679
698
|
this.sessions.delete(sessionKey);
|
|
680
|
-
this.credentials.delete(sessionKey);
|
|
681
699
|
const set = this.userIdSessions.get(userId);
|
|
682
700
|
if (set) {
|
|
683
701
|
set.delete(sessionId);
|
|
@@ -691,14 +709,12 @@ var MemoryStorageBackend = class {
|
|
|
691
709
|
}
|
|
692
710
|
async clearAll() {
|
|
693
711
|
this.sessions.clear();
|
|
694
|
-
this.credentials.clear();
|
|
695
712
|
this.userIdSessions.clear();
|
|
696
713
|
}
|
|
697
714
|
async cleanupExpired() {
|
|
698
715
|
for (const [key, session] of this.sessions.entries()) {
|
|
699
716
|
if (!isSessionExpired(session)) continue;
|
|
700
717
|
this.sessions.delete(key);
|
|
701
|
-
this.credentials.delete(key);
|
|
702
718
|
const set = this.userIdSessions.get(session.userId);
|
|
703
719
|
if (set) {
|
|
704
720
|
set.delete(session.sessionId);
|
|
@@ -721,7 +737,6 @@ var FileStorageBackend = class {
|
|
|
721
737
|
constructor(options = {}) {
|
|
722
738
|
__publicField(this, "filePath");
|
|
723
739
|
__publicField(this, "memoryCache", null);
|
|
724
|
-
__publicField(this, "credentialsCache", null);
|
|
725
740
|
__publicField(this, "initialized", false);
|
|
726
741
|
this.filePath = options.path || "./sessions.json";
|
|
727
742
|
}
|
|
@@ -736,22 +751,15 @@ var FileStorageBackend = class {
|
|
|
736
751
|
const data = await fs2.promises.readFile(this.filePath, "utf-8");
|
|
737
752
|
const json = JSON.parse(data);
|
|
738
753
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
739
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
740
754
|
if (Array.isArray(json.sessions)) {
|
|
741
755
|
json.sessions.forEach((s) => {
|
|
742
756
|
const session = normalizeStoredSession(s);
|
|
743
757
|
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
744
758
|
});
|
|
745
759
|
}
|
|
746
|
-
if (Array.isArray(json.credentials)) {
|
|
747
|
-
json.credentials.forEach((c) => {
|
|
748
|
-
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
760
|
} catch (error) {
|
|
752
761
|
if (error.code === "ENOENT") {
|
|
753
762
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
754
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
755
763
|
await this.flush();
|
|
756
764
|
} else {
|
|
757
765
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -765,10 +773,9 @@ var FileStorageBackend = class {
|
|
|
765
773
|
if (!this.initialized) await this.init();
|
|
766
774
|
}
|
|
767
775
|
async flush() {
|
|
768
|
-
if (!this.memoryCache
|
|
776
|
+
if (!this.memoryCache) return;
|
|
769
777
|
await fs2.promises.writeFile(this.filePath, JSON.stringify({
|
|
770
|
-
sessions: Array.from(this.memoryCache.values())
|
|
771
|
-
credentials: Array.from(this.credentialsCache.values())
|
|
778
|
+
sessions: Array.from(this.memoryCache.values())
|
|
772
779
|
}, null, 2), "utf-8");
|
|
773
780
|
}
|
|
774
781
|
getSessionKey(userId, sessionId) {
|
|
@@ -803,29 +810,49 @@ var FileStorageBackend = class {
|
|
|
803
810
|
async patchCredentials(userId, sessionId, data) {
|
|
804
811
|
await this.ensureInitialized();
|
|
805
812
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
806
|
-
|
|
813
|
+
const current = this.memoryCache.get(sessionKey);
|
|
814
|
+
if (!current) {
|
|
807
815
|
throw new Error(`Session ${sessionId} not found`);
|
|
808
816
|
}
|
|
809
|
-
|
|
810
|
-
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
817
|
+
this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
811
818
|
await this.flush();
|
|
812
819
|
}
|
|
813
|
-
async get(userId, sessionId) {
|
|
820
|
+
async get(userId, sessionId, options) {
|
|
814
821
|
await this.ensureInitialized();
|
|
815
822
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
816
|
-
|
|
823
|
+
const session = this.memoryCache.get(sessionKey) || null;
|
|
824
|
+
if (!session) return null;
|
|
825
|
+
if (!options?.includeCredentials) {
|
|
826
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
827
|
+
return sessionOnly;
|
|
828
|
+
}
|
|
829
|
+
return session;
|
|
817
830
|
}
|
|
818
831
|
async getCredentials(userId, sessionId) {
|
|
819
832
|
await this.ensureInitialized();
|
|
820
833
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
821
|
-
|
|
822
|
-
|
|
834
|
+
const session = this.memoryCache.get(sessionKey);
|
|
835
|
+
if (!session) return null;
|
|
836
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
837
|
+
return {
|
|
838
|
+
sessionId,
|
|
839
|
+
userId,
|
|
840
|
+
clientInformation,
|
|
841
|
+
tokens,
|
|
842
|
+
codeVerifier,
|
|
843
|
+
codeVerifierChallenge,
|
|
844
|
+
codeVerifierNonce,
|
|
845
|
+
clientId,
|
|
846
|
+
oauthState
|
|
847
|
+
};
|
|
823
848
|
}
|
|
824
849
|
async clearCredentials(userId, sessionId) {
|
|
825
850
|
await this.patchCredentials(userId, sessionId, {
|
|
826
851
|
clientInformation: null,
|
|
827
852
|
tokens: null,
|
|
828
853
|
codeVerifier: null,
|
|
854
|
+
codeVerifierChallenge: null,
|
|
855
|
+
codeVerifierNonce: null,
|
|
829
856
|
clientId: null,
|
|
830
857
|
oauthState: null
|
|
831
858
|
});
|
|
@@ -842,7 +869,6 @@ var FileStorageBackend = class {
|
|
|
842
869
|
await this.ensureInitialized();
|
|
843
870
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
844
871
|
const deleted = this.memoryCache.delete(sessionKey);
|
|
845
|
-
this.credentialsCache.delete(sessionKey);
|
|
846
872
|
if (deleted) {
|
|
847
873
|
await this.flush();
|
|
848
874
|
}
|
|
@@ -854,7 +880,6 @@ var FileStorageBackend = class {
|
|
|
854
880
|
async clearAll() {
|
|
855
881
|
await this.ensureInitialized();
|
|
856
882
|
this.memoryCache.clear();
|
|
857
|
-
this.credentialsCache.clear();
|
|
858
883
|
await this.flush();
|
|
859
884
|
}
|
|
860
885
|
async cleanupExpired() {
|
|
@@ -863,7 +888,6 @@ var FileStorageBackend = class {
|
|
|
863
888
|
for (const [key, session] of this.memoryCache.entries()) {
|
|
864
889
|
if (!isSessionExpired(session)) continue;
|
|
865
890
|
this.memoryCache.delete(key);
|
|
866
|
-
this.credentialsCache.delete(key);
|
|
867
891
|
changed = true;
|
|
868
892
|
}
|
|
869
893
|
if (changed) {
|
|
@@ -880,12 +904,10 @@ var SqliteStorage = class {
|
|
|
880
904
|
constructor(options = {}) {
|
|
881
905
|
__publicField(this, "db", null);
|
|
882
906
|
__publicField(this, "table");
|
|
883
|
-
__publicField(this, "credentialsTable");
|
|
884
907
|
__publicField(this, "initialized", false);
|
|
885
908
|
__publicField(this, "dbPath");
|
|
886
909
|
this.dbPath = options.path || "./sessions.db";
|
|
887
910
|
this.table = options.table || "mcp_sessions";
|
|
888
|
-
this.credentialsTable = `${this.table}_credentials`;
|
|
889
911
|
}
|
|
890
912
|
async init() {
|
|
891
913
|
if (this.initialized) return;
|
|
@@ -905,13 +927,6 @@ var SqliteStorage = class {
|
|
|
905
927
|
expiresAt INTEGER
|
|
906
928
|
);
|
|
907
929
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
908
|
-
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
909
|
-
sessionId TEXT NOT NULL,
|
|
910
|
-
userId TEXT NOT NULL,
|
|
911
|
-
data TEXT NOT NULL,
|
|
912
|
-
PRIMARY KEY (userId, sessionId),
|
|
913
|
-
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
914
|
-
);
|
|
915
930
|
`);
|
|
916
931
|
this.initialized = true;
|
|
917
932
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -968,41 +983,54 @@ var SqliteStorage = class {
|
|
|
968
983
|
}
|
|
969
984
|
async patchCredentials(userId, sessionId, data) {
|
|
970
985
|
this.ensureInitialized();
|
|
971
|
-
|
|
986
|
+
const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
|
|
987
|
+
if (!currentSession) {
|
|
972
988
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
973
989
|
}
|
|
974
|
-
const
|
|
975
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
990
|
+
const updated = { ...currentSession, ...data, sessionId, userId };
|
|
976
991
|
const stmt = this.db.prepare(
|
|
977
|
-
`
|
|
978
|
-
VALUES (?, ?, ?)
|
|
979
|
-
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
992
|
+
`UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
|
|
980
993
|
);
|
|
981
|
-
stmt.run(
|
|
994
|
+
stmt.run(JSON.stringify(updated), sessionId, userId);
|
|
982
995
|
}
|
|
983
|
-
async get(userId, sessionId) {
|
|
996
|
+
async get(userId, sessionId, options) {
|
|
984
997
|
this.ensureInitialized();
|
|
985
998
|
const stmt = this.db.prepare(
|
|
986
999
|
`SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
987
1000
|
);
|
|
988
1001
|
const row = stmt.get(sessionId, userId);
|
|
989
1002
|
if (!row) return null;
|
|
990
|
-
|
|
1003
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
1004
|
+
if (!options?.includeCredentials) {
|
|
1005
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
1006
|
+
return sessionOnly;
|
|
1007
|
+
}
|
|
1008
|
+
return session;
|
|
991
1009
|
}
|
|
992
1010
|
async getCredentials(userId, sessionId) {
|
|
993
1011
|
this.ensureInitialized();
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1012
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
1013
|
+
if (!session) return null;
|
|
1014
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
1015
|
+
return {
|
|
1016
|
+
sessionId,
|
|
1017
|
+
userId,
|
|
1018
|
+
clientInformation,
|
|
1019
|
+
tokens,
|
|
1020
|
+
codeVerifier,
|
|
1021
|
+
codeVerifierChallenge,
|
|
1022
|
+
codeVerifierNonce,
|
|
1023
|
+
clientId,
|
|
1024
|
+
oauthState
|
|
1025
|
+
};
|
|
1000
1026
|
}
|
|
1001
1027
|
async clearCredentials(userId, sessionId) {
|
|
1002
1028
|
await this.patchCredentials(userId, sessionId, {
|
|
1003
1029
|
clientInformation: null,
|
|
1004
1030
|
tokens: null,
|
|
1005
1031
|
codeVerifier: null,
|
|
1032
|
+
codeVerifierChallenge: null,
|
|
1033
|
+
codeVerifierNonce: null,
|
|
1006
1034
|
clientId: null,
|
|
1007
1035
|
oauthState: null
|
|
1008
1036
|
});
|
|
@@ -1028,9 +1056,6 @@ var SqliteStorage = class {
|
|
|
1028
1056
|
const stmt = this.db.prepare(
|
|
1029
1057
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
1030
1058
|
);
|
|
1031
|
-
this.db.prepare(
|
|
1032
|
-
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
1033
|
-
).run(sessionId, userId);
|
|
1034
1059
|
stmt.run(sessionId, userId);
|
|
1035
1060
|
}
|
|
1036
1061
|
async listAllIds() {
|
|
@@ -1041,7 +1066,6 @@ var SqliteStorage = class {
|
|
|
1041
1066
|
}
|
|
1042
1067
|
async clearAll() {
|
|
1043
1068
|
this.ensureInitialized();
|
|
1044
|
-
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
1045
1069
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
1046
1070
|
stmt.run();
|
|
1047
1071
|
}
|
|
@@ -1055,14 +1079,6 @@ var SqliteStorage = class {
|
|
|
1055
1079
|
deleteStmt.run(row.sessionId, row.userId);
|
|
1056
1080
|
}
|
|
1057
1081
|
}
|
|
1058
|
-
this.db.prepare(
|
|
1059
|
-
`DELETE FROM ${this.credentialsTable}
|
|
1060
|
-
WHERE NOT EXISTS (
|
|
1061
|
-
SELECT 1 FROM ${this.table}
|
|
1062
|
-
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1063
|
-
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1064
|
-
)`
|
|
1065
|
-
).run();
|
|
1066
1082
|
}
|
|
1067
1083
|
async disconnect() {
|
|
1068
1084
|
if (this.db) {
|
|
@@ -1103,8 +1119,8 @@ function encryptObject(data) {
|
|
|
1103
1119
|
}
|
|
1104
1120
|
try {
|
|
1105
1121
|
const text = JSON.stringify(data);
|
|
1106
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
1107
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
1122
|
+
const iv = crypto$1.randomBytes(IV_LENGTH);
|
|
1123
|
+
const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
|
|
1108
1124
|
let encrypted = cipher.update(text, "utf-8", "hex");
|
|
1109
1125
|
encrypted += cipher.final("hex");
|
|
1110
1126
|
const authTag = cipher.getAuthTag().toString("hex");
|
|
@@ -1132,7 +1148,7 @@ function decryptObject(data) {
|
|
|
1132
1148
|
const iv = Buffer.from(parts[2], "hex");
|
|
1133
1149
|
const authTag = Buffer.from(parts[3], "hex");
|
|
1134
1150
|
const encryptedText = parts[4];
|
|
1135
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
1151
|
+
const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
|
|
1136
1152
|
decipher.setAuthTag(authTag);
|
|
1137
1153
|
let decrypted = decipher.update(encryptedText, "hex", "utf-8");
|
|
1138
1154
|
decrypted += decipher.final("utf-8");
|
|
@@ -1150,7 +1166,6 @@ var SupabaseStorageBackend = class {
|
|
|
1150
1166
|
}
|
|
1151
1167
|
async init() {
|
|
1152
1168
|
await this.assertTable("mcp_sessions", "session_id");
|
|
1153
|
-
await this.assertTable("mcp_credentials", "session_id");
|
|
1154
1169
|
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1155
1170
|
}
|
|
1156
1171
|
async assertTable(table, column) {
|
|
@@ -1181,18 +1196,13 @@ var SupabaseStorageBackend = class {
|
|
|
1181
1196
|
headers: decryptObject(row.headers),
|
|
1182
1197
|
authUrl: row.auth_url,
|
|
1183
1198
|
status: row.status ?? "pending",
|
|
1184
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
clientInformation: decryptObject(row?.client_information),
|
|
1192
|
-
tokens: decryptObject(row?.tokens),
|
|
1193
|
-
codeVerifier: decryptObject(row?.code_verifier),
|
|
1194
|
-
clientId: row?.client_id,
|
|
1195
|
-
oauthState: row?.oauth_state
|
|
1199
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1200
|
+
clientInformation: decryptObject(row.client_information),
|
|
1201
|
+
tokens: decryptObject(row.tokens),
|
|
1202
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1203
|
+
clientId: row.client_id,
|
|
1204
|
+
oauthState: row.oauth_state,
|
|
1205
|
+
enabled: row.enabled ?? true
|
|
1196
1206
|
};
|
|
1197
1207
|
}
|
|
1198
1208
|
hasCredentialData(data) {
|
|
@@ -1250,6 +1260,7 @@ var SupabaseStorageBackend = class {
|
|
|
1250
1260
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
1251
1261
|
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1252
1262
|
if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
|
|
1263
|
+
if ("enabled" in data) updateData.enabled = data.enabled;
|
|
1253
1264
|
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1254
1265
|
let updatedRows = null;
|
|
1255
1266
|
if (shouldUpdateSession) {
|
|
@@ -1271,23 +1282,22 @@ var SupabaseStorageBackend = class {
|
|
|
1271
1282
|
}
|
|
1272
1283
|
async patchCredentials(userId, sessionId, data) {
|
|
1273
1284
|
if (!this.hasCredentialData(data)) return;
|
|
1274
|
-
const
|
|
1275
|
-
user_id: userId,
|
|
1276
|
-
session_id: sessionId,
|
|
1285
|
+
const updateData = {
|
|
1277
1286
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1278
1287
|
};
|
|
1279
|
-
if ("clientInformation" in data)
|
|
1280
|
-
if ("tokens" in data)
|
|
1281
|
-
if ("codeVerifier" in data)
|
|
1282
|
-
if ("clientId" in data)
|
|
1283
|
-
if ("oauthState" in data)
|
|
1284
|
-
const { error } = await this.supabase.from("
|
|
1288
|
+
if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1289
|
+
if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1290
|
+
if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1291
|
+
if ("clientId" in data) updateData.client_id = data.clientId ?? null;
|
|
1292
|
+
if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
|
|
1293
|
+
const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
|
|
1285
1294
|
if (error) {
|
|
1286
1295
|
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1287
1296
|
}
|
|
1288
1297
|
}
|
|
1289
|
-
async get(userId, sessionId) {
|
|
1290
|
-
const
|
|
1298
|
+
async get(userId, sessionId, options) {
|
|
1299
|
+
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";
|
|
1300
|
+
const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1291
1301
|
if (error) {
|
|
1292
1302
|
console.error("[SupabaseStorage] Failed to get session:", error);
|
|
1293
1303
|
return null;
|
|
@@ -1296,19 +1306,21 @@ var SupabaseStorageBackend = class {
|
|
|
1296
1306
|
return this.mapRowToSessionData(data);
|
|
1297
1307
|
}
|
|
1298
1308
|
async getCredentials(userId, sessionId) {
|
|
1299
|
-
const { data, error } = await this.supabase.from("
|
|
1309
|
+
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();
|
|
1300
1310
|
if (error) {
|
|
1301
1311
|
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1302
1312
|
return null;
|
|
1303
1313
|
}
|
|
1304
|
-
if (data)
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1314
|
+
if (!data) return null;
|
|
1315
|
+
return {
|
|
1316
|
+
sessionId,
|
|
1317
|
+
userId,
|
|
1318
|
+
clientInformation: decryptObject(data.client_information),
|
|
1319
|
+
tokens: decryptObject(data.tokens),
|
|
1320
|
+
codeVerifier: decryptObject(data.code_verifier),
|
|
1321
|
+
clientId: data.client_id,
|
|
1322
|
+
oauthState: data.oauth_state
|
|
1323
|
+
};
|
|
1312
1324
|
}
|
|
1313
1325
|
async list(userId) {
|
|
1314
1326
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
@@ -1319,7 +1331,14 @@ var SupabaseStorageBackend = class {
|
|
|
1319
1331
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
1320
1332
|
}
|
|
1321
1333
|
async clearCredentials(userId, sessionId) {
|
|
1322
|
-
const { error } = await this.supabase.from("
|
|
1334
|
+
const { error } = await this.supabase.from("mcp_sessions").update({
|
|
1335
|
+
client_information: null,
|
|
1336
|
+
tokens: null,
|
|
1337
|
+
code_verifier: null,
|
|
1338
|
+
client_id: null,
|
|
1339
|
+
oauth_state: null,
|
|
1340
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1341
|
+
}).eq("user_id", userId).eq("session_id", sessionId);
|
|
1323
1342
|
if (error) {
|
|
1324
1343
|
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1325
1344
|
}
|
|
@@ -1347,10 +1366,6 @@ var SupabaseStorageBackend = class {
|
|
|
1347
1366
|
return data.map((row) => row.session_id);
|
|
1348
1367
|
}
|
|
1349
1368
|
async clearAll() {
|
|
1350
|
-
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1351
|
-
if (credentialsError) {
|
|
1352
|
-
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1353
|
-
}
|
|
1354
1369
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
1355
1370
|
if (error) {
|
|
1356
1371
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
@@ -1377,16 +1392,12 @@ var NeonStorageBackend = class {
|
|
|
1377
1392
|
constructor(sql, options = {}) {
|
|
1378
1393
|
this.sql = sql;
|
|
1379
1394
|
__publicField(this, "tableName");
|
|
1380
|
-
__publicField(this, "credentialsTableName");
|
|
1381
1395
|
const schema = options.schema || "public";
|
|
1382
1396
|
const table = options.table || "mcp_sessions";
|
|
1383
|
-
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
1384
1397
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1385
|
-
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
1386
1398
|
}
|
|
1387
1399
|
async init() {
|
|
1388
1400
|
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1389
|
-
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1390
1401
|
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1391
1402
|
}
|
|
1392
1403
|
async assertTable(qualifiedName, displayName) {
|
|
@@ -1424,18 +1435,13 @@ var NeonStorageBackend = class {
|
|
|
1424
1435
|
headers: decryptObject(row.headers),
|
|
1425
1436
|
authUrl: row.auth_url ?? void 0,
|
|
1426
1437
|
status: row.status ?? "pending",
|
|
1427
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1428
|
-
};
|
|
1429
|
-
}
|
|
1430
|
-
mapRowToCredentials(row, userId, sessionId) {
|
|
1431
|
-
return {
|
|
1432
|
-
sessionId,
|
|
1433
|
-
userId,
|
|
1438
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1434
1439
|
clientInformation: decryptObject(row.client_information),
|
|
1435
1440
|
tokens: decryptObject(row.tokens),
|
|
1436
1441
|
codeVerifier: decryptObject(row.code_verifier),
|
|
1437
1442
|
clientId: row.client_id ?? void 0,
|
|
1438
|
-
oauthState: row.oauth_state
|
|
1443
|
+
oauthState: row.oauth_state,
|
|
1444
|
+
enabled: row.enabled ?? true
|
|
1439
1445
|
};
|
|
1440
1446
|
}
|
|
1441
1447
|
hasCredentialData(data) {
|
|
@@ -1505,7 +1511,7 @@ var NeonStorageBackend = class {
|
|
|
1505
1511
|
const updatedSession = { ...currentSession, ...data };
|
|
1506
1512
|
const status = updatedSession.status ?? "pending";
|
|
1507
1513
|
const expiresAt = resolveSessionExpiresAt(status);
|
|
1508
|
-
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;
|
|
1514
|
+
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;
|
|
1509
1515
|
if (shouldUpdateSession) {
|
|
1510
1516
|
const setClauses = [];
|
|
1511
1517
|
const values = [];
|
|
@@ -1528,6 +1534,9 @@ var NeonStorageBackend = class {
|
|
|
1528
1534
|
const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
|
|
1529
1535
|
addSet("tool_policy", toolPolicy);
|
|
1530
1536
|
}
|
|
1537
|
+
if ("enabled" in data) {
|
|
1538
|
+
addSet("enabled", updatedSession.enabled);
|
|
1539
|
+
}
|
|
1531
1540
|
setClauses.push("updated_at = now()");
|
|
1532
1541
|
const updatedRows = await this.sql.query(
|
|
1533
1542
|
`UPDATE ${this.tableName}
|
|
@@ -1543,45 +1552,45 @@ var NeonStorageBackend = class {
|
|
|
1543
1552
|
}
|
|
1544
1553
|
async patchCredentials(userId, sessionId, data) {
|
|
1545
1554
|
if (!this.hasCredentialData(data)) return;
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
"codeVerifier" in data,
|
|
1576
|
-
"clientId" in data,
|
|
1577
|
-
"oauthState" in data
|
|
1578
|
-
]
|
|
1555
|
+
const setClauses = [];
|
|
1556
|
+
const values = [];
|
|
1557
|
+
let paramIndex = 1;
|
|
1558
|
+
const addSet = (column, value) => {
|
|
1559
|
+
setClauses.push(`${column} = $${paramIndex++}`);
|
|
1560
|
+
values.push(value);
|
|
1561
|
+
};
|
|
1562
|
+
if ("clientInformation" in data) {
|
|
1563
|
+
addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
|
|
1564
|
+
}
|
|
1565
|
+
if ("tokens" in data) {
|
|
1566
|
+
addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
|
|
1567
|
+
}
|
|
1568
|
+
if ("codeVerifier" in data) {
|
|
1569
|
+
addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
|
|
1570
|
+
}
|
|
1571
|
+
if ("clientId" in data) {
|
|
1572
|
+
addSet("client_id", data.clientId ?? null);
|
|
1573
|
+
}
|
|
1574
|
+
if ("oauthState" in data) {
|
|
1575
|
+
addSet("oauth_state", data.oauthState ?? null);
|
|
1576
|
+
}
|
|
1577
|
+
setClauses.push("updated_at = now()");
|
|
1578
|
+
const updatedRows = await this.sql.query(
|
|
1579
|
+
`UPDATE ${this.tableName}
|
|
1580
|
+
SET ${setClauses.join(", ")}
|
|
1581
|
+
WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
|
|
1582
|
+
RETURNING id`,
|
|
1583
|
+
[...values, userId, sessionId]
|
|
1579
1584
|
);
|
|
1585
|
+
if (updatedRows.length === 0) {
|
|
1586
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1587
|
+
}
|
|
1580
1588
|
}
|
|
1581
|
-
async get(userId, sessionId) {
|
|
1589
|
+
async get(userId, sessionId, options) {
|
|
1582
1590
|
try {
|
|
1591
|
+
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";
|
|
1583
1592
|
const rows = await this.sql.query(
|
|
1584
|
-
`SELECT
|
|
1593
|
+
`SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1585
1594
|
[userId, sessionId]
|
|
1586
1595
|
);
|
|
1587
1596
|
if (!rows[0]) return null;
|
|
@@ -1593,18 +1602,22 @@ var NeonStorageBackend = class {
|
|
|
1593
1602
|
}
|
|
1594
1603
|
async getCredentials(userId, sessionId) {
|
|
1595
1604
|
try {
|
|
1596
|
-
const
|
|
1597
|
-
`SELECT
|
|
1598
|
-
|
|
1599
|
-
);
|
|
1600
|
-
if (credentialRows[0]) {
|
|
1601
|
-
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1602
|
-
}
|
|
1603
|
-
const sessionRows = await this.sql.query(
|
|
1604
|
-
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1605
|
+
const rows = await this.sql.query(
|
|
1606
|
+
`SELECT client_information, tokens, code_verifier, client_id, oauth_state
|
|
1607
|
+
FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1605
1608
|
[userId, sessionId]
|
|
1606
1609
|
);
|
|
1607
|
-
|
|
1610
|
+
if (!rows[0]) return null;
|
|
1611
|
+
const row = rows[0];
|
|
1612
|
+
return {
|
|
1613
|
+
sessionId,
|
|
1614
|
+
userId,
|
|
1615
|
+
clientInformation: decryptObject(row.client_information),
|
|
1616
|
+
tokens: decryptObject(row.tokens),
|
|
1617
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1618
|
+
clientId: row.client_id ?? void 0,
|
|
1619
|
+
oauthState: row.oauth_state
|
|
1620
|
+
};
|
|
1608
1621
|
} catch (error) {
|
|
1609
1622
|
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1610
1623
|
return null;
|
|
@@ -1625,7 +1638,9 @@ var NeonStorageBackend = class {
|
|
|
1625
1638
|
async clearCredentials(userId, sessionId) {
|
|
1626
1639
|
try {
|
|
1627
1640
|
await this.sql.query(
|
|
1628
|
-
`
|
|
1641
|
+
`UPDATE ${this.tableName}
|
|
1642
|
+
SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
|
|
1643
|
+
WHERE user_id = $1 AND session_id = $2`,
|
|
1629
1644
|
[userId, sessionId]
|
|
1630
1645
|
);
|
|
1631
1646
|
} catch (error) {
|
|
@@ -1667,7 +1682,6 @@ var NeonStorageBackend = class {
|
|
|
1667
1682
|
}
|
|
1668
1683
|
async clearAll() {
|
|
1669
1684
|
try {
|
|
1670
|
-
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1671
1685
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1672
1686
|
} catch (error) {
|
|
1673
1687
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1912,6 +1926,60 @@ function onSessionMutation(listener) {
|
|
|
1912
1926
|
sessionMutationListeners.delete(listener);
|
|
1913
1927
|
};
|
|
1914
1928
|
}
|
|
1929
|
+
function withDbObservability(store, emit) {
|
|
1930
|
+
const readMethods = /* @__PURE__ */ new Set([
|
|
1931
|
+
"get",
|
|
1932
|
+
"getCredentials",
|
|
1933
|
+
"list",
|
|
1934
|
+
"listIds",
|
|
1935
|
+
"listAllIds"
|
|
1936
|
+
]);
|
|
1937
|
+
return new Proxy(store, {
|
|
1938
|
+
get(target, prop, receiver) {
|
|
1939
|
+
const original = Reflect.get(target, prop, receiver);
|
|
1940
|
+
if (typeof original !== "function") return original;
|
|
1941
|
+
return (...args) => {
|
|
1942
|
+
const method = prop;
|
|
1943
|
+
const isRead = readMethods.has(prop);
|
|
1944
|
+
const start = performance.now();
|
|
1945
|
+
const emitEvent = (error) => {
|
|
1946
|
+
emit({
|
|
1947
|
+
type: isRead ? "db:read" : "db:write",
|
|
1948
|
+
level: error ? "error" : "debug",
|
|
1949
|
+
message: `${method}(${args.map(
|
|
1950
|
+
(a) => typeof a === "string" ? a.slice(0, 24) : typeof a
|
|
1951
|
+
).join(", ")})`,
|
|
1952
|
+
sessionId: typeof args[1] === "string" ? args[1] : void 0,
|
|
1953
|
+
payload: {
|
|
1954
|
+
method,
|
|
1955
|
+
argTypes: args.map((a) => typeof a),
|
|
1956
|
+
durationMs: performance.now() - start,
|
|
1957
|
+
...error ? { error } : {}
|
|
1958
|
+
},
|
|
1959
|
+
timestamp: Date.now()
|
|
1960
|
+
});
|
|
1961
|
+
};
|
|
1962
|
+
try {
|
|
1963
|
+
const result = original.apply(target, args);
|
|
1964
|
+
if (result instanceof Promise) {
|
|
1965
|
+
return result.then((r) => {
|
|
1966
|
+
emitEvent();
|
|
1967
|
+
return r;
|
|
1968
|
+
}).catch((e) => {
|
|
1969
|
+
emitEvent(String(e));
|
|
1970
|
+
throw e;
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
emitEvent();
|
|
1974
|
+
return result;
|
|
1975
|
+
} catch (e) {
|
|
1976
|
+
emitEvent(String(e));
|
|
1977
|
+
throw e;
|
|
1978
|
+
}
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1915
1983
|
var sessions = new Proxy({}, {
|
|
1916
1984
|
get(_target, prop) {
|
|
1917
1985
|
return async (...args) => {
|
|
@@ -1931,6 +1999,24 @@ var sessions = new Proxy({}, {
|
|
|
1931
1999
|
});
|
|
1932
2000
|
|
|
1933
2001
|
// src/server/mcp/storage-oauth-provider.ts
|
|
2002
|
+
var codeVerifierContext = new async_hooks.AsyncLocalStorage();
|
|
2003
|
+
function runWithCodeVerifierState(verifier, method, fn) {
|
|
2004
|
+
return codeVerifierContext.run({ verifier, method }, fn);
|
|
2005
|
+
}
|
|
2006
|
+
function base64UrlEncode(bytes) {
|
|
2007
|
+
let binary = "";
|
|
2008
|
+
for (const byte of bytes) {
|
|
2009
|
+
binary += String.fromCharCode(byte);
|
|
2010
|
+
}
|
|
2011
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2012
|
+
}
|
|
2013
|
+
async function createCodeChallenge(verifier) {
|
|
2014
|
+
const digest = await crypto.subtle.digest(
|
|
2015
|
+
"SHA-256",
|
|
2016
|
+
new TextEncoder().encode(verifier)
|
|
2017
|
+
);
|
|
2018
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
2019
|
+
}
|
|
1934
2020
|
var StorageOAuthClientProvider = class {
|
|
1935
2021
|
/**
|
|
1936
2022
|
* Creates a new session-backed OAuth provider
|
|
@@ -1946,8 +2032,12 @@ var StorageOAuthClientProvider = class {
|
|
|
1946
2032
|
__publicField(this, "logoUri");
|
|
1947
2033
|
__publicField(this, "policyUri");
|
|
1948
2034
|
__publicField(this, "clientSecret");
|
|
2035
|
+
__publicField(this, "_store");
|
|
1949
2036
|
__publicField(this, "_authUrl");
|
|
1950
2037
|
__publicField(this, "_clientId");
|
|
2038
|
+
__publicField(this, "_cachedCodeVerifier");
|
|
2039
|
+
__publicField(this, "_hasCodeVerifier", false);
|
|
2040
|
+
__publicField(this, "_cachedTokens");
|
|
1951
2041
|
__publicField(this, "onRedirectCallback");
|
|
1952
2042
|
this.userId = options.userId;
|
|
1953
2043
|
this.serverId = options.serverId;
|
|
@@ -1959,6 +2049,8 @@ var StorageOAuthClientProvider = class {
|
|
|
1959
2049
|
this.policyUri = options.policyUri;
|
|
1960
2050
|
this._clientId = options.clientId;
|
|
1961
2051
|
this.clientSecret = options.clientSecret;
|
|
2052
|
+
this._cachedTokens = options.cachedTokens;
|
|
2053
|
+
this._store = options.sessionStore ?? sessions;
|
|
1962
2054
|
this.onRedirectCallback = options.onRedirect;
|
|
1963
2055
|
}
|
|
1964
2056
|
get clientMetadata() {
|
|
@@ -1986,7 +2078,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1986
2078
|
* @private
|
|
1987
2079
|
*/
|
|
1988
2080
|
async getCredentials() {
|
|
1989
|
-
const data = await
|
|
2081
|
+
const data = await this._store.getCredentials(this.userId, this.sessionId);
|
|
1990
2082
|
if (!data) {
|
|
1991
2083
|
return { userId: this.userId, sessionId: this.sessionId };
|
|
1992
2084
|
}
|
|
@@ -1999,35 +2091,33 @@ var StorageOAuthClientProvider = class {
|
|
|
1999
2091
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
2000
2092
|
*/
|
|
2001
2093
|
async patchCredentials(data) {
|
|
2002
|
-
await
|
|
2094
|
+
await this._store.patchCredentials(this.userId, this.sessionId, data);
|
|
2003
2095
|
}
|
|
2004
2096
|
/**
|
|
2005
|
-
* Retrieves stored OAuth client information
|
|
2097
|
+
* Retrieves stored OAuth client information.
|
|
2006
2098
|
*/
|
|
2007
2099
|
async clientInformation() {
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
await this.patchCredentials({
|
|
2014
|
-
clientId: this._clientId,
|
|
2015
|
-
clientInformation: {
|
|
2016
|
-
client_id: this._clientId,
|
|
2017
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2018
|
-
}
|
|
2019
|
-
});
|
|
2100
|
+
if (this._clientId) {
|
|
2101
|
+
return {
|
|
2102
|
+
client_id: this._clientId,
|
|
2103
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2104
|
+
};
|
|
2020
2105
|
}
|
|
2021
|
-
|
|
2022
|
-
|
|
2106
|
+
const data = await this.getCredentials();
|
|
2107
|
+
if (this._cachedTokens === void 0) {
|
|
2108
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2023
2109
|
}
|
|
2024
|
-
if (
|
|
2025
|
-
|
|
2110
|
+
if (data.clientId) {
|
|
2111
|
+
this._clientId = data.clientId;
|
|
2112
|
+
if (data.clientInformation) {
|
|
2113
|
+
return data.clientInformation;
|
|
2114
|
+
}
|
|
2115
|
+
return {
|
|
2116
|
+
client_id: data.clientId,
|
|
2117
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2118
|
+
};
|
|
2026
2119
|
}
|
|
2027
|
-
return
|
|
2028
|
-
client_id: this._clientId,
|
|
2029
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2030
|
-
};
|
|
2120
|
+
return void 0;
|
|
2031
2121
|
}
|
|
2032
2122
|
/**
|
|
2033
2123
|
* Stores OAuth client information
|
|
@@ -2044,11 +2134,30 @@ var StorageOAuthClientProvider = class {
|
|
|
2044
2134
|
*/
|
|
2045
2135
|
async saveTokens(tokens) {
|
|
2046
2136
|
await this.patchCredentials({ tokens });
|
|
2137
|
+
this._cachedTokens = tokens;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* Persists static client credentials to DB immediately on creation.
|
|
2141
|
+
* Ensures getOrCreateClient() finds them on rehydration, even when the
|
|
2142
|
+
* server allows anonymous connect and never triggers DCR.
|
|
2143
|
+
*/
|
|
2144
|
+
async initializeCredentials() {
|
|
2145
|
+
if (this._clientId) {
|
|
2146
|
+
await this.patchCredentials({
|
|
2147
|
+
clientId: this._clientId,
|
|
2148
|
+
clientInformation: {
|
|
2149
|
+
client_id: this._clientId,
|
|
2150
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2151
|
+
}
|
|
2152
|
+
});
|
|
2153
|
+
}
|
|
2047
2154
|
}
|
|
2048
2155
|
get authUrl() {
|
|
2049
2156
|
return this._authUrl;
|
|
2050
2157
|
}
|
|
2051
2158
|
async state() {
|
|
2159
|
+
this._cachedCodeVerifier = void 0;
|
|
2160
|
+
this._hasCodeVerifier = false;
|
|
2052
2161
|
const nonce = nanoid.nanoid(32);
|
|
2053
2162
|
await this.patchCredentials({
|
|
2054
2163
|
oauthState: {
|
|
@@ -2069,7 +2178,7 @@ var StorageOAuthClientProvider = class {
|
|
|
2069
2178
|
if (parsed.sessionId !== this.sessionId) {
|
|
2070
2179
|
return { valid: false, error: "OAuth state mismatch" };
|
|
2071
2180
|
}
|
|
2072
|
-
const data = await
|
|
2181
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
2073
2182
|
if (!data) {
|
|
2074
2183
|
return { valid: false, error: "Session not found" };
|
|
2075
2184
|
}
|
|
@@ -2094,49 +2203,77 @@ var StorageOAuthClientProvider = class {
|
|
|
2094
2203
|
}
|
|
2095
2204
|
async redirectToAuthorization(authUrl) {
|
|
2096
2205
|
this._authUrl = authUrl.toString();
|
|
2097
|
-
|
|
2206
|
+
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
|
2207
|
+
const state = authUrl.searchParams.get("state");
|
|
2208
|
+
if (this._cachedCodeVerifier && codeChallenge && state) {
|
|
2209
|
+
const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
|
|
2210
|
+
if (expectedChallenge === codeChallenge) {
|
|
2211
|
+
const parsed = parseOAuthState(state);
|
|
2212
|
+
if (parsed) {
|
|
2213
|
+
await this.patchCredentials({
|
|
2214
|
+
codeVerifier: this._cachedCodeVerifier
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2098
2219
|
if (this.onRedirectCallback) {
|
|
2099
2220
|
this.onRedirectCallback(authUrl.toString());
|
|
2100
2221
|
}
|
|
2101
2222
|
}
|
|
2102
2223
|
async invalidateCredentials(scope) {
|
|
2103
2224
|
if (scope === "all") {
|
|
2104
|
-
|
|
2225
|
+
this._cachedTokens = void 0;
|
|
2226
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
2105
2227
|
} else {
|
|
2106
2228
|
const updates = {};
|
|
2107
2229
|
if (scope === "client") {
|
|
2108
2230
|
updates.clientInformation = null;
|
|
2109
2231
|
updates.clientId = null;
|
|
2110
2232
|
} else if (scope === "tokens") {
|
|
2233
|
+
this._cachedTokens = void 0;
|
|
2111
2234
|
updates.tokens = null;
|
|
2112
2235
|
} else if (scope === "verifier") {
|
|
2236
|
+
this._cachedCodeVerifier = void 0;
|
|
2237
|
+
this._hasCodeVerifier = false;
|
|
2113
2238
|
updates.codeVerifier = null;
|
|
2114
2239
|
}
|
|
2115
2240
|
await this.patchCredentials(updates);
|
|
2116
2241
|
}
|
|
2117
2242
|
}
|
|
2118
2243
|
async saveCodeVerifier(verifier) {
|
|
2119
|
-
|
|
2120
|
-
if (data.codeVerifier) {
|
|
2244
|
+
if (this._hasCodeVerifier) {
|
|
2121
2245
|
return;
|
|
2122
2246
|
}
|
|
2123
|
-
|
|
2247
|
+
this._cachedCodeVerifier = verifier;
|
|
2248
|
+
this._hasCodeVerifier = true;
|
|
2124
2249
|
}
|
|
2125
2250
|
async codeVerifier() {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
this._clientId = data.clientId;
|
|
2251
|
+
if (this._cachedCodeVerifier) {
|
|
2252
|
+
return this._cachedCodeVerifier;
|
|
2129
2253
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2254
|
+
const ctx = codeVerifierContext.getStore();
|
|
2255
|
+
if (ctx?.verifier) {
|
|
2256
|
+
return ctx.verifier;
|
|
2257
|
+
}
|
|
2258
|
+
const data = await this.getCredentials();
|
|
2259
|
+
if (data.codeVerifier) {
|
|
2260
|
+
return data.codeVerifier;
|
|
2132
2261
|
}
|
|
2133
|
-
|
|
2262
|
+
throw new Error("No code verifier found");
|
|
2134
2263
|
}
|
|
2135
2264
|
async deleteCodeVerifier() {
|
|
2136
|
-
|
|
2265
|
+
this._cachedCodeVerifier = void 0;
|
|
2266
|
+
this._hasCodeVerifier = false;
|
|
2267
|
+
await this.patchCredentials({
|
|
2268
|
+
codeVerifier: null
|
|
2269
|
+
});
|
|
2137
2270
|
}
|
|
2138
2271
|
async tokens() {
|
|
2272
|
+
if (this._cachedTokens !== void 0) {
|
|
2273
|
+
return this._cachedTokens ?? void 0;
|
|
2274
|
+
}
|
|
2139
2275
|
const data = await this.getCredentials();
|
|
2276
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2140
2277
|
if (data.clientId && !this._clientId) {
|
|
2141
2278
|
this._clientId = data.clientId;
|
|
2142
2279
|
}
|
|
@@ -2231,23 +2368,10 @@ var MCPClient = class {
|
|
|
2231
2368
|
__publicField(this, "client");
|
|
2232
2369
|
__publicField(this, "oauthProvider", null);
|
|
2233
2370
|
__publicField(this, "transport", null);
|
|
2234
|
-
__publicField(this, "
|
|
2235
|
-
__publicField(this, "serverId");
|
|
2236
|
-
__publicField(this, "sessionId");
|
|
2237
|
-
__publicField(this, "serverName");
|
|
2238
|
-
__publicField(this, "transportType");
|
|
2239
|
-
__publicField(this, "serverUrl");
|
|
2240
|
-
__publicField(this, "callbackUrl");
|
|
2241
|
-
__publicField(this, "onRedirect");
|
|
2242
|
-
__publicField(this, "clientId");
|
|
2243
|
-
__publicField(this, "clientSecret");
|
|
2244
|
-
__publicField(this, "headers");
|
|
2245
|
-
/** OAuth Client Metadata */
|
|
2246
|
-
__publicField(this, "clientName");
|
|
2247
|
-
__publicField(this, "clientUri");
|
|
2248
|
-
__publicField(this, "logoUri");
|
|
2249
|
-
__publicField(this, "policyUri");
|
|
2371
|
+
__publicField(this, "config");
|
|
2250
2372
|
__publicField(this, "createdAt");
|
|
2373
|
+
__publicField(this, "_serverInfo");
|
|
2374
|
+
__publicField(this, "_store");
|
|
2251
2375
|
/** Event emitters for connection lifecycle */
|
|
2252
2376
|
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
2253
2377
|
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
@@ -2263,21 +2387,8 @@ var MCPClient = class {
|
|
|
2263
2387
|
* the memory when the client is no longer needed.
|
|
2264
2388
|
*/
|
|
2265
2389
|
__publicField(this, "cachedTools", null);
|
|
2266
|
-
this.
|
|
2267
|
-
this.
|
|
2268
|
-
this.callbackUrl = options.callbackUrl;
|
|
2269
|
-
this.onRedirect = options.onRedirect;
|
|
2270
|
-
this.userId = options.userId;
|
|
2271
|
-
this.serverId = options.serverId;
|
|
2272
|
-
this.sessionId = options.sessionId;
|
|
2273
|
-
this.transportType = options.transportType;
|
|
2274
|
-
this.clientId = options.clientId;
|
|
2275
|
-
this.clientSecret = options.clientSecret;
|
|
2276
|
-
this.headers = options.headers;
|
|
2277
|
-
this.clientName = options.clientName;
|
|
2278
|
-
this.clientUri = options.clientUri;
|
|
2279
|
-
this.logoUri = options.logoUri;
|
|
2280
|
-
this.policyUri = options.policyUri;
|
|
2390
|
+
this.config = { ...options };
|
|
2391
|
+
this._store = options.sessionStore ?? sessions;
|
|
2281
2392
|
this.client = new index_js.Client(
|
|
2282
2393
|
{
|
|
2283
2394
|
name: MCP_CLIENT_NAME,
|
|
@@ -2294,6 +2405,21 @@ var MCPClient = class {
|
|
|
2294
2405
|
}
|
|
2295
2406
|
);
|
|
2296
2407
|
}
|
|
2408
|
+
/** Shared session-shaped data for ensureSession and saveSession */
|
|
2409
|
+
get session() {
|
|
2410
|
+
return {
|
|
2411
|
+
sessionId: this.config.sessionId,
|
|
2412
|
+
userId: this.config.userId,
|
|
2413
|
+
serverId: this.config.serverId,
|
|
2414
|
+
serverName: this.config.serverName,
|
|
2415
|
+
serverUrl: this.config.serverUrl,
|
|
2416
|
+
callbackUrl: this.config.callbackUrl,
|
|
2417
|
+
transportType: this.config.transportType || "streamable-http",
|
|
2418
|
+
headers: this.config.headers,
|
|
2419
|
+
createdAt: this.createdAt ?? Date.now(),
|
|
2420
|
+
updatedAt: Date.now()
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2297
2423
|
/**
|
|
2298
2424
|
* Emit a connection state change event
|
|
2299
2425
|
* @private
|
|
@@ -2301,13 +2427,13 @@ var MCPClient = class {
|
|
|
2301
2427
|
emitStateChange(newState) {
|
|
2302
2428
|
const previousState = this.currentState;
|
|
2303
2429
|
this.currentState = newState;
|
|
2304
|
-
if (!this.serverId) return;
|
|
2430
|
+
if (!this.config.serverId) return;
|
|
2305
2431
|
this._onConnectionEvent.fire({
|
|
2306
2432
|
type: "state_changed",
|
|
2307
|
-
sessionId: this.sessionId,
|
|
2308
|
-
serverId: this.serverId,
|
|
2309
|
-
serverName: this.serverName || this.serverId,
|
|
2310
|
-
serverUrl: this.serverUrl || "",
|
|
2433
|
+
sessionId: this.config.sessionId,
|
|
2434
|
+
serverId: this.config.serverId,
|
|
2435
|
+
serverName: this.config.serverName || this.config.serverId,
|
|
2436
|
+
serverUrl: this.config.serverUrl || "",
|
|
2311
2437
|
createdAt: this.createdAt,
|
|
2312
2438
|
state: newState,
|
|
2313
2439
|
previousState,
|
|
@@ -2318,8 +2444,8 @@ var MCPClient = class {
|
|
|
2318
2444
|
level: "info",
|
|
2319
2445
|
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
2320
2446
|
displayMessage: `State changed to ${newState}`,
|
|
2321
|
-
sessionId: this.sessionId,
|
|
2322
|
-
serverId: this.serverId,
|
|
2447
|
+
sessionId: this.config.sessionId,
|
|
2448
|
+
serverId: this.config.serverId,
|
|
2323
2449
|
payload: { previousState, newState },
|
|
2324
2450
|
timestamp: Date.now(),
|
|
2325
2451
|
id: nanoid.nanoid()
|
|
@@ -2330,11 +2456,11 @@ var MCPClient = class {
|
|
|
2330
2456
|
* @private
|
|
2331
2457
|
*/
|
|
2332
2458
|
emitError(error, errorType = "unknown") {
|
|
2333
|
-
if (!this.serverId) return;
|
|
2459
|
+
if (!this.config.serverId) return;
|
|
2334
2460
|
this._onConnectionEvent.fire({
|
|
2335
2461
|
type: "error",
|
|
2336
|
-
sessionId: this.sessionId,
|
|
2337
|
-
serverId: this.serverId,
|
|
2462
|
+
sessionId: this.config.sessionId,
|
|
2463
|
+
serverId: this.config.serverId,
|
|
2338
2464
|
error,
|
|
2339
2465
|
errorType,
|
|
2340
2466
|
timestamp: Date.now()
|
|
@@ -2344,8 +2470,8 @@ var MCPClient = class {
|
|
|
2344
2470
|
level: "error",
|
|
2345
2471
|
message: error,
|
|
2346
2472
|
displayMessage: error,
|
|
2347
|
-
sessionId: this.sessionId,
|
|
2348
|
-
serverId: this.serverId,
|
|
2473
|
+
sessionId: this.config.sessionId,
|
|
2474
|
+
serverId: this.config.serverId,
|
|
2349
2475
|
payload: { errorType, error },
|
|
2350
2476
|
timestamp: Date.now(),
|
|
2351
2477
|
id: nanoid.nanoid()
|
|
@@ -2356,11 +2482,11 @@ var MCPClient = class {
|
|
|
2356
2482
|
* @private
|
|
2357
2483
|
*/
|
|
2358
2484
|
emitProgress(message) {
|
|
2359
|
-
if (!this.serverId) return;
|
|
2485
|
+
if (!this.config.serverId) return;
|
|
2360
2486
|
this._onConnectionEvent.fire({
|
|
2361
2487
|
type: "progress",
|
|
2362
|
-
sessionId: this.sessionId,
|
|
2363
|
-
serverId: this.serverId,
|
|
2488
|
+
sessionId: this.config.sessionId,
|
|
2489
|
+
serverId: this.config.serverId,
|
|
2364
2490
|
message,
|
|
2365
2491
|
timestamp: Date.now()
|
|
2366
2492
|
});
|
|
@@ -2378,14 +2504,14 @@ var MCPClient = class {
|
|
|
2378
2504
|
* @private
|
|
2379
2505
|
*/
|
|
2380
2506
|
getTransport(type) {
|
|
2381
|
-
if (!this.serverUrl) {
|
|
2507
|
+
if (!this.config.serverUrl) {
|
|
2382
2508
|
throw new Error("Server URL is required to create transport");
|
|
2383
2509
|
}
|
|
2384
|
-
const baseUrl = new URL(this.serverUrl);
|
|
2385
|
-
const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2510
|
+
const baseUrl = new URL(this.config.serverUrl);
|
|
2511
|
+
const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2386
2512
|
const transportOptions = {
|
|
2387
2513
|
...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
|
|
2388
|
-
...this.headers && { requestInit: { headers: this.headers } },
|
|
2514
|
+
...this.config.headers && { requestInit: { headers: this.config.headers } },
|
|
2389
2515
|
/**
|
|
2390
2516
|
* Custom fetch implementation to handle connection timeouts.
|
|
2391
2517
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
@@ -2423,78 +2549,74 @@ var MCPClient = class {
|
|
|
2423
2549
|
* @private
|
|
2424
2550
|
*/
|
|
2425
2551
|
async ensureSession() {
|
|
2426
|
-
if (this.oauthProvider)
|
|
2427
|
-
return;
|
|
2428
|
-
}
|
|
2552
|
+
if (this.oauthProvider) return;
|
|
2429
2553
|
this.emitStateChange("INITIALIZING");
|
|
2430
2554
|
this.emitProgress("Loading session configuration...");
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
throw new Error(`Session not found: ${this.sessionId}`);
|
|
2555
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2556
|
+
const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2557
|
+
if (!existingSession2) {
|
|
2558
|
+
throw new Error(`Session not found: ${this.config.sessionId}`);
|
|
2436
2559
|
}
|
|
2437
|
-
this.serverUrl = this.serverUrl ||
|
|
2438
|
-
this.callbackUrl = this.callbackUrl ||
|
|
2439
|
-
this.serverName = this.serverName ||
|
|
2440
|
-
this.serverId = this.serverId ||
|
|
2441
|
-
this.headers = this.headers ||
|
|
2442
|
-
this.createdAt =
|
|
2443
|
-
}
|
|
2444
|
-
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
2560
|
+
this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
|
|
2561
|
+
this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
|
|
2562
|
+
this.config.serverName = this.config.serverName || existingSession2.serverName;
|
|
2563
|
+
this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
|
|
2564
|
+
this.config.headers = this.config.headers || existingSession2.headers;
|
|
2565
|
+
this.createdAt = existingSession2.createdAt;
|
|
2566
|
+
}
|
|
2567
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2445
2568
|
throw new Error("Missing required connection metadata");
|
|
2446
2569
|
}
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
this.
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
this.
|
|
2465
|
-
|
|
2570
|
+
this.oauthProvider = new StorageOAuthClientProvider({
|
|
2571
|
+
userId: this.config.userId,
|
|
2572
|
+
serverId: this.config.serverId,
|
|
2573
|
+
sessionId: this.config.sessionId,
|
|
2574
|
+
redirectUrl: this.config.callbackUrl,
|
|
2575
|
+
clientName: this.config.clientName,
|
|
2576
|
+
clientUri: this.config.clientUri,
|
|
2577
|
+
logoUri: this.config.logoUri,
|
|
2578
|
+
policyUri: this.config.policyUri,
|
|
2579
|
+
clientId: this.config.clientId,
|
|
2580
|
+
clientSecret: this.config.clientSecret,
|
|
2581
|
+
cachedTokens: this.config.cachedCredentials?.tokens,
|
|
2582
|
+
sessionStore: this._store,
|
|
2583
|
+
onRedirect: (redirectUrl) => {
|
|
2584
|
+
if (this.config.serverId) {
|
|
2585
|
+
this._onConnectionEvent.fire({
|
|
2586
|
+
type: "auth_required",
|
|
2587
|
+
sessionId: this.config.sessionId,
|
|
2588
|
+
serverId: this.config.serverId,
|
|
2589
|
+
authUrl: redirectUrl,
|
|
2590
|
+
timestamp: Date.now()
|
|
2591
|
+
});
|
|
2466
2592
|
}
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
}
|
|
2472
|
-
|
|
2593
|
+
if (this.config.onRedirect) {
|
|
2594
|
+
this.config.onRedirect(redirectUrl);
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
});
|
|
2598
|
+
const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
|
|
2599
|
+
if (!existingSession) {
|
|
2473
2600
|
this.createdAt = Date.now();
|
|
2474
2601
|
const updatedAt = this.createdAt;
|
|
2475
2602
|
this._onObservabilityEvent.fire({
|
|
2476
2603
|
type: "mcp:client:session_created",
|
|
2477
2604
|
level: "info",
|
|
2478
|
-
message: `Creating pending session ${this.sessionId} for connection setup`,
|
|
2479
|
-
sessionId: this.sessionId,
|
|
2480
|
-
serverId: this.serverId,
|
|
2605
|
+
message: `Creating pending session ${this.config.sessionId} for connection setup`,
|
|
2606
|
+
sessionId: this.config.sessionId,
|
|
2607
|
+
serverId: this.config.serverId,
|
|
2481
2608
|
timestamp: Date.now(),
|
|
2482
2609
|
id: nanoid.nanoid()
|
|
2483
2610
|
});
|
|
2484
|
-
await
|
|
2485
|
-
|
|
2486
|
-
userId: this.userId,
|
|
2487
|
-
serverId: this.serverId,
|
|
2488
|
-
serverName: this.serverName,
|
|
2489
|
-
serverUrl: this.serverUrl,
|
|
2490
|
-
callbackUrl: this.callbackUrl,
|
|
2491
|
-
transportType: this.transportType || "streamable-http",
|
|
2492
|
-
headers: this.headers,
|
|
2493
|
-
createdAt: this.createdAt,
|
|
2611
|
+
await this._store.create({
|
|
2612
|
+
...this.session,
|
|
2494
2613
|
updatedAt,
|
|
2495
2614
|
status: "pending"
|
|
2496
2615
|
});
|
|
2497
2616
|
}
|
|
2617
|
+
if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
|
|
2618
|
+
await this.oauthProvider.initializeCredentials();
|
|
2619
|
+
}
|
|
2498
2620
|
}
|
|
2499
2621
|
/**
|
|
2500
2622
|
* Saves current session state to the session store
|
|
@@ -2502,34 +2624,14 @@ var MCPClient = class {
|
|
|
2502
2624
|
* @param status - Session lifecycle status used by storage cleanup
|
|
2503
2625
|
* @private
|
|
2504
2626
|
*/
|
|
2505
|
-
async saveSession(status = "active"
|
|
2506
|
-
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2627
|
+
async saveSession(status = "active") {
|
|
2628
|
+
if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
|
|
2507
2629
|
return;
|
|
2508
2630
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
userId: this.userId,
|
|
2512
|
-
serverId: this.serverId,
|
|
2513
|
-
serverName: this.serverName,
|
|
2514
|
-
serverUrl: this.serverUrl,
|
|
2515
|
-
callbackUrl: this.callbackUrl,
|
|
2516
|
-
transportType: this.transportType || "streamable-http",
|
|
2517
|
-
headers: this.headers,
|
|
2518
|
-
createdAt: this.createdAt || Date.now(),
|
|
2519
|
-
updatedAt: Date.now(),
|
|
2631
|
+
await this._store.update(this.config.userId, this.config.sessionId, {
|
|
2632
|
+
...this.session,
|
|
2520
2633
|
status
|
|
2521
|
-
};
|
|
2522
|
-
if (status === "active") {
|
|
2523
|
-
sessionData.authUrl = null;
|
|
2524
|
-
}
|
|
2525
|
-
if (existingSession === void 0) {
|
|
2526
|
-
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2527
|
-
}
|
|
2528
|
-
if (existingSession) {
|
|
2529
|
-
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2530
|
-
} else {
|
|
2531
|
-
await sessions.create(sessionData);
|
|
2532
|
-
}
|
|
2634
|
+
});
|
|
2533
2635
|
}
|
|
2534
2636
|
/**
|
|
2535
2637
|
* Removes transient setup/auth sessions without masking the original error.
|
|
@@ -2537,7 +2639,7 @@ var MCPClient = class {
|
|
|
2537
2639
|
*/
|
|
2538
2640
|
async deleteTransientSession() {
|
|
2539
2641
|
try {
|
|
2540
|
-
await
|
|
2642
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2541
2643
|
} catch {
|
|
2542
2644
|
}
|
|
2543
2645
|
}
|
|
@@ -2547,7 +2649,7 @@ var MCPClient = class {
|
|
|
2547
2649
|
* @private
|
|
2548
2650
|
*/
|
|
2549
2651
|
async tryConnect() {
|
|
2550
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2652
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2551
2653
|
let lastError;
|
|
2552
2654
|
for (const currentType of transportsToTry) {
|
|
2553
2655
|
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
@@ -2555,6 +2657,7 @@ var MCPClient = class {
|
|
|
2555
2657
|
const transport = this.getTransport(currentType);
|
|
2556
2658
|
this.transport = transport;
|
|
2557
2659
|
await this.client.connect(transport);
|
|
2660
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2558
2661
|
return { transportType: currentType };
|
|
2559
2662
|
} catch (error) {
|
|
2560
2663
|
lastError = error;
|
|
@@ -2570,8 +2673,8 @@ var MCPClient = class {
|
|
|
2570
2673
|
this._onObservabilityEvent.fire({
|
|
2571
2674
|
level: "warn",
|
|
2572
2675
|
message: `Transport ${currentType} failed, falling back`,
|
|
2573
|
-
sessionId: this.sessionId,
|
|
2574
|
-
serverId: this.serverId,
|
|
2676
|
+
sessionId: this.config.sessionId,
|
|
2677
|
+
serverId: this.config.serverId,
|
|
2575
2678
|
metadata: {
|
|
2576
2679
|
failedTransport: currentType,
|
|
2577
2680
|
error: errorMessage
|
|
@@ -2614,15 +2717,15 @@ var MCPClient = class {
|
|
|
2614
2717
|
try {
|
|
2615
2718
|
this.emitStateChange("CONNECTING");
|
|
2616
2719
|
const { transportType } = await this.tryConnect();
|
|
2617
|
-
this.transportType = transportType;
|
|
2720
|
+
this.config.transportType = transportType;
|
|
2618
2721
|
this.emitStateChange("CONNECTED");
|
|
2619
2722
|
this.emitProgress("Connected successfully");
|
|
2620
2723
|
this._onObservabilityEvent.fire({
|
|
2621
2724
|
type: "mcp:client:session_saved",
|
|
2622
2725
|
level: "info",
|
|
2623
|
-
message: `Saving active session ${this.sessionId} (connect success)`,
|
|
2624
|
-
sessionId: this.sessionId,
|
|
2625
|
-
serverId: this.serverId,
|
|
2726
|
+
message: `Saving active session ${this.config.sessionId} (connect success)`,
|
|
2727
|
+
sessionId: this.config.sessionId,
|
|
2728
|
+
serverId: this.config.serverId,
|
|
2626
2729
|
timestamp: Date.now(),
|
|
2627
2730
|
id: nanoid.nanoid()
|
|
2628
2731
|
});
|
|
@@ -2645,23 +2748,23 @@ var MCPClient = class {
|
|
|
2645
2748
|
this._onObservabilityEvent.fire({
|
|
2646
2749
|
type: "mcp:client:session_saved",
|
|
2647
2750
|
level: "info",
|
|
2648
|
-
message: `Saving pending OAuth session ${this.sessionId}`,
|
|
2649
|
-
sessionId: this.sessionId,
|
|
2650
|
-
serverId: this.serverId,
|
|
2751
|
+
message: `Saving pending OAuth session ${this.config.sessionId}`,
|
|
2752
|
+
sessionId: this.config.sessionId,
|
|
2753
|
+
serverId: this.config.serverId,
|
|
2651
2754
|
timestamp: Date.now(),
|
|
2652
2755
|
id: nanoid.nanoid()
|
|
2653
2756
|
});
|
|
2654
2757
|
await this.saveSession("pending");
|
|
2655
|
-
if (this.serverId) {
|
|
2758
|
+
if (this.config.serverId) {
|
|
2656
2759
|
this._onConnectionEvent.fire({
|
|
2657
2760
|
type: "auth_required",
|
|
2658
|
-
sessionId: this.sessionId,
|
|
2659
|
-
serverId: this.serverId,
|
|
2761
|
+
sessionId: this.config.sessionId,
|
|
2762
|
+
serverId: this.config.serverId,
|
|
2660
2763
|
authUrl,
|
|
2661
2764
|
timestamp: Date.now()
|
|
2662
2765
|
});
|
|
2663
|
-
if (authUrl && this.onRedirect) {
|
|
2664
|
-
this.onRedirect(authUrl);
|
|
2766
|
+
if (authUrl && this.config.onRedirect) {
|
|
2767
|
+
this.config.onRedirect(authUrl);
|
|
2665
2768
|
}
|
|
2666
2769
|
}
|
|
2667
2770
|
throw new UnauthorizedError("OAuth authorization required");
|
|
@@ -2670,9 +2773,9 @@ var MCPClient = class {
|
|
|
2670
2773
|
this.emitError(errorMessage, "connection");
|
|
2671
2774
|
this.emitStateChange("FAILED");
|
|
2672
2775
|
try {
|
|
2673
|
-
const existingSession = await
|
|
2776
|
+
const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2674
2777
|
if (!existingSession || existingSession.status !== "active") {
|
|
2675
|
-
await
|
|
2778
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2676
2779
|
}
|
|
2677
2780
|
} catch {
|
|
2678
2781
|
}
|
|
@@ -2697,16 +2800,16 @@ var MCPClient = class {
|
|
|
2697
2800
|
throw new Error(error);
|
|
2698
2801
|
}
|
|
2699
2802
|
if (state) {
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2803
|
+
try {
|
|
2804
|
+
await this.oauthProvider.consumeState(state);
|
|
2805
|
+
} catch (error) {
|
|
2806
|
+
const msg = error instanceof Error ? error.message : "Invalid OAuth state";
|
|
2807
|
+
this.emitError(msg, "auth");
|
|
2704
2808
|
this.emitStateChange("FAILED");
|
|
2705
|
-
throw
|
|
2809
|
+
throw error;
|
|
2706
2810
|
}
|
|
2707
|
-
await this.oauthProvider.consumeState(state);
|
|
2708
2811
|
}
|
|
2709
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2812
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2710
2813
|
let lastError;
|
|
2711
2814
|
let tokensExchanged = false;
|
|
2712
2815
|
let authenticatedStateEmitted = false;
|
|
@@ -2733,14 +2836,15 @@ var MCPClient = class {
|
|
|
2733
2836
|
}
|
|
2734
2837
|
}
|
|
2735
2838
|
await this.client.connect(this.transport);
|
|
2736
|
-
this.
|
|
2839
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2840
|
+
this.config.transportType = currentType;
|
|
2737
2841
|
this.emitStateChange("CONNECTED");
|
|
2738
2842
|
this._onObservabilityEvent.fire({
|
|
2739
2843
|
type: "mcp:client:session_saved",
|
|
2740
2844
|
level: "info",
|
|
2741
|
-
message: `Saving active session ${this.sessionId} (OAuth complete)`,
|
|
2742
|
-
sessionId: this.sessionId,
|
|
2743
|
-
serverId: this.serverId,
|
|
2845
|
+
message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
|
|
2846
|
+
sessionId: this.config.sessionId,
|
|
2847
|
+
serverId: this.config.serverId,
|
|
2744
2848
|
timestamp: Date.now(),
|
|
2745
2849
|
id: nanoid.nanoid()
|
|
2746
2850
|
});
|
|
@@ -2813,11 +2917,11 @@ var MCPClient = class {
|
|
|
2813
2917
|
this.emitStateChange("DISCOVERING");
|
|
2814
2918
|
try {
|
|
2815
2919
|
const result = await this.fetchTools();
|
|
2816
|
-
if (this.serverId) {
|
|
2920
|
+
if (this.config.serverId) {
|
|
2817
2921
|
this._onConnectionEvent.fire({
|
|
2818
2922
|
type: "tools_discovered",
|
|
2819
|
-
sessionId: this.sessionId,
|
|
2820
|
-
serverId: this.serverId,
|
|
2923
|
+
sessionId: this.config.sessionId,
|
|
2924
|
+
serverId: this.config.serverId,
|
|
2821
2925
|
toolCount: result.tools.length,
|
|
2822
2926
|
tools: result.tools,
|
|
2823
2927
|
timestamp: Date.now()
|
|
@@ -2857,8 +2961,8 @@ var MCPClient = class {
|
|
|
2857
2961
|
level: "info",
|
|
2858
2962
|
message: `Tool ${toolName} called successfully`,
|
|
2859
2963
|
displayMessage: `Called tool ${toolName}`,
|
|
2860
|
-
sessionId: this.sessionId,
|
|
2861
|
-
serverId: this.serverId,
|
|
2964
|
+
sessionId: this.config.sessionId,
|
|
2965
|
+
serverId: this.config.serverId,
|
|
2862
2966
|
payload: {
|
|
2863
2967
|
toolName,
|
|
2864
2968
|
args: toolArgs
|
|
@@ -2874,8 +2978,8 @@ var MCPClient = class {
|
|
|
2874
2978
|
level: "error",
|
|
2875
2979
|
message: errorMessage,
|
|
2876
2980
|
displayMessage: `Failed to call tool ${toolName}`,
|
|
2877
|
-
sessionId: this.sessionId,
|
|
2878
|
-
serverId: this.serverId,
|
|
2981
|
+
sessionId: this.config.sessionId,
|
|
2982
|
+
serverId: this.config.serverId,
|
|
2879
2983
|
payload: {
|
|
2880
2984
|
errorType: "tool_execution",
|
|
2881
2985
|
error: errorMessage,
|
|
@@ -3023,8 +3127,8 @@ var MCPClient = class {
|
|
|
3023
3127
|
type: "mcp:client:error",
|
|
3024
3128
|
level: "warn",
|
|
3025
3129
|
message: "Initialization failed during clearSession",
|
|
3026
|
-
sessionId: this.sessionId,
|
|
3027
|
-
serverId: this.serverId,
|
|
3130
|
+
sessionId: this.config.sessionId,
|
|
3131
|
+
serverId: this.config.serverId,
|
|
3028
3132
|
payload: { error: String(error) },
|
|
3029
3133
|
timestamp: Date.now(),
|
|
3030
3134
|
id: nanoid.nanoid()
|
|
@@ -3033,7 +3137,7 @@ var MCPClient = class {
|
|
|
3033
3137
|
if (this.oauthProvider) {
|
|
3034
3138
|
await this.oauthProvider.invalidateCredentials("all");
|
|
3035
3139
|
}
|
|
3036
|
-
await
|
|
3140
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
3037
3141
|
await this.disconnect();
|
|
3038
3142
|
}
|
|
3039
3143
|
/**
|
|
@@ -3067,19 +3171,19 @@ var MCPClient = class {
|
|
|
3067
3171
|
}
|
|
3068
3172
|
this.oauthProvider = null;
|
|
3069
3173
|
this.transport = null;
|
|
3070
|
-
if (this.serverId) {
|
|
3174
|
+
if (this.config.serverId) {
|
|
3071
3175
|
this._onConnectionEvent.fire({
|
|
3072
3176
|
type: "disconnected",
|
|
3073
|
-
sessionId: this.sessionId,
|
|
3074
|
-
serverId: this.serverId,
|
|
3177
|
+
sessionId: this.config.sessionId,
|
|
3178
|
+
serverId: this.config.serverId,
|
|
3075
3179
|
timestamp: Date.now()
|
|
3076
3180
|
});
|
|
3077
3181
|
this._onObservabilityEvent.fire({
|
|
3078
3182
|
type: "mcp:client:disconnect",
|
|
3079
3183
|
level: "info",
|
|
3080
|
-
message: `Disconnected from ${this.serverId}`,
|
|
3081
|
-
sessionId: this.sessionId,
|
|
3082
|
-
serverId: this.serverId,
|
|
3184
|
+
message: `Disconnected from ${this.config.serverId}`,
|
|
3185
|
+
sessionId: this.config.sessionId,
|
|
3186
|
+
serverId: this.config.serverId,
|
|
3083
3187
|
payload: {},
|
|
3084
3188
|
timestamp: Date.now(),
|
|
3085
3189
|
id: nanoid.nanoid()
|
|
@@ -3104,42 +3208,52 @@ var MCPClient = class {
|
|
|
3104
3208
|
* @returns Server URL or empty string if not set
|
|
3105
3209
|
*/
|
|
3106
3210
|
getServerUrl() {
|
|
3107
|
-
return this.serverUrl || "";
|
|
3211
|
+
return this.config.serverUrl || "";
|
|
3108
3212
|
}
|
|
3109
3213
|
/**
|
|
3110
3214
|
* Gets the OAuth callback URL
|
|
3111
3215
|
* @returns Callback URL or empty string if not set
|
|
3112
3216
|
*/
|
|
3113
3217
|
getCallbackUrl() {
|
|
3114
|
-
return this.callbackUrl || "";
|
|
3218
|
+
return this.config.callbackUrl || "";
|
|
3115
3219
|
}
|
|
3116
3220
|
/**
|
|
3117
3221
|
* Gets the transport type being used
|
|
3118
3222
|
* @returns Transport type (defaults to 'streamable-http')
|
|
3119
3223
|
*/
|
|
3120
3224
|
getTransportType() {
|
|
3121
|
-
return this.transportType || "streamable-http";
|
|
3225
|
+
return this.config.transportType || "streamable-http";
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Gets the full server metadata from the MCP initialize response.
|
|
3229
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3230
|
+
* Returns undefined if the client hasn't connected yet.
|
|
3231
|
+
*/
|
|
3232
|
+
getServerInfo() {
|
|
3233
|
+
return this._serverInfo;
|
|
3122
3234
|
}
|
|
3123
3235
|
/**
|
|
3124
|
-
* Gets the human-readable server name
|
|
3236
|
+
* Gets the human-readable server name.
|
|
3237
|
+
* Prefers the server's reported title/name from the initialize response,
|
|
3238
|
+
* falling back to the name provided at construction or session metadata.
|
|
3125
3239
|
* @returns Server name or undefined
|
|
3126
3240
|
*/
|
|
3127
3241
|
getServerName() {
|
|
3128
|
-
return this.serverName;
|
|
3242
|
+
return this.config.serverName;
|
|
3129
3243
|
}
|
|
3130
3244
|
/**
|
|
3131
3245
|
* Gets the server ID
|
|
3132
3246
|
* @returns Server ID or undefined
|
|
3133
3247
|
*/
|
|
3134
3248
|
getServerId() {
|
|
3135
|
-
return this.serverId;
|
|
3249
|
+
return this.config.serverId;
|
|
3136
3250
|
}
|
|
3137
3251
|
/**
|
|
3138
3252
|
* Gets the session ID
|
|
3139
3253
|
* @returns Session ID
|
|
3140
3254
|
*/
|
|
3141
3255
|
getSessionId() {
|
|
3142
|
-
return this.sessionId;
|
|
3256
|
+
return this.config.sessionId;
|
|
3143
3257
|
}
|
|
3144
3258
|
};
|
|
3145
3259
|
|
|
@@ -3166,6 +3280,14 @@ var ToolPolicyGateway = class {
|
|
|
3166
3280
|
getServerId() {
|
|
3167
3281
|
return this.client.getServerId?.();
|
|
3168
3282
|
}
|
|
3283
|
+
/**
|
|
3284
|
+
* Returns the full server metadata from the underlying client, if available.
|
|
3285
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3286
|
+
* Available only after the client has connected and completed initialization.
|
|
3287
|
+
*/
|
|
3288
|
+
getServerInfo() {
|
|
3289
|
+
return this.client.getServerInfo?.();
|
|
3290
|
+
}
|
|
3169
3291
|
/**
|
|
3170
3292
|
* Returns the human-readable server name from the underlying client, if available.
|
|
3171
3293
|
*/
|
|
@@ -3294,6 +3416,7 @@ var MultiSessionClient = class {
|
|
|
3294
3416
|
__publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
|
|
3295
3417
|
__publicField(this, "userId");
|
|
3296
3418
|
__publicField(this, "options");
|
|
3419
|
+
__publicField(this, "_store");
|
|
3297
3420
|
this.userId = userId;
|
|
3298
3421
|
this.options = {
|
|
3299
3422
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
@@ -3301,6 +3424,7 @@ var MultiSessionClient = class {
|
|
|
3301
3424
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
3302
3425
|
...options
|
|
3303
3426
|
};
|
|
3427
|
+
this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
|
|
3304
3428
|
}
|
|
3305
3429
|
// -----------------------------------------------------------------------
|
|
3306
3430
|
// Public API
|
|
@@ -3384,9 +3508,9 @@ var MultiSessionClient = class {
|
|
|
3384
3508
|
* to querying the storage backend via `sessions.list(userId)`.
|
|
3385
3509
|
*/
|
|
3386
3510
|
async fetchActiveSessions() {
|
|
3387
|
-
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await
|
|
3511
|
+
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
|
|
3388
3512
|
return sessionList.filter(
|
|
3389
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
3513
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
|
|
3390
3514
|
);
|
|
3391
3515
|
}
|
|
3392
3516
|
/**
|
|
@@ -3459,8 +3583,13 @@ var MultiSessionClient = class {
|
|
|
3459
3583
|
callbackUrl: session.callbackUrl,
|
|
3460
3584
|
serverName: session.serverName,
|
|
3461
3585
|
transportType: session.transportType,
|
|
3462
|
-
headers: session.headers
|
|
3586
|
+
headers: session.headers,
|
|
3587
|
+
hasSession: true,
|
|
3588
|
+
sessionStore: this._store
|
|
3463
3589
|
});
|
|
3590
|
+
if (this.options.onObservabilityEvent) {
|
|
3591
|
+
client.onObservabilityEvent(this.options.onObservabilityEvent);
|
|
3592
|
+
}
|
|
3464
3593
|
const timeoutMs = this.options.timeout;
|
|
3465
3594
|
let timeoutTimer;
|
|
3466
3595
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3519,130 +3648,188 @@ function isConnectionEvent(event) {
|
|
|
3519
3648
|
}
|
|
3520
3649
|
|
|
3521
3650
|
// src/server/handlers/sse-handler.ts
|
|
3522
|
-
var
|
|
3651
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
3523
3652
|
function normalizeHeaders(headers) {
|
|
3524
3653
|
if (!headers || typeof headers !== "object") return void 0;
|
|
3525
|
-
const entries = Object.entries(headers).map(([
|
|
3654
|
+
const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
|
|
3526
3655
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3527
3656
|
}
|
|
3657
|
+
function validatingStateEvent(sessionId, session) {
|
|
3658
|
+
return {
|
|
3659
|
+
type: "state_changed",
|
|
3660
|
+
sessionId,
|
|
3661
|
+
serverId: session.serverId ?? "unknown",
|
|
3662
|
+
serverName: session.serverName ?? "Unknown",
|
|
3663
|
+
serverUrl: session.serverUrl,
|
|
3664
|
+
state: "VALIDATING",
|
|
3665
|
+
previousState: "DISCONNECTED",
|
|
3666
|
+
timestamp: Date.now()
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
function connectionErrorEvent(sessionId, serverId, error, errorType) {
|
|
3670
|
+
return {
|
|
3671
|
+
type: "error",
|
|
3672
|
+
sessionId,
|
|
3673
|
+
serverId: serverId ?? "unknown",
|
|
3674
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
3675
|
+
errorType,
|
|
3676
|
+
timestamp: Date.now()
|
|
3677
|
+
};
|
|
3678
|
+
}
|
|
3528
3679
|
var SSEConnectionManager = class {
|
|
3680
|
+
// -----------------------------------------------------------------------
|
|
3681
|
+
// Constructor
|
|
3682
|
+
// -----------------------------------------------------------------------
|
|
3683
|
+
/**
|
|
3684
|
+
* @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
|
|
3685
|
+
* @param sendEvent - Callback that writes a typed event onto the SSE stream.
|
|
3686
|
+
*/
|
|
3529
3687
|
constructor(options, sendEvent) {
|
|
3530
3688
|
this.options = options;
|
|
3531
3689
|
this.sendEvent = sendEvent;
|
|
3690
|
+
// -----------------------------------------------------------------------
|
|
3691
|
+
// State
|
|
3692
|
+
// -----------------------------------------------------------------------
|
|
3532
3693
|
__publicField(this, "userId");
|
|
3694
|
+
/** Active MCP transports keyed by sessionId. */
|
|
3533
3695
|
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
3696
|
+
/** Instrumented session store — always wraps `sessions` with DB observability. */
|
|
3697
|
+
__publicField(this, "observedStore");
|
|
3534
3698
|
__publicField(this, "heartbeatTimer");
|
|
3535
|
-
__publicField(this, "
|
|
3699
|
+
__publicField(this, "active", true);
|
|
3536
3700
|
this.userId = options.userId;
|
|
3701
|
+
this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
|
|
3537
3702
|
this.startHeartbeat();
|
|
3538
3703
|
}
|
|
3704
|
+
// -----------------------------------------------------------------------
|
|
3705
|
+
// Public API
|
|
3706
|
+
// -----------------------------------------------------------------------
|
|
3539
3707
|
/**
|
|
3540
|
-
*
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
}
|
|
3551
|
-
return metadata;
|
|
3552
|
-
}
|
|
3553
|
-
/**
|
|
3554
|
-
* Start heartbeat to keep connection alive
|
|
3555
|
-
*/
|
|
3556
|
-
startHeartbeat() {
|
|
3557
|
-
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
3558
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3559
|
-
if (this.isActive) {
|
|
3560
|
-
this.sendEvent({
|
|
3561
|
-
level: "debug",
|
|
3562
|
-
message: "heartbeat",
|
|
3563
|
-
timestamp: Date.now()
|
|
3564
|
-
});
|
|
3565
|
-
}
|
|
3566
|
-
}, interval);
|
|
3567
|
-
}
|
|
3568
|
-
/**
|
|
3569
|
-
* Handle incoming RPC requests
|
|
3570
|
-
* Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
|
|
3708
|
+
* Dispatches an incoming RPC request, emits timing observability,
|
|
3709
|
+
* and returns the response.
|
|
3710
|
+
*
|
|
3711
|
+
* Emits `rpc:start` before dispatch and `rpc:end` on completion
|
|
3712
|
+
* (success or error), each carrying the method name, sessionId, and
|
|
3713
|
+
* duration. All events flow through the unified `onObservability`
|
|
3714
|
+
* and the SSE stream.
|
|
3715
|
+
*
|
|
3716
|
+
* @param request - The deserialized RPC envelope.
|
|
3717
|
+
* @returns The RPC response (success or error).
|
|
3571
3718
|
*/
|
|
3572
3719
|
async handleRequest(request) {
|
|
3720
|
+
const method = request.method;
|
|
3721
|
+
const sessionId = request.params?.sessionId;
|
|
3722
|
+
const t0 = performance.now();
|
|
3723
|
+
this.sendEvent({
|
|
3724
|
+
type: "rpc:start",
|
|
3725
|
+
level: "debug",
|
|
3726
|
+
message: method,
|
|
3727
|
+
sessionId,
|
|
3728
|
+
timestamp: Date.now()
|
|
3729
|
+
});
|
|
3573
3730
|
try {
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
break;
|
|
3585
|
-
case "disconnect":
|
|
3586
|
-
result = await this.disconnect(request.params);
|
|
3587
|
-
break;
|
|
3588
|
-
case "listTools":
|
|
3589
|
-
result = await this.listTools(request.params);
|
|
3590
|
-
break;
|
|
3591
|
-
case "setToolPolicy":
|
|
3592
|
-
result = await this.setToolPolicy(request.params);
|
|
3593
|
-
break;
|
|
3594
|
-
case "getToolPolicy":
|
|
3595
|
-
result = await this.getToolPolicy(request.params);
|
|
3596
|
-
break;
|
|
3597
|
-
case "callTool":
|
|
3598
|
-
result = await this.callTool(request.params);
|
|
3599
|
-
break;
|
|
3600
|
-
case "getSession":
|
|
3601
|
-
result = await this.getSession(request.params);
|
|
3602
|
-
break;
|
|
3603
|
-
case "finishAuth":
|
|
3604
|
-
result = await this.finishAuth(request.params);
|
|
3605
|
-
break;
|
|
3606
|
-
case "listPrompts":
|
|
3607
|
-
result = await this.listPrompts(request.params);
|
|
3608
|
-
break;
|
|
3609
|
-
case "getPrompt":
|
|
3610
|
-
result = await this.getPrompt(request.params);
|
|
3611
|
-
break;
|
|
3612
|
-
case "listResources":
|
|
3613
|
-
result = await this.listResources(request.params);
|
|
3614
|
-
break;
|
|
3615
|
-
case "readResource":
|
|
3616
|
-
result = await this.readResource(request.params);
|
|
3617
|
-
break;
|
|
3618
|
-
default:
|
|
3619
|
-
throw new Error(`Unknown method: ${request.method}`);
|
|
3620
|
-
}
|
|
3621
|
-
const response = {
|
|
3622
|
-
id: request.id,
|
|
3623
|
-
result
|
|
3624
|
-
};
|
|
3731
|
+
const result = await this.dispatchImpl(request);
|
|
3732
|
+
this.sendEvent({
|
|
3733
|
+
type: "rpc:end",
|
|
3734
|
+
level: "debug",
|
|
3735
|
+
message: method,
|
|
3736
|
+
sessionId,
|
|
3737
|
+
payload: { durationMs: performance.now() - t0 },
|
|
3738
|
+
timestamp: Date.now()
|
|
3739
|
+
});
|
|
3740
|
+
const response = { id: request.id, result };
|
|
3625
3741
|
this.sendEvent(response);
|
|
3626
3742
|
return response;
|
|
3627
3743
|
} catch (error) {
|
|
3628
|
-
|
|
3744
|
+
this.sendEvent({
|
|
3745
|
+
type: "rpc:end",
|
|
3746
|
+
level: "error",
|
|
3747
|
+
message: method,
|
|
3748
|
+
sessionId,
|
|
3749
|
+
payload: { durationMs: performance.now() - t0, error: String(error) },
|
|
3750
|
+
timestamp: Date.now()
|
|
3751
|
+
});
|
|
3752
|
+
const response = {
|
|
3629
3753
|
id: request.id,
|
|
3630
3754
|
error: {
|
|
3631
3755
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
3632
3756
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
3633
3757
|
}
|
|
3634
3758
|
};
|
|
3635
|
-
this.sendEvent(
|
|
3636
|
-
return
|
|
3759
|
+
this.sendEvent(response);
|
|
3760
|
+
return response;
|
|
3761
|
+
}
|
|
3762
|
+
}
|
|
3763
|
+
/**
|
|
3764
|
+
* Tears down all active MCP transports and stops the heartbeat timer.
|
|
3765
|
+
*
|
|
3766
|
+
* Disconnects are issued in parallel across all sessions. After calling
|
|
3767
|
+
* this method the manager instance should be discarded.
|
|
3768
|
+
*/
|
|
3769
|
+
async dispose() {
|
|
3770
|
+
this.active = false;
|
|
3771
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
3772
|
+
await Promise.all(
|
|
3773
|
+
Array.from(this.clients.values()).map((c) => c.disconnect())
|
|
3774
|
+
);
|
|
3775
|
+
this.clients.clear();
|
|
3776
|
+
}
|
|
3777
|
+
// -----------------------------------------------------------------------
|
|
3778
|
+
// RPC Dispatch (raw — called by handleRequest which adds timing)
|
|
3779
|
+
// -----------------------------------------------------------------------
|
|
3780
|
+
/**
|
|
3781
|
+
* Routes an RPC method name to the appropriate private handler.
|
|
3782
|
+
*
|
|
3783
|
+
* @throws {Error} When the method name is unrecognized.
|
|
3784
|
+
*/
|
|
3785
|
+
async dispatchImpl(request) {
|
|
3786
|
+
switch (request.method) {
|
|
3787
|
+
case "listSessions":
|
|
3788
|
+
return this.listSessions();
|
|
3789
|
+
case "connect":
|
|
3790
|
+
return this.connect(request.params);
|
|
3791
|
+
case "reconnect":
|
|
3792
|
+
return this.reconnect(request.params);
|
|
3793
|
+
case "disconnect":
|
|
3794
|
+
return this.disconnect(request.params);
|
|
3795
|
+
case "listTools":
|
|
3796
|
+
return this.listTools(request.params);
|
|
3797
|
+
case "setToolPolicy":
|
|
3798
|
+
return this.setToolPolicy(request.params);
|
|
3799
|
+
case "getToolPolicy":
|
|
3800
|
+
return this.getToolPolicy(request.params);
|
|
3801
|
+
case "updateSession":
|
|
3802
|
+
return this.updateSession(request.params);
|
|
3803
|
+
case "callTool":
|
|
3804
|
+
return this.callTool(request.params);
|
|
3805
|
+
case "getSession":
|
|
3806
|
+
return this.getSession(request.params);
|
|
3807
|
+
case "finishAuth":
|
|
3808
|
+
return this.finishAuth(request.params);
|
|
3809
|
+
case "listPrompts":
|
|
3810
|
+
return this.listPrompts(request.params);
|
|
3811
|
+
case "getPrompt":
|
|
3812
|
+
return this.getPrompt(request.params);
|
|
3813
|
+
case "listResources":
|
|
3814
|
+
return this.listResources(request.params);
|
|
3815
|
+
case "readResource":
|
|
3816
|
+
return this.readResource(request.params);
|
|
3817
|
+
default:
|
|
3818
|
+
throw new Error(`Unknown RPC method: ${request.method}`);
|
|
3637
3819
|
}
|
|
3638
3820
|
}
|
|
3821
|
+
// -----------------------------------------------------------------------
|
|
3822
|
+
// Session Query
|
|
3823
|
+
// -----------------------------------------------------------------------
|
|
3639
3824
|
/**
|
|
3640
|
-
*
|
|
3825
|
+
* Lists all sessions owned by the current user.
|
|
3826
|
+
*
|
|
3827
|
+
* Returns a lightweight view — session metadata only, no credential fields.
|
|
3641
3828
|
*/
|
|
3642
3829
|
async listSessions() {
|
|
3643
|
-
const
|
|
3830
|
+
const all = await sessions.list(this.userId);
|
|
3644
3831
|
return {
|
|
3645
|
-
sessions:
|
|
3832
|
+
sessions: all.map((s) => ({
|
|
3646
3833
|
sessionId: s.sessionId,
|
|
3647
3834
|
serverId: s.serverId,
|
|
3648
3835
|
serverName: s.serverName,
|
|
@@ -3651,215 +3838,203 @@ var SSEConnectionManager = class {
|
|
|
3651
3838
|
createdAt: s.createdAt,
|
|
3652
3839
|
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3653
3840
|
status: s.status ?? "pending",
|
|
3654
|
-
toolPolicy: s.toolPolicy
|
|
3841
|
+
toolPolicy: s.toolPolicy,
|
|
3842
|
+
enabled: s.enabled ?? true
|
|
3655
3843
|
}))
|
|
3656
3844
|
};
|
|
3657
3845
|
}
|
|
3658
3846
|
/**
|
|
3659
|
-
*
|
|
3847
|
+
* Restores and validates a previously persisted session.
|
|
3848
|
+
*
|
|
3849
|
+
* Loads the full session row (including credentials) from storage, creates
|
|
3850
|
+
* a new MCP transport, connects to the remote server, and emits the
|
|
3851
|
+
* discovered (policy-filtered) tool list via SSE.
|
|
3660
3852
|
*/
|
|
3661
|
-
async
|
|
3662
|
-
const
|
|
3663
|
-
|
|
3664
|
-
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
|
|
3665
|
-
const existingSessions = await sessions.list(this.userId);
|
|
3666
|
-
const duplicate = existingSessions.find(
|
|
3667
|
-
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
3668
|
-
);
|
|
3669
|
-
if (duplicate) {
|
|
3670
|
-
if (duplicate.status === "pending") {
|
|
3671
|
-
await this.getSession({ sessionId: duplicate.sessionId });
|
|
3672
|
-
return {
|
|
3673
|
-
sessionId: duplicate.sessionId,
|
|
3674
|
-
success: true
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
3678
|
-
}
|
|
3679
|
-
const sessionId = await sessions.generateSessionId();
|
|
3853
|
+
async getSession(params) {
|
|
3854
|
+
const session = await this.requireSession(params.sessionId);
|
|
3855
|
+
this.sendEvent(validatingStateEvent(params.sessionId, session));
|
|
3680
3856
|
try {
|
|
3681
|
-
const
|
|
3682
|
-
|
|
3683
|
-
userId: this.userId,
|
|
3684
|
-
sessionId,
|
|
3685
|
-
serverId,
|
|
3686
|
-
serverName,
|
|
3687
|
-
serverUrl,
|
|
3688
|
-
callbackUrl,
|
|
3689
|
-
transportType,
|
|
3690
|
-
headers,
|
|
3691
|
-
clientId,
|
|
3692
|
-
clientSecret,
|
|
3693
|
-
...clientMetadata
|
|
3694
|
-
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
3695
|
-
});
|
|
3696
|
-
this.clients.set(sessionId, client);
|
|
3697
|
-
client.onConnectionEvent((event) => {
|
|
3698
|
-
this.emitConnectionEvent(event);
|
|
3699
|
-
});
|
|
3700
|
-
client.onObservabilityEvent((event) => {
|
|
3701
|
-
this.sendEvent(event);
|
|
3702
|
-
});
|
|
3857
|
+
const client = this.restoreClient(session);
|
|
3858
|
+
this.attachClientEvents(client);
|
|
3703
3859
|
await client.connect();
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
success: true
|
|
3708
|
-
};
|
|
3860
|
+
this.clients.set(params.sessionId, client);
|
|
3861
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3862
|
+
return { success: true, toolCount: result.tools.length };
|
|
3709
3863
|
} catch (error) {
|
|
3710
|
-
|
|
3711
|
-
this.clients.delete(sessionId);
|
|
3712
|
-
return {
|
|
3713
|
-
sessionId,
|
|
3714
|
-
success: true
|
|
3715
|
-
};
|
|
3716
|
-
}
|
|
3717
|
-
this.emitConnectionEvent({
|
|
3718
|
-
type: "error",
|
|
3719
|
-
sessionId,
|
|
3720
|
-
serverId,
|
|
3721
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3722
|
-
errorType: "connection",
|
|
3723
|
-
timestamp: Date.now()
|
|
3724
|
-
});
|
|
3725
|
-
this.clients.delete(sessionId);
|
|
3864
|
+
this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
|
|
3726
3865
|
throw error;
|
|
3727
3866
|
}
|
|
3728
3867
|
}
|
|
3868
|
+
// -----------------------------------------------------------------------
|
|
3869
|
+
// Connection Lifecycle
|
|
3870
|
+
// -----------------------------------------------------------------------
|
|
3729
3871
|
/**
|
|
3730
|
-
*
|
|
3731
|
-
*
|
|
3872
|
+
* Initiates a connection to a new MCP server.
|
|
3873
|
+
*
|
|
3874
|
+
* If a session for the same `serverId` or `serverUrl` already exists and
|
|
3875
|
+
* is still in a `pending` (OAuth) state, the existing session is resumed
|
|
3876
|
+
* instead of creating a duplicate.
|
|
3877
|
+
*
|
|
3878
|
+
* `UnauthorizedError` is treated as a pending-auth state and returned as
|
|
3879
|
+
* a successful result (the client will then redirect to the auth URL).
|
|
3732
3880
|
*/
|
|
3733
|
-
async
|
|
3734
|
-
const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
|
|
3881
|
+
async connect(params) {
|
|
3735
3882
|
const headers = normalizeHeaders(params.headers);
|
|
3736
|
-
const serverId =
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3739
|
-
(
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
if (existingClient) {
|
|
3745
|
-
await existingClient.disconnect();
|
|
3746
|
-
this.clients.delete(duplicate.sessionId);
|
|
3883
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3884
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3885
|
+
if (existing) {
|
|
3886
|
+
if (existing.status === "pending") {
|
|
3887
|
+
return this.getSession({ sessionId: existing.sessionId }).then(() => ({
|
|
3888
|
+
sessionId: existing.sessionId,
|
|
3889
|
+
success: true
|
|
3890
|
+
}));
|
|
3747
3891
|
}
|
|
3892
|
+
throw new Error(
|
|
3893
|
+
`Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
|
|
3894
|
+
);
|
|
3748
3895
|
}
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3896
|
+
const sessionId = await sessions.generateSessionId();
|
|
3897
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3898
|
+
const client = new MCPClient({
|
|
3899
|
+
userId: this.userId,
|
|
3900
|
+
sessionId,
|
|
3901
|
+
serverId,
|
|
3902
|
+
serverName: params.serverName,
|
|
3903
|
+
serverUrl: params.serverUrl,
|
|
3904
|
+
callbackUrl: params.callbackUrl,
|
|
3905
|
+
transportType: params.transportType,
|
|
3906
|
+
headers,
|
|
3907
|
+
sessionStore: this.observedStore,
|
|
3908
|
+
...metadata
|
|
3909
|
+
});
|
|
3910
|
+
this.cacheClient(sessionId, client);
|
|
3911
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3912
|
+
}
|
|
3913
|
+
/**
|
|
3914
|
+
* Reconnects to an MCP server by tearing down the active transport if one
|
|
3915
|
+
* exists and instantiating a fresh connection, reusing the existing session
|
|
3916
|
+
* (and its stored credentials / DCR client info) from the database.
|
|
3917
|
+
*/
|
|
3918
|
+
async reconnect(params) {
|
|
3919
|
+
const headers = normalizeHeaders(params.headers);
|
|
3920
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3921
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3922
|
+
const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
|
|
3923
|
+
if (existing) {
|
|
3924
|
+
const staleClient = this.clients.get(existing.sessionId);
|
|
3925
|
+
if (staleClient) {
|
|
3926
|
+
await staleClient.disconnect();
|
|
3927
|
+
this.clients.delete(existing.sessionId);
|
|
3778
3928
|
}
|
|
3779
|
-
this.emitConnectionEvent({
|
|
3780
|
-
type: "error",
|
|
3781
|
-
sessionId,
|
|
3782
|
-
serverId,
|
|
3783
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3784
|
-
errorType: "connection",
|
|
3785
|
-
timestamp: Date.now()
|
|
3786
|
-
});
|
|
3787
|
-
this.clients.delete(sessionId);
|
|
3788
|
-
throw error;
|
|
3789
3929
|
}
|
|
3930
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3931
|
+
const client = new MCPClient({
|
|
3932
|
+
userId: this.userId,
|
|
3933
|
+
sessionId,
|
|
3934
|
+
serverId,
|
|
3935
|
+
serverName: params.serverName,
|
|
3936
|
+
serverUrl: params.serverUrl,
|
|
3937
|
+
callbackUrl: params.callbackUrl,
|
|
3938
|
+
transportType: params.transportType,
|
|
3939
|
+
headers,
|
|
3940
|
+
sessionStore: this.observedStore,
|
|
3941
|
+
...metadata
|
|
3942
|
+
});
|
|
3943
|
+
this.cacheClient(sessionId, client);
|
|
3944
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3790
3945
|
}
|
|
3791
3946
|
/**
|
|
3792
|
-
*
|
|
3947
|
+
* Disconnects from an MCP server.
|
|
3948
|
+
*
|
|
3949
|
+
* If an active in-memory transport exists it delegates to `clearSession()`
|
|
3950
|
+
* (which sends the server an HTTP DELETE per the Streamable spec). If no
|
|
3951
|
+
* active transport is available (orphaned session from a failed OAuth flow),
|
|
3952
|
+
* the session row is removed directly from storage.
|
|
3793
3953
|
*/
|
|
3794
3954
|
async disconnect(params) {
|
|
3795
|
-
const
|
|
3796
|
-
const client = this.clients.get(sessionId);
|
|
3955
|
+
const client = this.clients.get(params.sessionId);
|
|
3797
3956
|
if (client) {
|
|
3798
3957
|
await client.clearSession();
|
|
3799
|
-
this.clients.delete(sessionId);
|
|
3958
|
+
this.clients.delete(params.sessionId);
|
|
3800
3959
|
} else {
|
|
3801
|
-
await sessions.delete(this.userId, sessionId);
|
|
3960
|
+
await sessions.delete(this.userId, params.sessionId);
|
|
3802
3961
|
}
|
|
3803
3962
|
return { success: true };
|
|
3804
3963
|
}
|
|
3964
|
+
// -----------------------------------------------------------------------
|
|
3965
|
+
// OAuth
|
|
3966
|
+
// -----------------------------------------------------------------------
|
|
3805
3967
|
/**
|
|
3806
|
-
*
|
|
3968
|
+
* Completes the OAuth 2.1 authorization code flow for a pending session.
|
|
3969
|
+
*
|
|
3970
|
+
* Loads the stored session (with credentials), creates a fresh `MCPClient`,
|
|
3971
|
+
* and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
|
|
3972
|
+
* so the PKCE code verifier is available without a DB read.
|
|
3973
|
+
*
|
|
3974
|
+
* The session's stored `transportType` is intentionally **omitted** from the
|
|
3975
|
+
* client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
|
|
3976
|
+
* first, fall back to SSE), which is required for servers like Neon that
|
|
3977
|
+
* only support SSE transport.
|
|
3807
3978
|
*/
|
|
3808
|
-
async
|
|
3809
|
-
const
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3979
|
+
async finishAuth(params) {
|
|
3980
|
+
const parsed = parseOAuthState(params.state);
|
|
3981
|
+
const sessionId = parsed?.sessionId ?? params.state;
|
|
3982
|
+
const session = await this.requireSession(sessionId);
|
|
3983
|
+
try {
|
|
3984
|
+
const clientId = session.clientId ?? void 0;
|
|
3985
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
3986
|
+
const client = new MCPClient({
|
|
3987
|
+
userId: this.userId,
|
|
3988
|
+
sessionId,
|
|
3989
|
+
serverId: session.serverId,
|
|
3990
|
+
serverName: session.serverName,
|
|
3991
|
+
serverUrl: session.serverUrl,
|
|
3992
|
+
callbackUrl: session.callbackUrl,
|
|
3993
|
+
headers: session.headers,
|
|
3994
|
+
clientId,
|
|
3995
|
+
clientSecret,
|
|
3996
|
+
hasSession: true,
|
|
3997
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
3998
|
+
sessionStore: this.observedStore
|
|
3999
|
+
});
|
|
4000
|
+
this.attachClientEvents(client);
|
|
4001
|
+
await runWithCodeVerifierState(
|
|
4002
|
+
session.codeVerifier ?? "",
|
|
4003
|
+
"S256",
|
|
4004
|
+
() => client.finishAuth(params.code, params.state)
|
|
4005
|
+
);
|
|
4006
|
+
this.clients.set(sessionId, client);
|
|
4007
|
+
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4008
|
+
return { success: true, toolCount: result.tools.length };
|
|
4009
|
+
} catch (error) {
|
|
4010
|
+
this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
|
|
4011
|
+
throw error;
|
|
3816
4012
|
}
|
|
3817
|
-
const client = new MCPClient({
|
|
3818
|
-
userId: this.userId,
|
|
3819
|
-
sessionId,
|
|
3820
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3821
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3822
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3823
|
-
// reloading during initialize().
|
|
3824
|
-
serverId: session.serverId,
|
|
3825
|
-
serverName: session.serverName,
|
|
3826
|
-
serverUrl: session.serverUrl,
|
|
3827
|
-
callbackUrl: session.callbackUrl,
|
|
3828
|
-
transportType: session.transportType,
|
|
3829
|
-
headers: session.headers
|
|
3830
|
-
});
|
|
3831
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3832
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3833
|
-
await client.connect();
|
|
3834
|
-
this.clients.set(sessionId, client);
|
|
3835
|
-
return client;
|
|
3836
4013
|
}
|
|
4014
|
+
// -----------------------------------------------------------------------
|
|
4015
|
+
// Tool Discovery & Policy
|
|
4016
|
+
// -----------------------------------------------------------------------
|
|
3837
4017
|
/**
|
|
3838
|
-
* Fetches
|
|
4018
|
+
* Fetches the complete tool list from the remote MCP server and emits a
|
|
4019
|
+
* `tools_discovered` SSE event with two lists:
|
|
3839
4020
|
*
|
|
3840
|
-
*
|
|
3841
|
-
* - `
|
|
3842
|
-
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
3843
|
-
* that blocked tools still appear as checkboxes in the dialog.
|
|
4021
|
+
* - `tools` — policy-filtered (what agents are allowed to call)
|
|
4022
|
+
* - `allTools` — complete, unfiltered (used by the management UI)
|
|
3844
4023
|
*
|
|
3845
|
-
* `fetchTools()`
|
|
3846
|
-
* `gateway.listTools()`
|
|
3847
|
-
* network round-trip is made regardless of how many callers follow.
|
|
4024
|
+
* Only a single network round-trip is made: `fetchTools()` populates the
|
|
4025
|
+
* in-memory cache, then `gateway.listTools()` reuses that cache.
|
|
3848
4026
|
*
|
|
3849
|
-
* @param sessionId - The session
|
|
3850
|
-
* @returns The session record and the
|
|
3851
|
-
* @throws {Error}
|
|
4027
|
+
* @param sessionId - The session to discover tools for.
|
|
4028
|
+
* @returns The session record and the filtered tool result.
|
|
4029
|
+
* @throws {Error} If the session does not exist in storage.
|
|
3852
4030
|
*/
|
|
3853
4031
|
async listPolicyFilteredTools(sessionId) {
|
|
3854
|
-
const session = await
|
|
3855
|
-
if (!session) {
|
|
3856
|
-
throw new Error("Session not found");
|
|
3857
|
-
}
|
|
4032
|
+
const session = await this.requireSession(sessionId);
|
|
3858
4033
|
const client = await this.getOrCreateClient(sessionId);
|
|
3859
4034
|
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
3860
4035
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
3861
4036
|
const result = await gateway.listTools();
|
|
3862
|
-
this.
|
|
4037
|
+
this.sendEvent({
|
|
3863
4038
|
type: "tools_discovered",
|
|
3864
4039
|
sessionId,
|
|
3865
4040
|
serverId: session.serverId ?? "unknown",
|
|
@@ -3871,254 +4046,254 @@ var SSEConnectionManager = class {
|
|
|
3871
4046
|
return { session, result };
|
|
3872
4047
|
}
|
|
3873
4048
|
/**
|
|
3874
|
-
* Returns the policy-filtered tool list for a session
|
|
3875
|
-
*
|
|
3876
|
-
*
|
|
4049
|
+
* Returns the policy-filtered tool list for a session.
|
|
4050
|
+
*
|
|
4051
|
+
* Delegates to {@link listPolicyFilteredTools}, which also emits a
|
|
4052
|
+
* `tools_discovered` SSE event to keep the client state current.
|
|
3877
4053
|
*/
|
|
3878
4054
|
async listTools(params) {
|
|
3879
|
-
const {
|
|
3880
|
-
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4055
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3881
4056
|
return { tools: result.tools };
|
|
3882
4057
|
}
|
|
3883
4058
|
/**
|
|
3884
|
-
*
|
|
4059
|
+
* Returns all raw tools annotated with their effective policy state
|
|
4060
|
+
* for display in the management UI.
|
|
3885
4061
|
*/
|
|
3886
4062
|
async getToolPolicy(params) {
|
|
3887
|
-
const
|
|
3888
|
-
const
|
|
3889
|
-
if (!session) {
|
|
3890
|
-
throw new Error("Session not found");
|
|
3891
|
-
}
|
|
3892
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4063
|
+
const session = await this.requireSession(params.sessionId);
|
|
4064
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3893
4065
|
const allTools = await client.fetchTools();
|
|
3894
|
-
const
|
|
4066
|
+
const policy = session.toolPolicy ?? {
|
|
3895
4067
|
mode: "all",
|
|
3896
4068
|
toolIds: [],
|
|
3897
4069
|
updatedAt: session.updatedAt ?? session.createdAt
|
|
3898
4070
|
};
|
|
3899
4071
|
const serverId = session.serverId ?? "unknown";
|
|
3900
|
-
const tools = allTools.tools.map((
|
|
3901
|
-
...
|
|
3902
|
-
toolId: createToolId(serverId,
|
|
3903
|
-
allowed: isToolAllowed(
|
|
4072
|
+
const tools = allTools.tools.map((t) => ({
|
|
4073
|
+
...t,
|
|
4074
|
+
toolId: createToolId(serverId, t.name),
|
|
4075
|
+
allowed: isToolAllowed(policy, t.name, session.serverId)
|
|
3904
4076
|
}));
|
|
3905
4077
|
return {
|
|
3906
|
-
toolPolicy,
|
|
4078
|
+
toolPolicy: policy,
|
|
3907
4079
|
tools,
|
|
3908
4080
|
toolCount: tools.length,
|
|
3909
|
-
allowedToolCount: tools.filter((
|
|
4081
|
+
allowedToolCount: tools.filter((t) => t.allowed).length
|
|
3910
4082
|
};
|
|
3911
4083
|
}
|
|
3912
4084
|
/**
|
|
3913
|
-
* Persists a new tool access policy
|
|
3914
|
-
*
|
|
4085
|
+
* Persists a new tool access policy and broadcasts the updated filtered
|
|
4086
|
+
* tool list via a `tools_discovered` SSE event.
|
|
3915
4087
|
*
|
|
3916
|
-
*
|
|
3917
|
-
*
|
|
3918
|
-
* an additional round-trip to the server.
|
|
3919
|
-
*
|
|
3920
|
-
* @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
|
|
3921
|
-
* @throws {Error} When the session does not exist or the policy references unknown tool IDs.
|
|
4088
|
+
* @throws {Error} If the session does not exist or the policy references
|
|
4089
|
+
* tool IDs that don't match any known tool.
|
|
3922
4090
|
*/
|
|
3923
4091
|
async setToolPolicy(params) {
|
|
3924
|
-
const
|
|
3925
|
-
const
|
|
3926
|
-
if (!session) {
|
|
3927
|
-
throw new Error("Session not found");
|
|
3928
|
-
}
|
|
3929
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4092
|
+
const session = await this.requireSession(params.sessionId);
|
|
4093
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3930
4094
|
const allTools = await client.fetchTools();
|
|
3931
|
-
const
|
|
3932
|
-
validateToolPolicyAgainstTools(
|
|
3933
|
-
await sessions.update(this.userId, sessionId, { toolPolicy });
|
|
3934
|
-
const
|
|
3935
|
-
|
|
4095
|
+
const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
|
|
4096
|
+
validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
|
|
4097
|
+
await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
|
|
4098
|
+
const filtered = createToolPolicyGateway(
|
|
4099
|
+
this.userId,
|
|
4100
|
+
params.sessionId,
|
|
4101
|
+
client
|
|
4102
|
+
).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
|
|
4103
|
+
this.sendEvent({
|
|
3936
4104
|
type: "tools_discovered",
|
|
3937
|
-
sessionId,
|
|
4105
|
+
sessionId: params.sessionId,
|
|
3938
4106
|
serverId: session.serverId ?? "unknown",
|
|
3939
|
-
toolCount:
|
|
3940
|
-
tools:
|
|
4107
|
+
toolCount: filtered.length,
|
|
4108
|
+
tools: filtered,
|
|
3941
4109
|
allTools: allTools.tools,
|
|
3942
4110
|
timestamp: Date.now()
|
|
3943
4111
|
});
|
|
3944
|
-
return {
|
|
3945
|
-
success: true,
|
|
3946
|
-
toolPolicy,
|
|
3947
|
-
tools: filteredTools,
|
|
3948
|
-
toolCount: filteredTools.length
|
|
3949
|
-
};
|
|
3950
|
-
}
|
|
3951
|
-
/**
|
|
3952
|
-
* Proxies a tool invocation to the remote MCP server.
|
|
3953
|
-
* Resolves the client for the given session and delegates to the tool router.
|
|
3954
|
-
*/
|
|
3955
|
-
async callTool(params) {
|
|
3956
|
-
const { sessionId, toolName, toolArgs } = params;
|
|
3957
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
3958
|
-
const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
|
|
3959
|
-
const meta = result._meta || {};
|
|
3960
|
-
return {
|
|
3961
|
-
...result,
|
|
3962
|
-
_meta: {
|
|
3963
|
-
...meta,
|
|
3964
|
-
sessionId
|
|
3965
|
-
}
|
|
3966
|
-
};
|
|
4112
|
+
return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
|
|
3967
4113
|
}
|
|
3968
4114
|
/**
|
|
3969
|
-
*
|
|
4115
|
+
* Enables or disables a session for agent tool discovery.
|
|
4116
|
+
*
|
|
4117
|
+
* Disabled sessions retain their OAuth tokens and connection metadata
|
|
4118
|
+
* but are hidden from `MultiSessionClient.connect()` and blocked from
|
|
4119
|
+
* RPC tool access. Re-enabling does not require re-authentication.
|
|
4120
|
+
*
|
|
4121
|
+
* @param params - `{ sessionId, enabled: boolean }`
|
|
4122
|
+
* @returns `{ success: true }`
|
|
4123
|
+
* @throws {Error} If the session does not exist.
|
|
3970
4124
|
*/
|
|
3971
|
-
async
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
throw new Error("Session not found");
|
|
3976
|
-
}
|
|
3977
|
-
this.emitConnectionEvent({
|
|
3978
|
-
type: "state_changed",
|
|
3979
|
-
sessionId,
|
|
3980
|
-
serverId: session.serverId ?? "unknown",
|
|
3981
|
-
serverName: session.serverName ?? "Unknown",
|
|
3982
|
-
serverUrl: session.serverUrl,
|
|
3983
|
-
state: "VALIDATING",
|
|
3984
|
-
previousState: "DISCONNECTED",
|
|
3985
|
-
timestamp: Date.now()
|
|
3986
|
-
});
|
|
3987
|
-
try {
|
|
3988
|
-
const clientMetadata = await this.getResolvedClientMetadata();
|
|
3989
|
-
const client = new MCPClient({
|
|
3990
|
-
userId: this.userId,
|
|
3991
|
-
sessionId,
|
|
3992
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3993
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3994
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3995
|
-
// reloading during initialize().
|
|
3996
|
-
serverId: session.serverId,
|
|
3997
|
-
serverName: session.serverName,
|
|
3998
|
-
serverUrl: session.serverUrl,
|
|
3999
|
-
callbackUrl: session.callbackUrl,
|
|
4000
|
-
transportType: session.transportType,
|
|
4001
|
-
headers: session.headers,
|
|
4002
|
-
...clientMetadata
|
|
4003
|
-
});
|
|
4004
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4005
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
4006
|
-
await client.connect();
|
|
4007
|
-
this.clients.set(sessionId, client);
|
|
4008
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4009
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4010
|
-
} catch (error) {
|
|
4011
|
-
this.emitConnectionEvent({
|
|
4012
|
-
type: "error",
|
|
4013
|
-
sessionId,
|
|
4014
|
-
serverId: session.serverId ?? "unknown",
|
|
4015
|
-
error: error instanceof Error ? error.message : "Validation failed",
|
|
4016
|
-
errorType: "validation",
|
|
4017
|
-
timestamp: Date.now()
|
|
4018
|
-
});
|
|
4019
|
-
throw error;
|
|
4020
|
-
}
|
|
4125
|
+
async updateSession(params) {
|
|
4126
|
+
await this.requireSession(params.sessionId);
|
|
4127
|
+
await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
|
|
4128
|
+
return { success: true };
|
|
4021
4129
|
}
|
|
4130
|
+
// -----------------------------------------------------------------------
|
|
4131
|
+
// Tool Execution
|
|
4132
|
+
// -----------------------------------------------------------------------
|
|
4022
4133
|
/**
|
|
4023
|
-
*
|
|
4134
|
+
* Proxies a tool invocation to the remote MCP server.
|
|
4135
|
+
*
|
|
4136
|
+
* Resolves (or creates) the transport for the given session, runs the call
|
|
4137
|
+
* through the tool-policy gateway, and injects `sessionId` into the result
|
|
4138
|
+
* metadata so the client can route the response without scanning all sessions.
|
|
4024
4139
|
*/
|
|
4025
|
-
async
|
|
4026
|
-
const
|
|
4027
|
-
const
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
}
|
|
4034
|
-
try {
|
|
4035
|
-
const client = new MCPClient({
|
|
4036
|
-
userId: this.userId,
|
|
4037
|
-
sessionId,
|
|
4038
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4039
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4040
|
-
// original connection metadata instead of relying on lazy
|
|
4041
|
-
// reloading during initialize().
|
|
4042
|
-
serverId: session.serverId,
|
|
4043
|
-
serverName: session.serverName,
|
|
4044
|
-
serverUrl: session.serverUrl,
|
|
4045
|
-
callbackUrl: session.callbackUrl,
|
|
4046
|
-
// NOTE: transportType is intentionally omitted here.
|
|
4047
|
-
// The session's stored transportType is a placeholder ('streamable-http')
|
|
4048
|
-
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
4049
|
-
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
4050
|
-
// Neon that only support SSE transport.
|
|
4051
|
-
headers: session.headers
|
|
4052
|
-
});
|
|
4053
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4054
|
-
await client.finishAuth(code, oauthState);
|
|
4055
|
-
this.clients.set(sessionId, client);
|
|
4056
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4057
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4058
|
-
} catch (error) {
|
|
4059
|
-
this.emitConnectionEvent({
|
|
4060
|
-
type: "error",
|
|
4061
|
-
sessionId,
|
|
4062
|
-
serverId: session.serverId ?? "unknown",
|
|
4063
|
-
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
4064
|
-
errorType: "auth",
|
|
4065
|
-
timestamp: Date.now()
|
|
4066
|
-
});
|
|
4067
|
-
throw error;
|
|
4068
|
-
}
|
|
4140
|
+
async callTool(params) {
|
|
4141
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4142
|
+
const result = await createToolPolicyGateway(
|
|
4143
|
+
this.userId,
|
|
4144
|
+
params.sessionId,
|
|
4145
|
+
client
|
|
4146
|
+
).callTool(params.toolName, params.toolArgs);
|
|
4147
|
+
return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
|
|
4069
4148
|
}
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4149
|
+
// -----------------------------------------------------------------------
|
|
4150
|
+
// Prompts
|
|
4151
|
+
// -----------------------------------------------------------------------
|
|
4152
|
+
/** Lists all prompts exposed by the remote MCP server. */
|
|
4073
4153
|
async listPrompts(params) {
|
|
4074
|
-
const
|
|
4075
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4154
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4076
4155
|
const result = await client.listPrompts();
|
|
4077
4156
|
return { prompts: result.prompts };
|
|
4078
4157
|
}
|
|
4079
|
-
/**
|
|
4080
|
-
* Get a specific prompt
|
|
4081
|
-
*/
|
|
4158
|
+
/** Retrieves a specific prompt by name with optional arguments. */
|
|
4082
4159
|
async getPrompt(params) {
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
return await client.getPrompt(name, args);
|
|
4160
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4161
|
+
return client.getPrompt(params.name, params.args);
|
|
4086
4162
|
}
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4163
|
+
// -----------------------------------------------------------------------
|
|
4164
|
+
// Resources
|
|
4165
|
+
// -----------------------------------------------------------------------
|
|
4166
|
+
/** Lists all resources exposed by the remote MCP server. */
|
|
4090
4167
|
async listResources(params) {
|
|
4091
|
-
const
|
|
4092
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4168
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4093
4169
|
const result = await client.listResources();
|
|
4094
4170
|
return { resources: result.resources };
|
|
4095
4171
|
}
|
|
4172
|
+
/** Reads a specific resource identified by URI. */
|
|
4173
|
+
async readResource(params) {
|
|
4174
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4175
|
+
return client.readResource(params.uri);
|
|
4176
|
+
}
|
|
4177
|
+
// -----------------------------------------------------------------------
|
|
4178
|
+
// Internal Helpers
|
|
4179
|
+
// -----------------------------------------------------------------------
|
|
4180
|
+
/** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
|
|
4181
|
+
async getResolvedClientMetadata(request) {
|
|
4182
|
+
let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
|
|
4183
|
+
if (this.options.getClientMetadata) {
|
|
4184
|
+
metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
|
|
4185
|
+
}
|
|
4186
|
+
return metadata;
|
|
4187
|
+
}
|
|
4188
|
+
/** Ensures the given session exists in storage and throws otherwise. */
|
|
4189
|
+
async requireSession(sessionId) {
|
|
4190
|
+
const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
|
|
4191
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
4192
|
+
return session;
|
|
4193
|
+
}
|
|
4194
|
+
/** Finds an existing session matching the given serverId or serverUrl. */
|
|
4195
|
+
async findExistingSession(serverId, serverUrl) {
|
|
4196
|
+
const all = await sessions.list(this.userId);
|
|
4197
|
+
return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
|
|
4198
|
+
}
|
|
4199
|
+
/** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
|
|
4200
|
+
normalizeServerId(raw) {
|
|
4201
|
+
return raw && raw.length <= 12 ? raw : generateServerId();
|
|
4202
|
+
}
|
|
4096
4203
|
/**
|
|
4097
|
-
*
|
|
4204
|
+
* Returns the cached in-memory transport for `sessionId`, or creates one
|
|
4205
|
+
* from the persisted session row (with credentials) and connects it.
|
|
4098
4206
|
*/
|
|
4099
|
-
async
|
|
4100
|
-
const
|
|
4101
|
-
|
|
4102
|
-
|
|
4207
|
+
async getOrCreateClient(sessionId) {
|
|
4208
|
+
const existing = this.clients.get(sessionId);
|
|
4209
|
+
if (existing) return existing;
|
|
4210
|
+
const session = await this.requireSession(sessionId);
|
|
4211
|
+
if (session.enabled === false) {
|
|
4212
|
+
throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
|
|
4213
|
+
}
|
|
4214
|
+
const client = this.restoreClient(session);
|
|
4215
|
+
this.attachClientEvents(client);
|
|
4216
|
+
await client.connect();
|
|
4217
|
+
this.clients.set(sessionId, client);
|
|
4218
|
+
return client;
|
|
4219
|
+
}
|
|
4220
|
+
/**
|
|
4221
|
+
* Builds an `MCPClient` from a stored session row.
|
|
4222
|
+
*
|
|
4223
|
+
* Extracts `clientId` and `clientSecret` from the session's credential
|
|
4224
|
+
* fields and passes `hasSession: true` + `cachedCredentials` so the client
|
|
4225
|
+
* can skip redundant existence checks and credential reads.
|
|
4226
|
+
*/
|
|
4227
|
+
restoreClient(session) {
|
|
4228
|
+
const clientId = session.clientId ?? void 0;
|
|
4229
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
4230
|
+
return new MCPClient({
|
|
4231
|
+
userId: this.userId,
|
|
4232
|
+
sessionId: session.sessionId,
|
|
4233
|
+
serverId: session.serverId,
|
|
4234
|
+
serverName: session.serverName,
|
|
4235
|
+
serverUrl: session.serverUrl,
|
|
4236
|
+
callbackUrl: session.callbackUrl,
|
|
4237
|
+
transportType: session.transportType,
|
|
4238
|
+
headers: session.headers,
|
|
4239
|
+
clientId,
|
|
4240
|
+
clientSecret,
|
|
4241
|
+
hasSession: true,
|
|
4242
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
4243
|
+
sessionStore: this.observedStore
|
|
4244
|
+
});
|
|
4103
4245
|
}
|
|
4104
4246
|
/**
|
|
4105
|
-
*
|
|
4247
|
+
* Wires connection and observability events from the client into the
|
|
4248
|
+
* unified observability channel. Connection events go to `sendEvent`
|
|
4249
|
+
* (SSE stream); observability events go to `emitObs` (user callback
|
|
4250
|
+
* + SSE stream).
|
|
4106
4251
|
*/
|
|
4107
|
-
|
|
4108
|
-
this.sendEvent(
|
|
4252
|
+
attachClientEvents(client) {
|
|
4253
|
+
client.onConnectionEvent((e) => this.sendEvent(e));
|
|
4254
|
+
client.onObservabilityEvent((e) => this.sendEvent(e));
|
|
4109
4255
|
}
|
|
4110
4256
|
/**
|
|
4111
|
-
*
|
|
4257
|
+
* Registers the client in the in-memory cache and attaches its event
|
|
4258
|
+
* listeners to the unified observability channel.
|
|
4112
4259
|
*/
|
|
4113
|
-
|
|
4114
|
-
this.
|
|
4115
|
-
|
|
4116
|
-
|
|
4260
|
+
cacheClient(sessionId, client) {
|
|
4261
|
+
this.attachClientEvents(client);
|
|
4262
|
+
this.clients.set(sessionId, client);
|
|
4263
|
+
}
|
|
4264
|
+
/**
|
|
4265
|
+
* Attempts a `client.connect()` and, on success, discovers tools.
|
|
4266
|
+
*
|
|
4267
|
+
* If the server responds with `UnauthorizedError`, the session is
|
|
4268
|
+
* treated as pending OAuth — a successful result is returned with
|
|
4269
|
+
* the sessionId so the browser client can redirect to the auth URL.
|
|
4270
|
+
*
|
|
4271
|
+
* For any other error an `error` connection event is emitted and the
|
|
4272
|
+
* client is removed from the in-memory cache before re-throwing.
|
|
4273
|
+
*/
|
|
4274
|
+
async connectAndDiscover(client, sessionId, serverId) {
|
|
4275
|
+
try {
|
|
4276
|
+
await client.connect();
|
|
4277
|
+
await this.listPolicyFilteredTools(sessionId);
|
|
4278
|
+
return { sessionId, success: true };
|
|
4279
|
+
} catch (error) {
|
|
4280
|
+
if (error instanceof UnauthorizedError) {
|
|
4281
|
+
this.clients.delete(sessionId);
|
|
4282
|
+
return { sessionId, success: true };
|
|
4283
|
+
}
|
|
4284
|
+
this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
|
|
4285
|
+
this.clients.delete(sessionId);
|
|
4286
|
+
throw error;
|
|
4117
4287
|
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
this.
|
|
4288
|
+
}
|
|
4289
|
+
/** Starts the periodic heartbeat timer. */
|
|
4290
|
+
startHeartbeat() {
|
|
4291
|
+
const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
4292
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4293
|
+
if (this.active) {
|
|
4294
|
+
this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
|
|
4295
|
+
}
|
|
4296
|
+
}, ms);
|
|
4122
4297
|
}
|
|
4123
4298
|
};
|
|
4124
4299
|
function createSSEHandler(options) {
|
|
@@ -4147,8 +4322,7 @@ function createSSEHandler(options) {
|
|
|
4147
4322
|
});
|
|
4148
4323
|
req.on("end", async () => {
|
|
4149
4324
|
try {
|
|
4150
|
-
|
|
4151
|
-
await manager.handleRequest(request);
|
|
4325
|
+
await manager.handleRequest(JSON.parse(body));
|
|
4152
4326
|
} catch {
|
|
4153
4327
|
}
|
|
4154
4328
|
});
|
|
@@ -4323,8 +4497,11 @@ exports.StorageOAuthClientProvider = StorageOAuthClientProvider;
|
|
|
4323
4497
|
exports.UnauthorizedError = UnauthorizedError;
|
|
4324
4498
|
exports.createNextMcpHandler = createNextMcpHandler;
|
|
4325
4499
|
exports.createSSEHandler = createSSEHandler;
|
|
4500
|
+
exports.decryptObject = decryptObject;
|
|
4501
|
+
exports.encryptObject = encryptObject;
|
|
4326
4502
|
exports.onSessionMutation = onSessionMutation;
|
|
4327
4503
|
exports.sanitizeServerLabel = sanitizeServerLabel;
|
|
4328
4504
|
exports.sessions = sessions;
|
|
4505
|
+
exports.withDbObservability = withDbObservability;
|
|
4329
4506
|
//# sourceMappingURL=index.js.map
|
|
4330
4507
|
//# sourceMappingURL=index.js.map
|