@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/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
|
var appBridge = require('@modelcontextprotocol/ext-apps/app-bridge');
|
|
13
14
|
|
|
14
15
|
function _interopNamespace(e) {
|
|
@@ -44,9 +45,9 @@ var __export = (target, all) => {
|
|
|
44
45
|
};
|
|
45
46
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
46
47
|
|
|
47
|
-
// node_modules/tsup/assets/cjs_shims.js
|
|
48
|
+
// ../../node_modules/tsup/assets/cjs_shims.js
|
|
48
49
|
var init_cjs_shims = __esm({
|
|
49
|
-
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
50
|
+
"../../node_modules/tsup/assets/cjs_shims.js"() {
|
|
50
51
|
}
|
|
51
52
|
});
|
|
52
53
|
|
|
@@ -332,7 +333,6 @@ var RedisStorageBackend = class {
|
|
|
332
333
|
constructor(redis2) {
|
|
333
334
|
this.redis = redis2;
|
|
334
335
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
335
|
-
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
336
336
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
337
337
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
338
338
|
}
|
|
@@ -351,9 +351,6 @@ var RedisStorageBackend = class {
|
|
|
351
351
|
getSessionKey(userId, sessionId) {
|
|
352
352
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
353
353
|
}
|
|
354
|
-
getCredentialsKey(userId, sessionId) {
|
|
355
|
-
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
356
|
-
}
|
|
357
354
|
/**
|
|
358
355
|
* Generates Redis key for tracking all sessions for a user
|
|
359
356
|
* @private
|
|
@@ -443,48 +440,54 @@ var RedisStorageBackend = class {
|
|
|
443
440
|
}
|
|
444
441
|
async patchCredentials(userId, sessionId, data) {
|
|
445
442
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
if (!sessionExists) {
|
|
443
|
+
const currentStr = await this.redis.get(sessionKey);
|
|
444
|
+
if (!currentStr) {
|
|
449
445
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
450
446
|
}
|
|
451
|
-
const
|
|
452
|
-
const
|
|
453
|
-
const effectiveTtl =
|
|
454
|
-
|
|
455
|
-
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
456
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
457
|
-
if (effectiveTtl > 0) {
|
|
458
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
459
|
-
} else {
|
|
460
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
461
|
-
}
|
|
447
|
+
const current = JSON.parse(currentStr);
|
|
448
|
+
const updated = { ...current, ...data, sessionId, userId };
|
|
449
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
450
|
+
await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
|
|
462
451
|
}
|
|
463
|
-
async get(userId, sessionId) {
|
|
452
|
+
async get(userId, sessionId, options) {
|
|
464
453
|
try {
|
|
465
454
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
466
455
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
467
|
-
if (!sessionDataStr)
|
|
468
|
-
|
|
456
|
+
if (!sessionDataStr) return null;
|
|
457
|
+
const session = normalizeStoredSession(JSON.parse(sessionDataStr));
|
|
458
|
+
if (!options?.includeCredentials) {
|
|
459
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
460
|
+
return sessionOnly;
|
|
469
461
|
}
|
|
470
|
-
|
|
471
|
-
return normalizeStoredSession(session);
|
|
462
|
+
return session;
|
|
472
463
|
} catch (error) {
|
|
473
464
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
474
465
|
return null;
|
|
475
466
|
}
|
|
476
467
|
}
|
|
477
468
|
async getCredentials(userId, sessionId) {
|
|
478
|
-
const session = await this.get(userId, sessionId);
|
|
469
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
479
470
|
if (!session) return null;
|
|
480
|
-
const
|
|
481
|
-
return
|
|
471
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
472
|
+
return {
|
|
473
|
+
sessionId,
|
|
474
|
+
userId,
|
|
475
|
+
clientInformation,
|
|
476
|
+
tokens,
|
|
477
|
+
codeVerifier,
|
|
478
|
+
codeVerifierChallenge,
|
|
479
|
+
codeVerifierNonce,
|
|
480
|
+
clientId,
|
|
481
|
+
oauthState
|
|
482
|
+
};
|
|
482
483
|
}
|
|
483
484
|
async clearCredentials(userId, sessionId) {
|
|
484
485
|
await this.patchCredentials(userId, sessionId, {
|
|
485
486
|
clientInformation: null,
|
|
486
487
|
tokens: null,
|
|
487
488
|
codeVerifier: null,
|
|
489
|
+
codeVerifierChallenge: null,
|
|
490
|
+
codeVerifierNonce: null,
|
|
488
491
|
clientId: null,
|
|
489
492
|
oauthState: null
|
|
490
493
|
});
|
|
@@ -517,10 +520,9 @@ var RedisStorageBackend = class {
|
|
|
517
520
|
async delete(userId, sessionId) {
|
|
518
521
|
try {
|
|
519
522
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
520
|
-
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
521
523
|
const userIdKey = this.getUserIdKey(userId);
|
|
522
524
|
await this.redis.srem(userIdKey, sessionId);
|
|
523
|
-
await this.redis.del(sessionKey
|
|
525
|
+
await this.redis.del(sessionKey);
|
|
524
526
|
} catch (error) {
|
|
525
527
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
526
528
|
}
|
|
@@ -551,9 +553,8 @@ var RedisStorageBackend = class {
|
|
|
551
553
|
async clearAll() {
|
|
552
554
|
try {
|
|
553
555
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
554
|
-
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
555
556
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
556
|
-
const allKeys = [...keys, ...
|
|
557
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
557
558
|
if (allKeys.length > 0) {
|
|
558
559
|
await this.redis.del(...allKeys);
|
|
559
560
|
}
|
|
@@ -577,7 +578,6 @@ var RedisStorageBackend = class {
|
|
|
577
578
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
578
579
|
if (staleSessionIds.length > 0) {
|
|
579
580
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
580
|
-
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
581
581
|
}
|
|
582
582
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
583
583
|
if (remainingCount === 0) {
|
|
@@ -603,7 +603,6 @@ var MemoryStorageBackend = class {
|
|
|
603
603
|
constructor() {
|
|
604
604
|
// Map<userId:sessionId, Session>
|
|
605
605
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
606
|
-
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
607
606
|
// Map<userId, Set<sessionId>>
|
|
608
607
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
609
608
|
}
|
|
@@ -641,26 +640,46 @@ var MemoryStorageBackend = class {
|
|
|
641
640
|
}
|
|
642
641
|
async patchCredentials(userId, sessionId, data) {
|
|
643
642
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
644
|
-
|
|
643
|
+
const current = this.sessions.get(sessionKey);
|
|
644
|
+
if (!current) {
|
|
645
645
|
throw new Error(`Session ${sessionId} not found`);
|
|
646
646
|
}
|
|
647
|
-
|
|
648
|
-
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
647
|
+
this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
649
648
|
}
|
|
650
|
-
async get(userId, sessionId) {
|
|
649
|
+
async get(userId, sessionId, options) {
|
|
651
650
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
652
|
-
|
|
651
|
+
const session = this.sessions.get(sessionKey) || null;
|
|
652
|
+
if (!session) return session;
|
|
653
|
+
if (!options?.includeCredentials) {
|
|
654
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
655
|
+
return sessionOnly;
|
|
656
|
+
}
|
|
657
|
+
return session;
|
|
653
658
|
}
|
|
654
659
|
async getCredentials(userId, sessionId) {
|
|
655
660
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
656
|
-
|
|
657
|
-
|
|
661
|
+
const session = this.sessions.get(sessionKey);
|
|
662
|
+
if (!session) return null;
|
|
663
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
664
|
+
return {
|
|
665
|
+
sessionId,
|
|
666
|
+
userId,
|
|
667
|
+
clientInformation,
|
|
668
|
+
tokens,
|
|
669
|
+
codeVerifier,
|
|
670
|
+
codeVerifierChallenge,
|
|
671
|
+
codeVerifierNonce,
|
|
672
|
+
clientId,
|
|
673
|
+
oauthState
|
|
674
|
+
};
|
|
658
675
|
}
|
|
659
676
|
async clearCredentials(userId, sessionId) {
|
|
660
677
|
await this.patchCredentials(userId, sessionId, {
|
|
661
678
|
clientInformation: null,
|
|
662
679
|
tokens: null,
|
|
663
680
|
codeVerifier: null,
|
|
681
|
+
codeVerifierChallenge: null,
|
|
682
|
+
codeVerifierNonce: null,
|
|
664
683
|
clientId: null,
|
|
665
684
|
oauthState: null
|
|
666
685
|
});
|
|
@@ -684,7 +703,6 @@ var MemoryStorageBackend = class {
|
|
|
684
703
|
async delete(userId, sessionId) {
|
|
685
704
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
686
705
|
this.sessions.delete(sessionKey);
|
|
687
|
-
this.credentials.delete(sessionKey);
|
|
688
706
|
const set = this.userIdSessions.get(userId);
|
|
689
707
|
if (set) {
|
|
690
708
|
set.delete(sessionId);
|
|
@@ -698,14 +716,12 @@ var MemoryStorageBackend = class {
|
|
|
698
716
|
}
|
|
699
717
|
async clearAll() {
|
|
700
718
|
this.sessions.clear();
|
|
701
|
-
this.credentials.clear();
|
|
702
719
|
this.userIdSessions.clear();
|
|
703
720
|
}
|
|
704
721
|
async cleanupExpired() {
|
|
705
722
|
for (const [key, session] of this.sessions.entries()) {
|
|
706
723
|
if (!isSessionExpired(session)) continue;
|
|
707
724
|
this.sessions.delete(key);
|
|
708
|
-
this.credentials.delete(key);
|
|
709
725
|
const set = this.userIdSessions.get(session.userId);
|
|
710
726
|
if (set) {
|
|
711
727
|
set.delete(session.sessionId);
|
|
@@ -728,7 +744,6 @@ var FileStorageBackend = class {
|
|
|
728
744
|
constructor(options = {}) {
|
|
729
745
|
__publicField(this, "filePath");
|
|
730
746
|
__publicField(this, "memoryCache", null);
|
|
731
|
-
__publicField(this, "credentialsCache", null);
|
|
732
747
|
__publicField(this, "initialized", false);
|
|
733
748
|
this.filePath = options.path || "./sessions.json";
|
|
734
749
|
}
|
|
@@ -743,22 +758,15 @@ var FileStorageBackend = class {
|
|
|
743
758
|
const data = await fs2.promises.readFile(this.filePath, "utf-8");
|
|
744
759
|
const json = JSON.parse(data);
|
|
745
760
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
746
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
747
761
|
if (Array.isArray(json.sessions)) {
|
|
748
762
|
json.sessions.forEach((s) => {
|
|
749
763
|
const session = normalizeStoredSession(s);
|
|
750
764
|
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
751
765
|
});
|
|
752
766
|
}
|
|
753
|
-
if (Array.isArray(json.credentials)) {
|
|
754
|
-
json.credentials.forEach((c) => {
|
|
755
|
-
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
756
|
-
});
|
|
757
|
-
}
|
|
758
767
|
} catch (error) {
|
|
759
768
|
if (error.code === "ENOENT") {
|
|
760
769
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
761
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
762
770
|
await this.flush();
|
|
763
771
|
} else {
|
|
764
772
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -772,10 +780,9 @@ var FileStorageBackend = class {
|
|
|
772
780
|
if (!this.initialized) await this.init();
|
|
773
781
|
}
|
|
774
782
|
async flush() {
|
|
775
|
-
if (!this.memoryCache
|
|
783
|
+
if (!this.memoryCache) return;
|
|
776
784
|
await fs2.promises.writeFile(this.filePath, JSON.stringify({
|
|
777
|
-
sessions: Array.from(this.memoryCache.values())
|
|
778
|
-
credentials: Array.from(this.credentialsCache.values())
|
|
785
|
+
sessions: Array.from(this.memoryCache.values())
|
|
779
786
|
}, null, 2), "utf-8");
|
|
780
787
|
}
|
|
781
788
|
getSessionKey(userId, sessionId) {
|
|
@@ -810,29 +817,49 @@ var FileStorageBackend = class {
|
|
|
810
817
|
async patchCredentials(userId, sessionId, data) {
|
|
811
818
|
await this.ensureInitialized();
|
|
812
819
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
813
|
-
|
|
820
|
+
const current = this.memoryCache.get(sessionKey);
|
|
821
|
+
if (!current) {
|
|
814
822
|
throw new Error(`Session ${sessionId} not found`);
|
|
815
823
|
}
|
|
816
|
-
|
|
817
|
-
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
824
|
+
this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
818
825
|
await this.flush();
|
|
819
826
|
}
|
|
820
|
-
async get(userId, sessionId) {
|
|
827
|
+
async get(userId, sessionId, options) {
|
|
821
828
|
await this.ensureInitialized();
|
|
822
829
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
823
|
-
|
|
830
|
+
const session = this.memoryCache.get(sessionKey) || null;
|
|
831
|
+
if (!session) return null;
|
|
832
|
+
if (!options?.includeCredentials) {
|
|
833
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
834
|
+
return sessionOnly;
|
|
835
|
+
}
|
|
836
|
+
return session;
|
|
824
837
|
}
|
|
825
838
|
async getCredentials(userId, sessionId) {
|
|
826
839
|
await this.ensureInitialized();
|
|
827
840
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
828
|
-
|
|
829
|
-
|
|
841
|
+
const session = this.memoryCache.get(sessionKey);
|
|
842
|
+
if (!session) return null;
|
|
843
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
844
|
+
return {
|
|
845
|
+
sessionId,
|
|
846
|
+
userId,
|
|
847
|
+
clientInformation,
|
|
848
|
+
tokens,
|
|
849
|
+
codeVerifier,
|
|
850
|
+
codeVerifierChallenge,
|
|
851
|
+
codeVerifierNonce,
|
|
852
|
+
clientId,
|
|
853
|
+
oauthState
|
|
854
|
+
};
|
|
830
855
|
}
|
|
831
856
|
async clearCredentials(userId, sessionId) {
|
|
832
857
|
await this.patchCredentials(userId, sessionId, {
|
|
833
858
|
clientInformation: null,
|
|
834
859
|
tokens: null,
|
|
835
860
|
codeVerifier: null,
|
|
861
|
+
codeVerifierChallenge: null,
|
|
862
|
+
codeVerifierNonce: null,
|
|
836
863
|
clientId: null,
|
|
837
864
|
oauthState: null
|
|
838
865
|
});
|
|
@@ -849,7 +876,6 @@ var FileStorageBackend = class {
|
|
|
849
876
|
await this.ensureInitialized();
|
|
850
877
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
851
878
|
const deleted = this.memoryCache.delete(sessionKey);
|
|
852
|
-
this.credentialsCache.delete(sessionKey);
|
|
853
879
|
if (deleted) {
|
|
854
880
|
await this.flush();
|
|
855
881
|
}
|
|
@@ -861,7 +887,6 @@ var FileStorageBackend = class {
|
|
|
861
887
|
async clearAll() {
|
|
862
888
|
await this.ensureInitialized();
|
|
863
889
|
this.memoryCache.clear();
|
|
864
|
-
this.credentialsCache.clear();
|
|
865
890
|
await this.flush();
|
|
866
891
|
}
|
|
867
892
|
async cleanupExpired() {
|
|
@@ -870,7 +895,6 @@ var FileStorageBackend = class {
|
|
|
870
895
|
for (const [key, session] of this.memoryCache.entries()) {
|
|
871
896
|
if (!isSessionExpired(session)) continue;
|
|
872
897
|
this.memoryCache.delete(key);
|
|
873
|
-
this.credentialsCache.delete(key);
|
|
874
898
|
changed = true;
|
|
875
899
|
}
|
|
876
900
|
if (changed) {
|
|
@@ -887,12 +911,10 @@ var SqliteStorage = class {
|
|
|
887
911
|
constructor(options = {}) {
|
|
888
912
|
__publicField(this, "db", null);
|
|
889
913
|
__publicField(this, "table");
|
|
890
|
-
__publicField(this, "credentialsTable");
|
|
891
914
|
__publicField(this, "initialized", false);
|
|
892
915
|
__publicField(this, "dbPath");
|
|
893
916
|
this.dbPath = options.path || "./sessions.db";
|
|
894
917
|
this.table = options.table || "mcp_sessions";
|
|
895
|
-
this.credentialsTable = `${this.table}_credentials`;
|
|
896
918
|
}
|
|
897
919
|
async init() {
|
|
898
920
|
if (this.initialized) return;
|
|
@@ -912,13 +934,6 @@ var SqliteStorage = class {
|
|
|
912
934
|
expiresAt INTEGER
|
|
913
935
|
);
|
|
914
936
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
915
|
-
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
916
|
-
sessionId TEXT NOT NULL,
|
|
917
|
-
userId TEXT NOT NULL,
|
|
918
|
-
data TEXT NOT NULL,
|
|
919
|
-
PRIMARY KEY (userId, sessionId),
|
|
920
|
-
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
921
|
-
);
|
|
922
937
|
`);
|
|
923
938
|
this.initialized = true;
|
|
924
939
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -975,41 +990,54 @@ var SqliteStorage = class {
|
|
|
975
990
|
}
|
|
976
991
|
async patchCredentials(userId, sessionId, data) {
|
|
977
992
|
this.ensureInitialized();
|
|
978
|
-
|
|
993
|
+
const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
|
|
994
|
+
if (!currentSession) {
|
|
979
995
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
980
996
|
}
|
|
981
|
-
const
|
|
982
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
997
|
+
const updated = { ...currentSession, ...data, sessionId, userId };
|
|
983
998
|
const stmt = this.db.prepare(
|
|
984
|
-
`
|
|
985
|
-
VALUES (?, ?, ?)
|
|
986
|
-
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
999
|
+
`UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
|
|
987
1000
|
);
|
|
988
|
-
stmt.run(
|
|
1001
|
+
stmt.run(JSON.stringify(updated), sessionId, userId);
|
|
989
1002
|
}
|
|
990
|
-
async get(userId, sessionId) {
|
|
1003
|
+
async get(userId, sessionId, options) {
|
|
991
1004
|
this.ensureInitialized();
|
|
992
1005
|
const stmt = this.db.prepare(
|
|
993
1006
|
`SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
994
1007
|
);
|
|
995
1008
|
const row = stmt.get(sessionId, userId);
|
|
996
1009
|
if (!row) return null;
|
|
997
|
-
|
|
1010
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
1011
|
+
if (!options?.includeCredentials) {
|
|
1012
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
1013
|
+
return sessionOnly;
|
|
1014
|
+
}
|
|
1015
|
+
return session;
|
|
998
1016
|
}
|
|
999
1017
|
async getCredentials(userId, sessionId) {
|
|
1000
1018
|
this.ensureInitialized();
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1019
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
1020
|
+
if (!session) return null;
|
|
1021
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
1022
|
+
return {
|
|
1023
|
+
sessionId,
|
|
1024
|
+
userId,
|
|
1025
|
+
clientInformation,
|
|
1026
|
+
tokens,
|
|
1027
|
+
codeVerifier,
|
|
1028
|
+
codeVerifierChallenge,
|
|
1029
|
+
codeVerifierNonce,
|
|
1030
|
+
clientId,
|
|
1031
|
+
oauthState
|
|
1032
|
+
};
|
|
1007
1033
|
}
|
|
1008
1034
|
async clearCredentials(userId, sessionId) {
|
|
1009
1035
|
await this.patchCredentials(userId, sessionId, {
|
|
1010
1036
|
clientInformation: null,
|
|
1011
1037
|
tokens: null,
|
|
1012
1038
|
codeVerifier: null,
|
|
1039
|
+
codeVerifierChallenge: null,
|
|
1040
|
+
codeVerifierNonce: null,
|
|
1013
1041
|
clientId: null,
|
|
1014
1042
|
oauthState: null
|
|
1015
1043
|
});
|
|
@@ -1035,9 +1063,6 @@ var SqliteStorage = class {
|
|
|
1035
1063
|
const stmt = this.db.prepare(
|
|
1036
1064
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
1037
1065
|
);
|
|
1038
|
-
this.db.prepare(
|
|
1039
|
-
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
1040
|
-
).run(sessionId, userId);
|
|
1041
1066
|
stmt.run(sessionId, userId);
|
|
1042
1067
|
}
|
|
1043
1068
|
async listAllIds() {
|
|
@@ -1048,7 +1073,6 @@ var SqliteStorage = class {
|
|
|
1048
1073
|
}
|
|
1049
1074
|
async clearAll() {
|
|
1050
1075
|
this.ensureInitialized();
|
|
1051
|
-
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
1052
1076
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
1053
1077
|
stmt.run();
|
|
1054
1078
|
}
|
|
@@ -1062,14 +1086,6 @@ var SqliteStorage = class {
|
|
|
1062
1086
|
deleteStmt.run(row.sessionId, row.userId);
|
|
1063
1087
|
}
|
|
1064
1088
|
}
|
|
1065
|
-
this.db.prepare(
|
|
1066
|
-
`DELETE FROM ${this.credentialsTable}
|
|
1067
|
-
WHERE NOT EXISTS (
|
|
1068
|
-
SELECT 1 FROM ${this.table}
|
|
1069
|
-
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1070
|
-
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1071
|
-
)`
|
|
1072
|
-
).run();
|
|
1073
1089
|
}
|
|
1074
1090
|
async disconnect() {
|
|
1075
1091
|
if (this.db) {
|
|
@@ -1110,8 +1126,8 @@ function encryptObject(data) {
|
|
|
1110
1126
|
}
|
|
1111
1127
|
try {
|
|
1112
1128
|
const text = JSON.stringify(data);
|
|
1113
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
1114
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
1129
|
+
const iv = crypto$1.randomBytes(IV_LENGTH);
|
|
1130
|
+
const cipher = crypto$1.createCipheriv(ALGORITHM, key, iv);
|
|
1115
1131
|
let encrypted = cipher.update(text, "utf-8", "hex");
|
|
1116
1132
|
encrypted += cipher.final("hex");
|
|
1117
1133
|
const authTag = cipher.getAuthTag().toString("hex");
|
|
@@ -1139,7 +1155,7 @@ function decryptObject(data) {
|
|
|
1139
1155
|
const iv = Buffer.from(parts[2], "hex");
|
|
1140
1156
|
const authTag = Buffer.from(parts[3], "hex");
|
|
1141
1157
|
const encryptedText = parts[4];
|
|
1142
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
1158
|
+
const decipher = crypto$1.createDecipheriv(ALGORITHM, key, iv);
|
|
1143
1159
|
decipher.setAuthTag(authTag);
|
|
1144
1160
|
let decrypted = decipher.update(encryptedText, "hex", "utf-8");
|
|
1145
1161
|
decrypted += decipher.final("utf-8");
|
|
@@ -1157,7 +1173,6 @@ var SupabaseStorageBackend = class {
|
|
|
1157
1173
|
}
|
|
1158
1174
|
async init() {
|
|
1159
1175
|
await this.assertTable("mcp_sessions", "session_id");
|
|
1160
|
-
await this.assertTable("mcp_credentials", "session_id");
|
|
1161
1176
|
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1162
1177
|
}
|
|
1163
1178
|
async assertTable(table, column) {
|
|
@@ -1188,18 +1203,13 @@ var SupabaseStorageBackend = class {
|
|
|
1188
1203
|
headers: decryptObject(row.headers),
|
|
1189
1204
|
authUrl: row.auth_url,
|
|
1190
1205
|
status: row.status ?? "pending",
|
|
1191
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
clientInformation: decryptObject(row?.client_information),
|
|
1199
|
-
tokens: decryptObject(row?.tokens),
|
|
1200
|
-
codeVerifier: decryptObject(row?.code_verifier),
|
|
1201
|
-
clientId: row?.client_id,
|
|
1202
|
-
oauthState: row?.oauth_state
|
|
1206
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1207
|
+
clientInformation: decryptObject(row.client_information),
|
|
1208
|
+
tokens: decryptObject(row.tokens),
|
|
1209
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1210
|
+
clientId: row.client_id,
|
|
1211
|
+
oauthState: row.oauth_state,
|
|
1212
|
+
enabled: row.enabled ?? true
|
|
1203
1213
|
};
|
|
1204
1214
|
}
|
|
1205
1215
|
hasCredentialData(data) {
|
|
@@ -1257,6 +1267,7 @@ var SupabaseStorageBackend = class {
|
|
|
1257
1267
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
1258
1268
|
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1259
1269
|
if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
|
|
1270
|
+
if ("enabled" in data) updateData.enabled = data.enabled;
|
|
1260
1271
|
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1261
1272
|
let updatedRows = null;
|
|
1262
1273
|
if (shouldUpdateSession) {
|
|
@@ -1278,23 +1289,22 @@ var SupabaseStorageBackend = class {
|
|
|
1278
1289
|
}
|
|
1279
1290
|
async patchCredentials(userId, sessionId, data) {
|
|
1280
1291
|
if (!this.hasCredentialData(data)) return;
|
|
1281
|
-
const
|
|
1282
|
-
user_id: userId,
|
|
1283
|
-
session_id: sessionId,
|
|
1292
|
+
const updateData = {
|
|
1284
1293
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1285
1294
|
};
|
|
1286
|
-
if ("clientInformation" in data)
|
|
1287
|
-
if ("tokens" in data)
|
|
1288
|
-
if ("codeVerifier" in data)
|
|
1289
|
-
if ("clientId" in data)
|
|
1290
|
-
if ("oauthState" in data)
|
|
1291
|
-
const { error } = await this.supabase.from("
|
|
1295
|
+
if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1296
|
+
if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1297
|
+
if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1298
|
+
if ("clientId" in data) updateData.client_id = data.clientId ?? null;
|
|
1299
|
+
if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
|
|
1300
|
+
const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
|
|
1292
1301
|
if (error) {
|
|
1293
1302
|
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1294
1303
|
}
|
|
1295
1304
|
}
|
|
1296
|
-
async get(userId, sessionId) {
|
|
1297
|
-
const
|
|
1305
|
+
async get(userId, sessionId, options) {
|
|
1306
|
+
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";
|
|
1307
|
+
const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1298
1308
|
if (error) {
|
|
1299
1309
|
console.error("[SupabaseStorage] Failed to get session:", error);
|
|
1300
1310
|
return null;
|
|
@@ -1303,19 +1313,21 @@ var SupabaseStorageBackend = class {
|
|
|
1303
1313
|
return this.mapRowToSessionData(data);
|
|
1304
1314
|
}
|
|
1305
1315
|
async getCredentials(userId, sessionId) {
|
|
1306
|
-
const { data, error } = await this.supabase.from("
|
|
1316
|
+
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();
|
|
1307
1317
|
if (error) {
|
|
1308
1318
|
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1309
1319
|
return null;
|
|
1310
1320
|
}
|
|
1311
|
-
if (data)
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1321
|
+
if (!data) return null;
|
|
1322
|
+
return {
|
|
1323
|
+
sessionId,
|
|
1324
|
+
userId,
|
|
1325
|
+
clientInformation: decryptObject(data.client_information),
|
|
1326
|
+
tokens: decryptObject(data.tokens),
|
|
1327
|
+
codeVerifier: decryptObject(data.code_verifier),
|
|
1328
|
+
clientId: data.client_id,
|
|
1329
|
+
oauthState: data.oauth_state
|
|
1330
|
+
};
|
|
1319
1331
|
}
|
|
1320
1332
|
async list(userId) {
|
|
1321
1333
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
@@ -1326,7 +1338,14 @@ var SupabaseStorageBackend = class {
|
|
|
1326
1338
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
1327
1339
|
}
|
|
1328
1340
|
async clearCredentials(userId, sessionId) {
|
|
1329
|
-
const { error } = await this.supabase.from("
|
|
1341
|
+
const { error } = await this.supabase.from("mcp_sessions").update({
|
|
1342
|
+
client_information: null,
|
|
1343
|
+
tokens: null,
|
|
1344
|
+
code_verifier: null,
|
|
1345
|
+
client_id: null,
|
|
1346
|
+
oauth_state: null,
|
|
1347
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1348
|
+
}).eq("user_id", userId).eq("session_id", sessionId);
|
|
1330
1349
|
if (error) {
|
|
1331
1350
|
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1332
1351
|
}
|
|
@@ -1354,10 +1373,6 @@ var SupabaseStorageBackend = class {
|
|
|
1354
1373
|
return data.map((row) => row.session_id);
|
|
1355
1374
|
}
|
|
1356
1375
|
async clearAll() {
|
|
1357
|
-
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1358
|
-
if (credentialsError) {
|
|
1359
|
-
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1360
|
-
}
|
|
1361
1376
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
1362
1377
|
if (error) {
|
|
1363
1378
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
@@ -1384,16 +1399,12 @@ var NeonStorageBackend = class {
|
|
|
1384
1399
|
constructor(sql, options = {}) {
|
|
1385
1400
|
this.sql = sql;
|
|
1386
1401
|
__publicField(this, "tableName");
|
|
1387
|
-
__publicField(this, "credentialsTableName");
|
|
1388
1402
|
const schema = options.schema || "public";
|
|
1389
1403
|
const table = options.table || "mcp_sessions";
|
|
1390
|
-
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
1391
1404
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1392
|
-
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
1393
1405
|
}
|
|
1394
1406
|
async init() {
|
|
1395
1407
|
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1396
|
-
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1397
1408
|
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1398
1409
|
}
|
|
1399
1410
|
async assertTable(qualifiedName, displayName) {
|
|
@@ -1431,18 +1442,13 @@ var NeonStorageBackend = class {
|
|
|
1431
1442
|
headers: decryptObject(row.headers),
|
|
1432
1443
|
authUrl: row.auth_url ?? void 0,
|
|
1433
1444
|
status: row.status ?? "pending",
|
|
1434
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
mapRowToCredentials(row, userId, sessionId) {
|
|
1438
|
-
return {
|
|
1439
|
-
sessionId,
|
|
1440
|
-
userId,
|
|
1445
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1441
1446
|
clientInformation: decryptObject(row.client_information),
|
|
1442
1447
|
tokens: decryptObject(row.tokens),
|
|
1443
1448
|
codeVerifier: decryptObject(row.code_verifier),
|
|
1444
1449
|
clientId: row.client_id ?? void 0,
|
|
1445
|
-
oauthState: row.oauth_state
|
|
1450
|
+
oauthState: row.oauth_state,
|
|
1451
|
+
enabled: row.enabled ?? true
|
|
1446
1452
|
};
|
|
1447
1453
|
}
|
|
1448
1454
|
hasCredentialData(data) {
|
|
@@ -1512,7 +1518,7 @@ var NeonStorageBackend = class {
|
|
|
1512
1518
|
const updatedSession = { ...currentSession, ...data };
|
|
1513
1519
|
const status = updatedSession.status ?? "pending";
|
|
1514
1520
|
const expiresAt = resolveSessionExpiresAt(status);
|
|
1515
|
-
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;
|
|
1521
|
+
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;
|
|
1516
1522
|
if (shouldUpdateSession) {
|
|
1517
1523
|
const setClauses = [];
|
|
1518
1524
|
const values = [];
|
|
@@ -1535,6 +1541,9 @@ var NeonStorageBackend = class {
|
|
|
1535
1541
|
const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
|
|
1536
1542
|
addSet("tool_policy", toolPolicy);
|
|
1537
1543
|
}
|
|
1544
|
+
if ("enabled" in data) {
|
|
1545
|
+
addSet("enabled", updatedSession.enabled);
|
|
1546
|
+
}
|
|
1538
1547
|
setClauses.push("updated_at = now()");
|
|
1539
1548
|
const updatedRows = await this.sql.query(
|
|
1540
1549
|
`UPDATE ${this.tableName}
|
|
@@ -1550,45 +1559,45 @@ var NeonStorageBackend = class {
|
|
|
1550
1559
|
}
|
|
1551
1560
|
async patchCredentials(userId, sessionId, data) {
|
|
1552
1561
|
if (!this.hasCredentialData(data)) return;
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
"codeVerifier" in data,
|
|
1583
|
-
"clientId" in data,
|
|
1584
|
-
"oauthState" in data
|
|
1585
|
-
]
|
|
1562
|
+
const setClauses = [];
|
|
1563
|
+
const values = [];
|
|
1564
|
+
let paramIndex = 1;
|
|
1565
|
+
const addSet = (column, value) => {
|
|
1566
|
+
setClauses.push(`${column} = $${paramIndex++}`);
|
|
1567
|
+
values.push(value);
|
|
1568
|
+
};
|
|
1569
|
+
if ("clientInformation" in data) {
|
|
1570
|
+
addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
|
|
1571
|
+
}
|
|
1572
|
+
if ("tokens" in data) {
|
|
1573
|
+
addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
|
|
1574
|
+
}
|
|
1575
|
+
if ("codeVerifier" in data) {
|
|
1576
|
+
addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
|
|
1577
|
+
}
|
|
1578
|
+
if ("clientId" in data) {
|
|
1579
|
+
addSet("client_id", data.clientId ?? null);
|
|
1580
|
+
}
|
|
1581
|
+
if ("oauthState" in data) {
|
|
1582
|
+
addSet("oauth_state", data.oauthState ?? null);
|
|
1583
|
+
}
|
|
1584
|
+
setClauses.push("updated_at = now()");
|
|
1585
|
+
const updatedRows = await this.sql.query(
|
|
1586
|
+
`UPDATE ${this.tableName}
|
|
1587
|
+
SET ${setClauses.join(", ")}
|
|
1588
|
+
WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
|
|
1589
|
+
RETURNING id`,
|
|
1590
|
+
[...values, userId, sessionId]
|
|
1586
1591
|
);
|
|
1592
|
+
if (updatedRows.length === 0) {
|
|
1593
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1594
|
+
}
|
|
1587
1595
|
}
|
|
1588
|
-
async get(userId, sessionId) {
|
|
1596
|
+
async get(userId, sessionId, options) {
|
|
1589
1597
|
try {
|
|
1598
|
+
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";
|
|
1590
1599
|
const rows = await this.sql.query(
|
|
1591
|
-
`SELECT
|
|
1600
|
+
`SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1592
1601
|
[userId, sessionId]
|
|
1593
1602
|
);
|
|
1594
1603
|
if (!rows[0]) return null;
|
|
@@ -1600,18 +1609,22 @@ var NeonStorageBackend = class {
|
|
|
1600
1609
|
}
|
|
1601
1610
|
async getCredentials(userId, sessionId) {
|
|
1602
1611
|
try {
|
|
1603
|
-
const
|
|
1604
|
-
`SELECT
|
|
1605
|
-
|
|
1606
|
-
);
|
|
1607
|
-
if (credentialRows[0]) {
|
|
1608
|
-
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1609
|
-
}
|
|
1610
|
-
const sessionRows = await this.sql.query(
|
|
1611
|
-
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1612
|
+
const rows = await this.sql.query(
|
|
1613
|
+
`SELECT client_information, tokens, code_verifier, client_id, oauth_state
|
|
1614
|
+
FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1612
1615
|
[userId, sessionId]
|
|
1613
1616
|
);
|
|
1614
|
-
|
|
1617
|
+
if (!rows[0]) return null;
|
|
1618
|
+
const row = rows[0];
|
|
1619
|
+
return {
|
|
1620
|
+
sessionId,
|
|
1621
|
+
userId,
|
|
1622
|
+
clientInformation: decryptObject(row.client_information),
|
|
1623
|
+
tokens: decryptObject(row.tokens),
|
|
1624
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1625
|
+
clientId: row.client_id ?? void 0,
|
|
1626
|
+
oauthState: row.oauth_state
|
|
1627
|
+
};
|
|
1615
1628
|
} catch (error) {
|
|
1616
1629
|
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1617
1630
|
return null;
|
|
@@ -1632,7 +1645,9 @@ var NeonStorageBackend = class {
|
|
|
1632
1645
|
async clearCredentials(userId, sessionId) {
|
|
1633
1646
|
try {
|
|
1634
1647
|
await this.sql.query(
|
|
1635
|
-
`
|
|
1648
|
+
`UPDATE ${this.tableName}
|
|
1649
|
+
SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
|
|
1650
|
+
WHERE user_id = $1 AND session_id = $2`,
|
|
1636
1651
|
[userId, sessionId]
|
|
1637
1652
|
);
|
|
1638
1653
|
} catch (error) {
|
|
@@ -1674,7 +1689,6 @@ var NeonStorageBackend = class {
|
|
|
1674
1689
|
}
|
|
1675
1690
|
async clearAll() {
|
|
1676
1691
|
try {
|
|
1677
|
-
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1678
1692
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1679
1693
|
} catch (error) {
|
|
1680
1694
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1919,6 +1933,60 @@ function onSessionMutation(listener) {
|
|
|
1919
1933
|
sessionMutationListeners.delete(listener);
|
|
1920
1934
|
};
|
|
1921
1935
|
}
|
|
1936
|
+
function withDbObservability(store, emit) {
|
|
1937
|
+
const readMethods = /* @__PURE__ */ new Set([
|
|
1938
|
+
"get",
|
|
1939
|
+
"getCredentials",
|
|
1940
|
+
"list",
|
|
1941
|
+
"listIds",
|
|
1942
|
+
"listAllIds"
|
|
1943
|
+
]);
|
|
1944
|
+
return new Proxy(store, {
|
|
1945
|
+
get(target, prop, receiver) {
|
|
1946
|
+
const original = Reflect.get(target, prop, receiver);
|
|
1947
|
+
if (typeof original !== "function") return original;
|
|
1948
|
+
return (...args) => {
|
|
1949
|
+
const method = prop;
|
|
1950
|
+
const isRead = readMethods.has(prop);
|
|
1951
|
+
const start = performance.now();
|
|
1952
|
+
const emitEvent = (error) => {
|
|
1953
|
+
emit({
|
|
1954
|
+
type: isRead ? "db:read" : "db:write",
|
|
1955
|
+
level: error ? "error" : "debug",
|
|
1956
|
+
message: `${method}(${args.map(
|
|
1957
|
+
(a) => typeof a === "string" ? a.slice(0, 24) : typeof a
|
|
1958
|
+
).join(", ")})`,
|
|
1959
|
+
sessionId: typeof args[1] === "string" ? args[1] : void 0,
|
|
1960
|
+
payload: {
|
|
1961
|
+
method,
|
|
1962
|
+
argTypes: args.map((a) => typeof a),
|
|
1963
|
+
durationMs: performance.now() - start,
|
|
1964
|
+
...error ? { error } : {}
|
|
1965
|
+
},
|
|
1966
|
+
timestamp: Date.now()
|
|
1967
|
+
});
|
|
1968
|
+
};
|
|
1969
|
+
try {
|
|
1970
|
+
const result = original.apply(target, args);
|
|
1971
|
+
if (result instanceof Promise) {
|
|
1972
|
+
return result.then((r) => {
|
|
1973
|
+
emitEvent();
|
|
1974
|
+
return r;
|
|
1975
|
+
}).catch((e) => {
|
|
1976
|
+
emitEvent(String(e));
|
|
1977
|
+
throw e;
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
emitEvent();
|
|
1981
|
+
return result;
|
|
1982
|
+
} catch (e) {
|
|
1983
|
+
emitEvent(String(e));
|
|
1984
|
+
throw e;
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1922
1990
|
var sessions = new Proxy({}, {
|
|
1923
1991
|
get(_target, prop) {
|
|
1924
1992
|
return async (...args) => {
|
|
@@ -1938,6 +2006,24 @@ var sessions = new Proxy({}, {
|
|
|
1938
2006
|
});
|
|
1939
2007
|
|
|
1940
2008
|
// src/server/mcp/storage-oauth-provider.ts
|
|
2009
|
+
var codeVerifierContext = new async_hooks.AsyncLocalStorage();
|
|
2010
|
+
function runWithCodeVerifierState(verifier, method, fn) {
|
|
2011
|
+
return codeVerifierContext.run({ verifier, method }, fn);
|
|
2012
|
+
}
|
|
2013
|
+
function base64UrlEncode(bytes) {
|
|
2014
|
+
let binary = "";
|
|
2015
|
+
for (const byte of bytes) {
|
|
2016
|
+
binary += String.fromCharCode(byte);
|
|
2017
|
+
}
|
|
2018
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2019
|
+
}
|
|
2020
|
+
async function createCodeChallenge(verifier) {
|
|
2021
|
+
const digest = await crypto.subtle.digest(
|
|
2022
|
+
"SHA-256",
|
|
2023
|
+
new TextEncoder().encode(verifier)
|
|
2024
|
+
);
|
|
2025
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
2026
|
+
}
|
|
1941
2027
|
var StorageOAuthClientProvider = class {
|
|
1942
2028
|
/**
|
|
1943
2029
|
* Creates a new session-backed OAuth provider
|
|
@@ -1953,8 +2039,12 @@ var StorageOAuthClientProvider = class {
|
|
|
1953
2039
|
__publicField(this, "logoUri");
|
|
1954
2040
|
__publicField(this, "policyUri");
|
|
1955
2041
|
__publicField(this, "clientSecret");
|
|
2042
|
+
__publicField(this, "_store");
|
|
1956
2043
|
__publicField(this, "_authUrl");
|
|
1957
2044
|
__publicField(this, "_clientId");
|
|
2045
|
+
__publicField(this, "_cachedCodeVerifier");
|
|
2046
|
+
__publicField(this, "_hasCodeVerifier", false);
|
|
2047
|
+
__publicField(this, "_cachedTokens");
|
|
1958
2048
|
__publicField(this, "onRedirectCallback");
|
|
1959
2049
|
this.userId = options.userId;
|
|
1960
2050
|
this.serverId = options.serverId;
|
|
@@ -1966,6 +2056,8 @@ var StorageOAuthClientProvider = class {
|
|
|
1966
2056
|
this.policyUri = options.policyUri;
|
|
1967
2057
|
this._clientId = options.clientId;
|
|
1968
2058
|
this.clientSecret = options.clientSecret;
|
|
2059
|
+
this._cachedTokens = options.cachedTokens;
|
|
2060
|
+
this._store = options.sessionStore ?? sessions;
|
|
1969
2061
|
this.onRedirectCallback = options.onRedirect;
|
|
1970
2062
|
}
|
|
1971
2063
|
get clientMetadata() {
|
|
@@ -1993,7 +2085,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1993
2085
|
* @private
|
|
1994
2086
|
*/
|
|
1995
2087
|
async getCredentials() {
|
|
1996
|
-
const data = await
|
|
2088
|
+
const data = await this._store.getCredentials(this.userId, this.sessionId);
|
|
1997
2089
|
if (!data) {
|
|
1998
2090
|
return { userId: this.userId, sessionId: this.sessionId };
|
|
1999
2091
|
}
|
|
@@ -2006,35 +2098,33 @@ var StorageOAuthClientProvider = class {
|
|
|
2006
2098
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
2007
2099
|
*/
|
|
2008
2100
|
async patchCredentials(data) {
|
|
2009
|
-
await
|
|
2101
|
+
await this._store.patchCredentials(this.userId, this.sessionId, data);
|
|
2010
2102
|
}
|
|
2011
2103
|
/**
|
|
2012
|
-
* Retrieves stored OAuth client information
|
|
2104
|
+
* Retrieves stored OAuth client information.
|
|
2013
2105
|
*/
|
|
2014
2106
|
async clientInformation() {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
await this.patchCredentials({
|
|
2021
|
-
clientId: this._clientId,
|
|
2022
|
-
clientInformation: {
|
|
2023
|
-
client_id: this._clientId,
|
|
2024
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2025
|
-
}
|
|
2026
|
-
});
|
|
2107
|
+
if (this._clientId) {
|
|
2108
|
+
return {
|
|
2109
|
+
client_id: this._clientId,
|
|
2110
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2111
|
+
};
|
|
2027
2112
|
}
|
|
2028
|
-
|
|
2029
|
-
|
|
2113
|
+
const data = await this.getCredentials();
|
|
2114
|
+
if (this._cachedTokens === void 0) {
|
|
2115
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2030
2116
|
}
|
|
2031
|
-
if (
|
|
2032
|
-
|
|
2117
|
+
if (data.clientId) {
|
|
2118
|
+
this._clientId = data.clientId;
|
|
2119
|
+
if (data.clientInformation) {
|
|
2120
|
+
return data.clientInformation;
|
|
2121
|
+
}
|
|
2122
|
+
return {
|
|
2123
|
+
client_id: data.clientId,
|
|
2124
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2125
|
+
};
|
|
2033
2126
|
}
|
|
2034
|
-
return
|
|
2035
|
-
client_id: this._clientId,
|
|
2036
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2037
|
-
};
|
|
2127
|
+
return void 0;
|
|
2038
2128
|
}
|
|
2039
2129
|
/**
|
|
2040
2130
|
* Stores OAuth client information
|
|
@@ -2051,11 +2141,30 @@ var StorageOAuthClientProvider = class {
|
|
|
2051
2141
|
*/
|
|
2052
2142
|
async saveTokens(tokens) {
|
|
2053
2143
|
await this.patchCredentials({ tokens });
|
|
2144
|
+
this._cachedTokens = tokens;
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Persists static client credentials to DB immediately on creation.
|
|
2148
|
+
* Ensures getOrCreateClient() finds them on rehydration, even when the
|
|
2149
|
+
* server allows anonymous connect and never triggers DCR.
|
|
2150
|
+
*/
|
|
2151
|
+
async initializeCredentials() {
|
|
2152
|
+
if (this._clientId) {
|
|
2153
|
+
await this.patchCredentials({
|
|
2154
|
+
clientId: this._clientId,
|
|
2155
|
+
clientInformation: {
|
|
2156
|
+
client_id: this._clientId,
|
|
2157
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2054
2161
|
}
|
|
2055
2162
|
get authUrl() {
|
|
2056
2163
|
return this._authUrl;
|
|
2057
2164
|
}
|
|
2058
2165
|
async state() {
|
|
2166
|
+
this._cachedCodeVerifier = void 0;
|
|
2167
|
+
this._hasCodeVerifier = false;
|
|
2059
2168
|
const nonce = nanoid.nanoid(32);
|
|
2060
2169
|
await this.patchCredentials({
|
|
2061
2170
|
oauthState: {
|
|
@@ -2076,7 +2185,7 @@ var StorageOAuthClientProvider = class {
|
|
|
2076
2185
|
if (parsed.sessionId !== this.sessionId) {
|
|
2077
2186
|
return { valid: false, error: "OAuth state mismatch" };
|
|
2078
2187
|
}
|
|
2079
|
-
const data = await
|
|
2188
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
2080
2189
|
if (!data) {
|
|
2081
2190
|
return { valid: false, error: "Session not found" };
|
|
2082
2191
|
}
|
|
@@ -2101,49 +2210,77 @@ var StorageOAuthClientProvider = class {
|
|
|
2101
2210
|
}
|
|
2102
2211
|
async redirectToAuthorization(authUrl) {
|
|
2103
2212
|
this._authUrl = authUrl.toString();
|
|
2104
|
-
|
|
2213
|
+
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
|
2214
|
+
const state = authUrl.searchParams.get("state");
|
|
2215
|
+
if (this._cachedCodeVerifier && codeChallenge && state) {
|
|
2216
|
+
const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
|
|
2217
|
+
if (expectedChallenge === codeChallenge) {
|
|
2218
|
+
const parsed = parseOAuthState(state);
|
|
2219
|
+
if (parsed) {
|
|
2220
|
+
await this.patchCredentials({
|
|
2221
|
+
codeVerifier: this._cachedCodeVerifier
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2105
2226
|
if (this.onRedirectCallback) {
|
|
2106
2227
|
this.onRedirectCallback(authUrl.toString());
|
|
2107
2228
|
}
|
|
2108
2229
|
}
|
|
2109
2230
|
async invalidateCredentials(scope) {
|
|
2110
2231
|
if (scope === "all") {
|
|
2111
|
-
|
|
2232
|
+
this._cachedTokens = void 0;
|
|
2233
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
2112
2234
|
} else {
|
|
2113
2235
|
const updates = {};
|
|
2114
2236
|
if (scope === "client") {
|
|
2115
2237
|
updates.clientInformation = null;
|
|
2116
2238
|
updates.clientId = null;
|
|
2117
2239
|
} else if (scope === "tokens") {
|
|
2240
|
+
this._cachedTokens = void 0;
|
|
2118
2241
|
updates.tokens = null;
|
|
2119
2242
|
} else if (scope === "verifier") {
|
|
2243
|
+
this._cachedCodeVerifier = void 0;
|
|
2244
|
+
this._hasCodeVerifier = false;
|
|
2120
2245
|
updates.codeVerifier = null;
|
|
2121
2246
|
}
|
|
2122
2247
|
await this.patchCredentials(updates);
|
|
2123
2248
|
}
|
|
2124
2249
|
}
|
|
2125
2250
|
async saveCodeVerifier(verifier) {
|
|
2126
|
-
|
|
2127
|
-
if (data.codeVerifier) {
|
|
2251
|
+
if (this._hasCodeVerifier) {
|
|
2128
2252
|
return;
|
|
2129
2253
|
}
|
|
2130
|
-
|
|
2254
|
+
this._cachedCodeVerifier = verifier;
|
|
2255
|
+
this._hasCodeVerifier = true;
|
|
2131
2256
|
}
|
|
2132
2257
|
async codeVerifier() {
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
this._clientId = data.clientId;
|
|
2258
|
+
if (this._cachedCodeVerifier) {
|
|
2259
|
+
return this._cachedCodeVerifier;
|
|
2136
2260
|
}
|
|
2137
|
-
|
|
2138
|
-
|
|
2261
|
+
const ctx = codeVerifierContext.getStore();
|
|
2262
|
+
if (ctx?.verifier) {
|
|
2263
|
+
return ctx.verifier;
|
|
2264
|
+
}
|
|
2265
|
+
const data = await this.getCredentials();
|
|
2266
|
+
if (data.codeVerifier) {
|
|
2267
|
+
return data.codeVerifier;
|
|
2139
2268
|
}
|
|
2140
|
-
|
|
2269
|
+
throw new Error("No code verifier found");
|
|
2141
2270
|
}
|
|
2142
2271
|
async deleteCodeVerifier() {
|
|
2143
|
-
|
|
2272
|
+
this._cachedCodeVerifier = void 0;
|
|
2273
|
+
this._hasCodeVerifier = false;
|
|
2274
|
+
await this.patchCredentials({
|
|
2275
|
+
codeVerifier: null
|
|
2276
|
+
});
|
|
2144
2277
|
}
|
|
2145
2278
|
async tokens() {
|
|
2279
|
+
if (this._cachedTokens !== void 0) {
|
|
2280
|
+
return this._cachedTokens ?? void 0;
|
|
2281
|
+
}
|
|
2146
2282
|
const data = await this.getCredentials();
|
|
2283
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2147
2284
|
if (data.clientId && !this._clientId) {
|
|
2148
2285
|
this._clientId = data.clientId;
|
|
2149
2286
|
}
|
|
@@ -2306,23 +2443,10 @@ var MCPClient = class {
|
|
|
2306
2443
|
__publicField(this, "client");
|
|
2307
2444
|
__publicField(this, "oauthProvider", null);
|
|
2308
2445
|
__publicField(this, "transport", null);
|
|
2309
|
-
__publicField(this, "
|
|
2310
|
-
__publicField(this, "serverId");
|
|
2311
|
-
__publicField(this, "sessionId");
|
|
2312
|
-
__publicField(this, "serverName");
|
|
2313
|
-
__publicField(this, "transportType");
|
|
2314
|
-
__publicField(this, "serverUrl");
|
|
2315
|
-
__publicField(this, "callbackUrl");
|
|
2316
|
-
__publicField(this, "onRedirect");
|
|
2317
|
-
__publicField(this, "clientId");
|
|
2318
|
-
__publicField(this, "clientSecret");
|
|
2319
|
-
__publicField(this, "headers");
|
|
2320
|
-
/** OAuth Client Metadata */
|
|
2321
|
-
__publicField(this, "clientName");
|
|
2322
|
-
__publicField(this, "clientUri");
|
|
2323
|
-
__publicField(this, "logoUri");
|
|
2324
|
-
__publicField(this, "policyUri");
|
|
2446
|
+
__publicField(this, "config");
|
|
2325
2447
|
__publicField(this, "createdAt");
|
|
2448
|
+
__publicField(this, "_serverInfo");
|
|
2449
|
+
__publicField(this, "_store");
|
|
2326
2450
|
/** Event emitters for connection lifecycle */
|
|
2327
2451
|
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
2328
2452
|
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
@@ -2338,21 +2462,8 @@ var MCPClient = class {
|
|
|
2338
2462
|
* the memory when the client is no longer needed.
|
|
2339
2463
|
*/
|
|
2340
2464
|
__publicField(this, "cachedTools", null);
|
|
2341
|
-
this.
|
|
2342
|
-
this.
|
|
2343
|
-
this.callbackUrl = options.callbackUrl;
|
|
2344
|
-
this.onRedirect = options.onRedirect;
|
|
2345
|
-
this.userId = options.userId;
|
|
2346
|
-
this.serverId = options.serverId;
|
|
2347
|
-
this.sessionId = options.sessionId;
|
|
2348
|
-
this.transportType = options.transportType;
|
|
2349
|
-
this.clientId = options.clientId;
|
|
2350
|
-
this.clientSecret = options.clientSecret;
|
|
2351
|
-
this.headers = options.headers;
|
|
2352
|
-
this.clientName = options.clientName;
|
|
2353
|
-
this.clientUri = options.clientUri;
|
|
2354
|
-
this.logoUri = options.logoUri;
|
|
2355
|
-
this.policyUri = options.policyUri;
|
|
2465
|
+
this.config = { ...options };
|
|
2466
|
+
this._store = options.sessionStore ?? sessions;
|
|
2356
2467
|
this.client = new index_js.Client(
|
|
2357
2468
|
{
|
|
2358
2469
|
name: MCP_CLIENT_NAME,
|
|
@@ -2369,6 +2480,21 @@ var MCPClient = class {
|
|
|
2369
2480
|
}
|
|
2370
2481
|
);
|
|
2371
2482
|
}
|
|
2483
|
+
/** Shared session-shaped data for ensureSession and saveSession */
|
|
2484
|
+
get session() {
|
|
2485
|
+
return {
|
|
2486
|
+
sessionId: this.config.sessionId,
|
|
2487
|
+
userId: this.config.userId,
|
|
2488
|
+
serverId: this.config.serverId,
|
|
2489
|
+
serverName: this.config.serverName,
|
|
2490
|
+
serverUrl: this.config.serverUrl,
|
|
2491
|
+
callbackUrl: this.config.callbackUrl,
|
|
2492
|
+
transportType: this.config.transportType || "streamable-http",
|
|
2493
|
+
headers: this.config.headers,
|
|
2494
|
+
createdAt: this.createdAt ?? Date.now(),
|
|
2495
|
+
updatedAt: Date.now()
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2372
2498
|
/**
|
|
2373
2499
|
* Emit a connection state change event
|
|
2374
2500
|
* @private
|
|
@@ -2376,13 +2502,13 @@ var MCPClient = class {
|
|
|
2376
2502
|
emitStateChange(newState) {
|
|
2377
2503
|
const previousState = this.currentState;
|
|
2378
2504
|
this.currentState = newState;
|
|
2379
|
-
if (!this.serverId) return;
|
|
2505
|
+
if (!this.config.serverId) return;
|
|
2380
2506
|
this._onConnectionEvent.fire({
|
|
2381
2507
|
type: "state_changed",
|
|
2382
|
-
sessionId: this.sessionId,
|
|
2383
|
-
serverId: this.serverId,
|
|
2384
|
-
serverName: this.serverName || this.serverId,
|
|
2385
|
-
serverUrl: this.serverUrl || "",
|
|
2508
|
+
sessionId: this.config.sessionId,
|
|
2509
|
+
serverId: this.config.serverId,
|
|
2510
|
+
serverName: this.config.serverName || this.config.serverId,
|
|
2511
|
+
serverUrl: this.config.serverUrl || "",
|
|
2386
2512
|
createdAt: this.createdAt,
|
|
2387
2513
|
state: newState,
|
|
2388
2514
|
previousState,
|
|
@@ -2393,8 +2519,8 @@ var MCPClient = class {
|
|
|
2393
2519
|
level: "info",
|
|
2394
2520
|
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
2395
2521
|
displayMessage: `State changed to ${newState}`,
|
|
2396
|
-
sessionId: this.sessionId,
|
|
2397
|
-
serverId: this.serverId,
|
|
2522
|
+
sessionId: this.config.sessionId,
|
|
2523
|
+
serverId: this.config.serverId,
|
|
2398
2524
|
payload: { previousState, newState },
|
|
2399
2525
|
timestamp: Date.now(),
|
|
2400
2526
|
id: nanoid.nanoid()
|
|
@@ -2405,11 +2531,11 @@ var MCPClient = class {
|
|
|
2405
2531
|
* @private
|
|
2406
2532
|
*/
|
|
2407
2533
|
emitError(error, errorType = "unknown") {
|
|
2408
|
-
if (!this.serverId) return;
|
|
2534
|
+
if (!this.config.serverId) return;
|
|
2409
2535
|
this._onConnectionEvent.fire({
|
|
2410
2536
|
type: "error",
|
|
2411
|
-
sessionId: this.sessionId,
|
|
2412
|
-
serverId: this.serverId,
|
|
2537
|
+
sessionId: this.config.sessionId,
|
|
2538
|
+
serverId: this.config.serverId,
|
|
2413
2539
|
error,
|
|
2414
2540
|
errorType,
|
|
2415
2541
|
timestamp: Date.now()
|
|
@@ -2419,8 +2545,8 @@ var MCPClient = class {
|
|
|
2419
2545
|
level: "error",
|
|
2420
2546
|
message: error,
|
|
2421
2547
|
displayMessage: error,
|
|
2422
|
-
sessionId: this.sessionId,
|
|
2423
|
-
serverId: this.serverId,
|
|
2548
|
+
sessionId: this.config.sessionId,
|
|
2549
|
+
serverId: this.config.serverId,
|
|
2424
2550
|
payload: { errorType, error },
|
|
2425
2551
|
timestamp: Date.now(),
|
|
2426
2552
|
id: nanoid.nanoid()
|
|
@@ -2431,11 +2557,11 @@ var MCPClient = class {
|
|
|
2431
2557
|
* @private
|
|
2432
2558
|
*/
|
|
2433
2559
|
emitProgress(message) {
|
|
2434
|
-
if (!this.serverId) return;
|
|
2560
|
+
if (!this.config.serverId) return;
|
|
2435
2561
|
this._onConnectionEvent.fire({
|
|
2436
2562
|
type: "progress",
|
|
2437
|
-
sessionId: this.sessionId,
|
|
2438
|
-
serverId: this.serverId,
|
|
2563
|
+
sessionId: this.config.sessionId,
|
|
2564
|
+
serverId: this.config.serverId,
|
|
2439
2565
|
message,
|
|
2440
2566
|
timestamp: Date.now()
|
|
2441
2567
|
});
|
|
@@ -2453,14 +2579,14 @@ var MCPClient = class {
|
|
|
2453
2579
|
* @private
|
|
2454
2580
|
*/
|
|
2455
2581
|
getTransport(type) {
|
|
2456
|
-
if (!this.serverUrl) {
|
|
2582
|
+
if (!this.config.serverUrl) {
|
|
2457
2583
|
throw new Error("Server URL is required to create transport");
|
|
2458
2584
|
}
|
|
2459
|
-
const baseUrl = new URL(this.serverUrl);
|
|
2460
|
-
const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2585
|
+
const baseUrl = new URL(this.config.serverUrl);
|
|
2586
|
+
const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2461
2587
|
const transportOptions = {
|
|
2462
2588
|
...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
|
|
2463
|
-
...this.headers && { requestInit: { headers: this.headers } },
|
|
2589
|
+
...this.config.headers && { requestInit: { headers: this.config.headers } },
|
|
2464
2590
|
/**
|
|
2465
2591
|
* Custom fetch implementation to handle connection timeouts.
|
|
2466
2592
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
@@ -2498,78 +2624,74 @@ var MCPClient = class {
|
|
|
2498
2624
|
* @private
|
|
2499
2625
|
*/
|
|
2500
2626
|
async ensureSession() {
|
|
2501
|
-
if (this.oauthProvider)
|
|
2502
|
-
return;
|
|
2503
|
-
}
|
|
2627
|
+
if (this.oauthProvider) return;
|
|
2504
2628
|
this.emitStateChange("INITIALIZING");
|
|
2505
2629
|
this.emitProgress("Loading session configuration...");
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
throw new Error(`Session not found: ${this.sessionId}`);
|
|
2630
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2631
|
+
const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2632
|
+
if (!existingSession2) {
|
|
2633
|
+
throw new Error(`Session not found: ${this.config.sessionId}`);
|
|
2511
2634
|
}
|
|
2512
|
-
this.serverUrl = this.serverUrl ||
|
|
2513
|
-
this.callbackUrl = this.callbackUrl ||
|
|
2514
|
-
this.serverName = this.serverName ||
|
|
2515
|
-
this.serverId = this.serverId ||
|
|
2516
|
-
this.headers = this.headers ||
|
|
2517
|
-
this.createdAt =
|
|
2518
|
-
}
|
|
2519
|
-
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
2635
|
+
this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
|
|
2636
|
+
this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
|
|
2637
|
+
this.config.serverName = this.config.serverName || existingSession2.serverName;
|
|
2638
|
+
this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
|
|
2639
|
+
this.config.headers = this.config.headers || existingSession2.headers;
|
|
2640
|
+
this.createdAt = existingSession2.createdAt;
|
|
2641
|
+
}
|
|
2642
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2520
2643
|
throw new Error("Missing required connection metadata");
|
|
2521
2644
|
}
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
this.
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
this.
|
|
2540
|
-
|
|
2645
|
+
this.oauthProvider = new StorageOAuthClientProvider({
|
|
2646
|
+
userId: this.config.userId,
|
|
2647
|
+
serverId: this.config.serverId,
|
|
2648
|
+
sessionId: this.config.sessionId,
|
|
2649
|
+
redirectUrl: this.config.callbackUrl,
|
|
2650
|
+
clientName: this.config.clientName,
|
|
2651
|
+
clientUri: this.config.clientUri,
|
|
2652
|
+
logoUri: this.config.logoUri,
|
|
2653
|
+
policyUri: this.config.policyUri,
|
|
2654
|
+
clientId: this.config.clientId,
|
|
2655
|
+
clientSecret: this.config.clientSecret,
|
|
2656
|
+
cachedTokens: this.config.cachedCredentials?.tokens,
|
|
2657
|
+
sessionStore: this._store,
|
|
2658
|
+
onRedirect: (redirectUrl) => {
|
|
2659
|
+
if (this.config.serverId) {
|
|
2660
|
+
this._onConnectionEvent.fire({
|
|
2661
|
+
type: "auth_required",
|
|
2662
|
+
sessionId: this.config.sessionId,
|
|
2663
|
+
serverId: this.config.serverId,
|
|
2664
|
+
authUrl: redirectUrl,
|
|
2665
|
+
timestamp: Date.now()
|
|
2666
|
+
});
|
|
2541
2667
|
}
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2668
|
+
if (this.config.onRedirect) {
|
|
2669
|
+
this.config.onRedirect(redirectUrl);
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
});
|
|
2673
|
+
const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
|
|
2674
|
+
if (!existingSession) {
|
|
2548
2675
|
this.createdAt = Date.now();
|
|
2549
2676
|
const updatedAt = this.createdAt;
|
|
2550
2677
|
this._onObservabilityEvent.fire({
|
|
2551
2678
|
type: "mcp:client:session_created",
|
|
2552
2679
|
level: "info",
|
|
2553
|
-
message: `Creating pending session ${this.sessionId} for connection setup`,
|
|
2554
|
-
sessionId: this.sessionId,
|
|
2555
|
-
serverId: this.serverId,
|
|
2680
|
+
message: `Creating pending session ${this.config.sessionId} for connection setup`,
|
|
2681
|
+
sessionId: this.config.sessionId,
|
|
2682
|
+
serverId: this.config.serverId,
|
|
2556
2683
|
timestamp: Date.now(),
|
|
2557
2684
|
id: nanoid.nanoid()
|
|
2558
2685
|
});
|
|
2559
|
-
await
|
|
2560
|
-
|
|
2561
|
-
userId: this.userId,
|
|
2562
|
-
serverId: this.serverId,
|
|
2563
|
-
serverName: this.serverName,
|
|
2564
|
-
serverUrl: this.serverUrl,
|
|
2565
|
-
callbackUrl: this.callbackUrl,
|
|
2566
|
-
transportType: this.transportType || "streamable-http",
|
|
2567
|
-
headers: this.headers,
|
|
2568
|
-
createdAt: this.createdAt,
|
|
2686
|
+
await this._store.create({
|
|
2687
|
+
...this.session,
|
|
2569
2688
|
updatedAt,
|
|
2570
2689
|
status: "pending"
|
|
2571
2690
|
});
|
|
2572
2691
|
}
|
|
2692
|
+
if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
|
|
2693
|
+
await this.oauthProvider.initializeCredentials();
|
|
2694
|
+
}
|
|
2573
2695
|
}
|
|
2574
2696
|
/**
|
|
2575
2697
|
* Saves current session state to the session store
|
|
@@ -2577,34 +2699,14 @@ var MCPClient = class {
|
|
|
2577
2699
|
* @param status - Session lifecycle status used by storage cleanup
|
|
2578
2700
|
* @private
|
|
2579
2701
|
*/
|
|
2580
|
-
async saveSession(status = "active"
|
|
2581
|
-
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2702
|
+
async saveSession(status = "active") {
|
|
2703
|
+
if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
|
|
2582
2704
|
return;
|
|
2583
2705
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
userId: this.userId,
|
|
2587
|
-
serverId: this.serverId,
|
|
2588
|
-
serverName: this.serverName,
|
|
2589
|
-
serverUrl: this.serverUrl,
|
|
2590
|
-
callbackUrl: this.callbackUrl,
|
|
2591
|
-
transportType: this.transportType || "streamable-http",
|
|
2592
|
-
headers: this.headers,
|
|
2593
|
-
createdAt: this.createdAt || Date.now(),
|
|
2594
|
-
updatedAt: Date.now(),
|
|
2706
|
+
await this._store.update(this.config.userId, this.config.sessionId, {
|
|
2707
|
+
...this.session,
|
|
2595
2708
|
status
|
|
2596
|
-
};
|
|
2597
|
-
if (status === "active") {
|
|
2598
|
-
sessionData.authUrl = null;
|
|
2599
|
-
}
|
|
2600
|
-
if (existingSession === void 0) {
|
|
2601
|
-
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2602
|
-
}
|
|
2603
|
-
if (existingSession) {
|
|
2604
|
-
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2605
|
-
} else {
|
|
2606
|
-
await sessions.create(sessionData);
|
|
2607
|
-
}
|
|
2709
|
+
});
|
|
2608
2710
|
}
|
|
2609
2711
|
/**
|
|
2610
2712
|
* Removes transient setup/auth sessions without masking the original error.
|
|
@@ -2612,7 +2714,7 @@ var MCPClient = class {
|
|
|
2612
2714
|
*/
|
|
2613
2715
|
async deleteTransientSession() {
|
|
2614
2716
|
try {
|
|
2615
|
-
await
|
|
2717
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2616
2718
|
} catch {
|
|
2617
2719
|
}
|
|
2618
2720
|
}
|
|
@@ -2622,7 +2724,7 @@ var MCPClient = class {
|
|
|
2622
2724
|
* @private
|
|
2623
2725
|
*/
|
|
2624
2726
|
async tryConnect() {
|
|
2625
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2727
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2626
2728
|
let lastError;
|
|
2627
2729
|
for (const currentType of transportsToTry) {
|
|
2628
2730
|
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
@@ -2630,6 +2732,7 @@ var MCPClient = class {
|
|
|
2630
2732
|
const transport = this.getTransport(currentType);
|
|
2631
2733
|
this.transport = transport;
|
|
2632
2734
|
await this.client.connect(transport);
|
|
2735
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2633
2736
|
return { transportType: currentType };
|
|
2634
2737
|
} catch (error) {
|
|
2635
2738
|
lastError = error;
|
|
@@ -2645,8 +2748,8 @@ var MCPClient = class {
|
|
|
2645
2748
|
this._onObservabilityEvent.fire({
|
|
2646
2749
|
level: "warn",
|
|
2647
2750
|
message: `Transport ${currentType} failed, falling back`,
|
|
2648
|
-
sessionId: this.sessionId,
|
|
2649
|
-
serverId: this.serverId,
|
|
2751
|
+
sessionId: this.config.sessionId,
|
|
2752
|
+
serverId: this.config.serverId,
|
|
2650
2753
|
metadata: {
|
|
2651
2754
|
failedTransport: currentType,
|
|
2652
2755
|
error: errorMessage
|
|
@@ -2689,15 +2792,15 @@ var MCPClient = class {
|
|
|
2689
2792
|
try {
|
|
2690
2793
|
this.emitStateChange("CONNECTING");
|
|
2691
2794
|
const { transportType } = await this.tryConnect();
|
|
2692
|
-
this.transportType = transportType;
|
|
2795
|
+
this.config.transportType = transportType;
|
|
2693
2796
|
this.emitStateChange("CONNECTED");
|
|
2694
2797
|
this.emitProgress("Connected successfully");
|
|
2695
2798
|
this._onObservabilityEvent.fire({
|
|
2696
2799
|
type: "mcp:client:session_saved",
|
|
2697
2800
|
level: "info",
|
|
2698
|
-
message: `Saving active session ${this.sessionId} (connect success)`,
|
|
2699
|
-
sessionId: this.sessionId,
|
|
2700
|
-
serverId: this.serverId,
|
|
2801
|
+
message: `Saving active session ${this.config.sessionId} (connect success)`,
|
|
2802
|
+
sessionId: this.config.sessionId,
|
|
2803
|
+
serverId: this.config.serverId,
|
|
2701
2804
|
timestamp: Date.now(),
|
|
2702
2805
|
id: nanoid.nanoid()
|
|
2703
2806
|
});
|
|
@@ -2720,23 +2823,23 @@ var MCPClient = class {
|
|
|
2720
2823
|
this._onObservabilityEvent.fire({
|
|
2721
2824
|
type: "mcp:client:session_saved",
|
|
2722
2825
|
level: "info",
|
|
2723
|
-
message: `Saving pending OAuth session ${this.sessionId}`,
|
|
2724
|
-
sessionId: this.sessionId,
|
|
2725
|
-
serverId: this.serverId,
|
|
2826
|
+
message: `Saving pending OAuth session ${this.config.sessionId}`,
|
|
2827
|
+
sessionId: this.config.sessionId,
|
|
2828
|
+
serverId: this.config.serverId,
|
|
2726
2829
|
timestamp: Date.now(),
|
|
2727
2830
|
id: nanoid.nanoid()
|
|
2728
2831
|
});
|
|
2729
2832
|
await this.saveSession("pending");
|
|
2730
|
-
if (this.serverId) {
|
|
2833
|
+
if (this.config.serverId) {
|
|
2731
2834
|
this._onConnectionEvent.fire({
|
|
2732
2835
|
type: "auth_required",
|
|
2733
|
-
sessionId: this.sessionId,
|
|
2734
|
-
serverId: this.serverId,
|
|
2836
|
+
sessionId: this.config.sessionId,
|
|
2837
|
+
serverId: this.config.serverId,
|
|
2735
2838
|
authUrl,
|
|
2736
2839
|
timestamp: Date.now()
|
|
2737
2840
|
});
|
|
2738
|
-
if (authUrl && this.onRedirect) {
|
|
2739
|
-
this.onRedirect(authUrl);
|
|
2841
|
+
if (authUrl && this.config.onRedirect) {
|
|
2842
|
+
this.config.onRedirect(authUrl);
|
|
2740
2843
|
}
|
|
2741
2844
|
}
|
|
2742
2845
|
throw new UnauthorizedError("OAuth authorization required");
|
|
@@ -2745,9 +2848,9 @@ var MCPClient = class {
|
|
|
2745
2848
|
this.emitError(errorMessage, "connection");
|
|
2746
2849
|
this.emitStateChange("FAILED");
|
|
2747
2850
|
try {
|
|
2748
|
-
const existingSession = await
|
|
2851
|
+
const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2749
2852
|
if (!existingSession || existingSession.status !== "active") {
|
|
2750
|
-
await
|
|
2853
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2751
2854
|
}
|
|
2752
2855
|
} catch {
|
|
2753
2856
|
}
|
|
@@ -2772,16 +2875,16 @@ var MCPClient = class {
|
|
|
2772
2875
|
throw new Error(error);
|
|
2773
2876
|
}
|
|
2774
2877
|
if (state) {
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2878
|
+
try {
|
|
2879
|
+
await this.oauthProvider.consumeState(state);
|
|
2880
|
+
} catch (error) {
|
|
2881
|
+
const msg = error instanceof Error ? error.message : "Invalid OAuth state";
|
|
2882
|
+
this.emitError(msg, "auth");
|
|
2779
2883
|
this.emitStateChange("FAILED");
|
|
2780
|
-
throw
|
|
2884
|
+
throw error;
|
|
2781
2885
|
}
|
|
2782
|
-
await this.oauthProvider.consumeState(state);
|
|
2783
2886
|
}
|
|
2784
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2887
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2785
2888
|
let lastError;
|
|
2786
2889
|
let tokensExchanged = false;
|
|
2787
2890
|
let authenticatedStateEmitted = false;
|
|
@@ -2808,14 +2911,15 @@ var MCPClient = class {
|
|
|
2808
2911
|
}
|
|
2809
2912
|
}
|
|
2810
2913
|
await this.client.connect(this.transport);
|
|
2811
|
-
this.
|
|
2914
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2915
|
+
this.config.transportType = currentType;
|
|
2812
2916
|
this.emitStateChange("CONNECTED");
|
|
2813
2917
|
this._onObservabilityEvent.fire({
|
|
2814
2918
|
type: "mcp:client:session_saved",
|
|
2815
2919
|
level: "info",
|
|
2816
|
-
message: `Saving active session ${this.sessionId} (OAuth complete)`,
|
|
2817
|
-
sessionId: this.sessionId,
|
|
2818
|
-
serverId: this.serverId,
|
|
2920
|
+
message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
|
|
2921
|
+
sessionId: this.config.sessionId,
|
|
2922
|
+
serverId: this.config.serverId,
|
|
2819
2923
|
timestamp: Date.now(),
|
|
2820
2924
|
id: nanoid.nanoid()
|
|
2821
2925
|
});
|
|
@@ -2888,11 +2992,11 @@ var MCPClient = class {
|
|
|
2888
2992
|
this.emitStateChange("DISCOVERING");
|
|
2889
2993
|
try {
|
|
2890
2994
|
const result = await this.fetchTools();
|
|
2891
|
-
if (this.serverId) {
|
|
2995
|
+
if (this.config.serverId) {
|
|
2892
2996
|
this._onConnectionEvent.fire({
|
|
2893
2997
|
type: "tools_discovered",
|
|
2894
|
-
sessionId: this.sessionId,
|
|
2895
|
-
serverId: this.serverId,
|
|
2998
|
+
sessionId: this.config.sessionId,
|
|
2999
|
+
serverId: this.config.serverId,
|
|
2896
3000
|
toolCount: result.tools.length,
|
|
2897
3001
|
tools: result.tools,
|
|
2898
3002
|
timestamp: Date.now()
|
|
@@ -2932,8 +3036,8 @@ var MCPClient = class {
|
|
|
2932
3036
|
level: "info",
|
|
2933
3037
|
message: `Tool ${toolName} called successfully`,
|
|
2934
3038
|
displayMessage: `Called tool ${toolName}`,
|
|
2935
|
-
sessionId: this.sessionId,
|
|
2936
|
-
serverId: this.serverId,
|
|
3039
|
+
sessionId: this.config.sessionId,
|
|
3040
|
+
serverId: this.config.serverId,
|
|
2937
3041
|
payload: {
|
|
2938
3042
|
toolName,
|
|
2939
3043
|
args: toolArgs
|
|
@@ -2949,8 +3053,8 @@ var MCPClient = class {
|
|
|
2949
3053
|
level: "error",
|
|
2950
3054
|
message: errorMessage,
|
|
2951
3055
|
displayMessage: `Failed to call tool ${toolName}`,
|
|
2952
|
-
sessionId: this.sessionId,
|
|
2953
|
-
serverId: this.serverId,
|
|
3056
|
+
sessionId: this.config.sessionId,
|
|
3057
|
+
serverId: this.config.serverId,
|
|
2954
3058
|
payload: {
|
|
2955
3059
|
errorType: "tool_execution",
|
|
2956
3060
|
error: errorMessage,
|
|
@@ -3098,8 +3202,8 @@ var MCPClient = class {
|
|
|
3098
3202
|
type: "mcp:client:error",
|
|
3099
3203
|
level: "warn",
|
|
3100
3204
|
message: "Initialization failed during clearSession",
|
|
3101
|
-
sessionId: this.sessionId,
|
|
3102
|
-
serverId: this.serverId,
|
|
3205
|
+
sessionId: this.config.sessionId,
|
|
3206
|
+
serverId: this.config.serverId,
|
|
3103
3207
|
payload: { error: String(error) },
|
|
3104
3208
|
timestamp: Date.now(),
|
|
3105
3209
|
id: nanoid.nanoid()
|
|
@@ -3108,7 +3212,7 @@ var MCPClient = class {
|
|
|
3108
3212
|
if (this.oauthProvider) {
|
|
3109
3213
|
await this.oauthProvider.invalidateCredentials("all");
|
|
3110
3214
|
}
|
|
3111
|
-
await
|
|
3215
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
3112
3216
|
await this.disconnect();
|
|
3113
3217
|
}
|
|
3114
3218
|
/**
|
|
@@ -3142,19 +3246,19 @@ var MCPClient = class {
|
|
|
3142
3246
|
}
|
|
3143
3247
|
this.oauthProvider = null;
|
|
3144
3248
|
this.transport = null;
|
|
3145
|
-
if (this.serverId) {
|
|
3249
|
+
if (this.config.serverId) {
|
|
3146
3250
|
this._onConnectionEvent.fire({
|
|
3147
3251
|
type: "disconnected",
|
|
3148
|
-
sessionId: this.sessionId,
|
|
3149
|
-
serverId: this.serverId,
|
|
3252
|
+
sessionId: this.config.sessionId,
|
|
3253
|
+
serverId: this.config.serverId,
|
|
3150
3254
|
timestamp: Date.now()
|
|
3151
3255
|
});
|
|
3152
3256
|
this._onObservabilityEvent.fire({
|
|
3153
3257
|
type: "mcp:client:disconnect",
|
|
3154
3258
|
level: "info",
|
|
3155
|
-
message: `Disconnected from ${this.serverId}`,
|
|
3156
|
-
sessionId: this.sessionId,
|
|
3157
|
-
serverId: this.serverId,
|
|
3259
|
+
message: `Disconnected from ${this.config.serverId}`,
|
|
3260
|
+
sessionId: this.config.sessionId,
|
|
3261
|
+
serverId: this.config.serverId,
|
|
3158
3262
|
payload: {},
|
|
3159
3263
|
timestamp: Date.now(),
|
|
3160
3264
|
id: nanoid.nanoid()
|
|
@@ -3179,42 +3283,52 @@ var MCPClient = class {
|
|
|
3179
3283
|
* @returns Server URL or empty string if not set
|
|
3180
3284
|
*/
|
|
3181
3285
|
getServerUrl() {
|
|
3182
|
-
return this.serverUrl || "";
|
|
3286
|
+
return this.config.serverUrl || "";
|
|
3183
3287
|
}
|
|
3184
3288
|
/**
|
|
3185
3289
|
* Gets the OAuth callback URL
|
|
3186
3290
|
* @returns Callback URL or empty string if not set
|
|
3187
3291
|
*/
|
|
3188
3292
|
getCallbackUrl() {
|
|
3189
|
-
return this.callbackUrl || "";
|
|
3293
|
+
return this.config.callbackUrl || "";
|
|
3190
3294
|
}
|
|
3191
3295
|
/**
|
|
3192
3296
|
* Gets the transport type being used
|
|
3193
3297
|
* @returns Transport type (defaults to 'streamable-http')
|
|
3194
3298
|
*/
|
|
3195
3299
|
getTransportType() {
|
|
3196
|
-
return this.transportType || "streamable-http";
|
|
3300
|
+
return this.config.transportType || "streamable-http";
|
|
3301
|
+
}
|
|
3302
|
+
/**
|
|
3303
|
+
* Gets the full server metadata from the MCP initialize response.
|
|
3304
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3305
|
+
* Returns undefined if the client hasn't connected yet.
|
|
3306
|
+
*/
|
|
3307
|
+
getServerInfo() {
|
|
3308
|
+
return this._serverInfo;
|
|
3197
3309
|
}
|
|
3198
3310
|
/**
|
|
3199
|
-
* Gets the human-readable server name
|
|
3311
|
+
* Gets the human-readable server name.
|
|
3312
|
+
* Prefers the server's reported title/name from the initialize response,
|
|
3313
|
+
* falling back to the name provided at construction or session metadata.
|
|
3200
3314
|
* @returns Server name or undefined
|
|
3201
3315
|
*/
|
|
3202
3316
|
getServerName() {
|
|
3203
|
-
return this.serverName;
|
|
3317
|
+
return this.config.serverName;
|
|
3204
3318
|
}
|
|
3205
3319
|
/**
|
|
3206
3320
|
* Gets the server ID
|
|
3207
3321
|
* @returns Server ID or undefined
|
|
3208
3322
|
*/
|
|
3209
3323
|
getServerId() {
|
|
3210
|
-
return this.serverId;
|
|
3324
|
+
return this.config.serverId;
|
|
3211
3325
|
}
|
|
3212
3326
|
/**
|
|
3213
3327
|
* Gets the session ID
|
|
3214
3328
|
* @returns Session ID
|
|
3215
3329
|
*/
|
|
3216
3330
|
getSessionId() {
|
|
3217
|
-
return this.sessionId;
|
|
3331
|
+
return this.config.sessionId;
|
|
3218
3332
|
}
|
|
3219
3333
|
};
|
|
3220
3334
|
|
|
@@ -3241,6 +3355,14 @@ var ToolPolicyGateway = class {
|
|
|
3241
3355
|
getServerId() {
|
|
3242
3356
|
return this.client.getServerId?.();
|
|
3243
3357
|
}
|
|
3358
|
+
/**
|
|
3359
|
+
* Returns the full server metadata from the underlying client, if available.
|
|
3360
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3361
|
+
* Available only after the client has connected and completed initialization.
|
|
3362
|
+
*/
|
|
3363
|
+
getServerInfo() {
|
|
3364
|
+
return this.client.getServerInfo?.();
|
|
3365
|
+
}
|
|
3244
3366
|
/**
|
|
3245
3367
|
* Returns the human-readable server name from the underlying client, if available.
|
|
3246
3368
|
*/
|
|
@@ -3369,6 +3491,7 @@ var MultiSessionClient = class {
|
|
|
3369
3491
|
__publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
|
|
3370
3492
|
__publicField(this, "userId");
|
|
3371
3493
|
__publicField(this, "options");
|
|
3494
|
+
__publicField(this, "_store");
|
|
3372
3495
|
this.userId = userId;
|
|
3373
3496
|
this.options = {
|
|
3374
3497
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
@@ -3376,6 +3499,7 @@ var MultiSessionClient = class {
|
|
|
3376
3499
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
3377
3500
|
...options
|
|
3378
3501
|
};
|
|
3502
|
+
this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
|
|
3379
3503
|
}
|
|
3380
3504
|
// -----------------------------------------------------------------------
|
|
3381
3505
|
// Public API
|
|
@@ -3459,9 +3583,9 @@ var MultiSessionClient = class {
|
|
|
3459
3583
|
* to querying the storage backend via `sessions.list(userId)`.
|
|
3460
3584
|
*/
|
|
3461
3585
|
async fetchActiveSessions() {
|
|
3462
|
-
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await
|
|
3586
|
+
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
|
|
3463
3587
|
return sessionList.filter(
|
|
3464
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
3588
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
|
|
3465
3589
|
);
|
|
3466
3590
|
}
|
|
3467
3591
|
/**
|
|
@@ -3534,8 +3658,13 @@ var MultiSessionClient = class {
|
|
|
3534
3658
|
callbackUrl: session.callbackUrl,
|
|
3535
3659
|
serverName: session.serverName,
|
|
3536
3660
|
transportType: session.transportType,
|
|
3537
|
-
headers: session.headers
|
|
3661
|
+
headers: session.headers,
|
|
3662
|
+
hasSession: true,
|
|
3663
|
+
sessionStore: this._store
|
|
3538
3664
|
});
|
|
3665
|
+
if (this.options.onObservabilityEvent) {
|
|
3666
|
+
client.onObservabilityEvent(this.options.onObservabilityEvent);
|
|
3667
|
+
}
|
|
3539
3668
|
const timeoutMs = this.options.timeout;
|
|
3540
3669
|
let timeoutTimer;
|
|
3541
3670
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3594,130 +3723,188 @@ function isConnectionEvent(event) {
|
|
|
3594
3723
|
}
|
|
3595
3724
|
|
|
3596
3725
|
// src/server/handlers/sse-handler.ts
|
|
3597
|
-
var
|
|
3726
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
3598
3727
|
function normalizeHeaders(headers) {
|
|
3599
3728
|
if (!headers || typeof headers !== "object") return void 0;
|
|
3600
|
-
const entries = Object.entries(headers).map(([
|
|
3729
|
+
const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
|
|
3601
3730
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3602
3731
|
}
|
|
3732
|
+
function validatingStateEvent(sessionId, session) {
|
|
3733
|
+
return {
|
|
3734
|
+
type: "state_changed",
|
|
3735
|
+
sessionId,
|
|
3736
|
+
serverId: session.serverId ?? "unknown",
|
|
3737
|
+
serverName: session.serverName ?? "Unknown",
|
|
3738
|
+
serverUrl: session.serverUrl,
|
|
3739
|
+
state: "VALIDATING",
|
|
3740
|
+
previousState: "DISCONNECTED",
|
|
3741
|
+
timestamp: Date.now()
|
|
3742
|
+
};
|
|
3743
|
+
}
|
|
3744
|
+
function connectionErrorEvent(sessionId, serverId, error, errorType) {
|
|
3745
|
+
return {
|
|
3746
|
+
type: "error",
|
|
3747
|
+
sessionId,
|
|
3748
|
+
serverId: serverId ?? "unknown",
|
|
3749
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
3750
|
+
errorType,
|
|
3751
|
+
timestamp: Date.now()
|
|
3752
|
+
};
|
|
3753
|
+
}
|
|
3603
3754
|
var SSEConnectionManager = class {
|
|
3755
|
+
// -----------------------------------------------------------------------
|
|
3756
|
+
// Constructor
|
|
3757
|
+
// -----------------------------------------------------------------------
|
|
3758
|
+
/**
|
|
3759
|
+
* @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
|
|
3760
|
+
* @param sendEvent - Callback that writes a typed event onto the SSE stream.
|
|
3761
|
+
*/
|
|
3604
3762
|
constructor(options, sendEvent) {
|
|
3605
3763
|
this.options = options;
|
|
3606
3764
|
this.sendEvent = sendEvent;
|
|
3765
|
+
// -----------------------------------------------------------------------
|
|
3766
|
+
// State
|
|
3767
|
+
// -----------------------------------------------------------------------
|
|
3607
3768
|
__publicField(this, "userId");
|
|
3769
|
+
/** Active MCP transports keyed by sessionId. */
|
|
3608
3770
|
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
3771
|
+
/** Instrumented session store — always wraps `sessions` with DB observability. */
|
|
3772
|
+
__publicField(this, "observedStore");
|
|
3609
3773
|
__publicField(this, "heartbeatTimer");
|
|
3610
|
-
__publicField(this, "
|
|
3774
|
+
__publicField(this, "active", true);
|
|
3611
3775
|
this.userId = options.userId;
|
|
3776
|
+
this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
|
|
3612
3777
|
this.startHeartbeat();
|
|
3613
3778
|
}
|
|
3779
|
+
// -----------------------------------------------------------------------
|
|
3780
|
+
// Public API
|
|
3781
|
+
// -----------------------------------------------------------------------
|
|
3614
3782
|
/**
|
|
3615
|
-
*
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
}
|
|
3626
|
-
return metadata;
|
|
3627
|
-
}
|
|
3628
|
-
/**
|
|
3629
|
-
* Start heartbeat to keep connection alive
|
|
3630
|
-
*/
|
|
3631
|
-
startHeartbeat() {
|
|
3632
|
-
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
3633
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3634
|
-
if (this.isActive) {
|
|
3635
|
-
this.sendEvent({
|
|
3636
|
-
level: "debug",
|
|
3637
|
-
message: "heartbeat",
|
|
3638
|
-
timestamp: Date.now()
|
|
3639
|
-
});
|
|
3640
|
-
}
|
|
3641
|
-
}, interval);
|
|
3642
|
-
}
|
|
3643
|
-
/**
|
|
3644
|
-
* Handle incoming RPC requests
|
|
3645
|
-
* Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
|
|
3783
|
+
* Dispatches an incoming RPC request, emits timing observability,
|
|
3784
|
+
* and returns the response.
|
|
3785
|
+
*
|
|
3786
|
+
* Emits `rpc:start` before dispatch and `rpc:end` on completion
|
|
3787
|
+
* (success or error), each carrying the method name, sessionId, and
|
|
3788
|
+
* duration. All events flow through the unified `onObservability`
|
|
3789
|
+
* and the SSE stream.
|
|
3790
|
+
*
|
|
3791
|
+
* @param request - The deserialized RPC envelope.
|
|
3792
|
+
* @returns The RPC response (success or error).
|
|
3646
3793
|
*/
|
|
3647
3794
|
async handleRequest(request) {
|
|
3795
|
+
const method = request.method;
|
|
3796
|
+
const sessionId = request.params?.sessionId;
|
|
3797
|
+
const t0 = performance.now();
|
|
3798
|
+
this.sendEvent({
|
|
3799
|
+
type: "rpc:start",
|
|
3800
|
+
level: "debug",
|
|
3801
|
+
message: method,
|
|
3802
|
+
sessionId,
|
|
3803
|
+
timestamp: Date.now()
|
|
3804
|
+
});
|
|
3648
3805
|
try {
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
break;
|
|
3660
|
-
case "disconnect":
|
|
3661
|
-
result = await this.disconnect(request.params);
|
|
3662
|
-
break;
|
|
3663
|
-
case "listTools":
|
|
3664
|
-
result = await this.listTools(request.params);
|
|
3665
|
-
break;
|
|
3666
|
-
case "setToolPolicy":
|
|
3667
|
-
result = await this.setToolPolicy(request.params);
|
|
3668
|
-
break;
|
|
3669
|
-
case "getToolPolicy":
|
|
3670
|
-
result = await this.getToolPolicy(request.params);
|
|
3671
|
-
break;
|
|
3672
|
-
case "callTool":
|
|
3673
|
-
result = await this.callTool(request.params);
|
|
3674
|
-
break;
|
|
3675
|
-
case "getSession":
|
|
3676
|
-
result = await this.getSession(request.params);
|
|
3677
|
-
break;
|
|
3678
|
-
case "finishAuth":
|
|
3679
|
-
result = await this.finishAuth(request.params);
|
|
3680
|
-
break;
|
|
3681
|
-
case "listPrompts":
|
|
3682
|
-
result = await this.listPrompts(request.params);
|
|
3683
|
-
break;
|
|
3684
|
-
case "getPrompt":
|
|
3685
|
-
result = await this.getPrompt(request.params);
|
|
3686
|
-
break;
|
|
3687
|
-
case "listResources":
|
|
3688
|
-
result = await this.listResources(request.params);
|
|
3689
|
-
break;
|
|
3690
|
-
case "readResource":
|
|
3691
|
-
result = await this.readResource(request.params);
|
|
3692
|
-
break;
|
|
3693
|
-
default:
|
|
3694
|
-
throw new Error(`Unknown method: ${request.method}`);
|
|
3695
|
-
}
|
|
3696
|
-
const response = {
|
|
3697
|
-
id: request.id,
|
|
3698
|
-
result
|
|
3699
|
-
};
|
|
3806
|
+
const result = await this.dispatchImpl(request);
|
|
3807
|
+
this.sendEvent({
|
|
3808
|
+
type: "rpc:end",
|
|
3809
|
+
level: "debug",
|
|
3810
|
+
message: method,
|
|
3811
|
+
sessionId,
|
|
3812
|
+
payload: { durationMs: performance.now() - t0 },
|
|
3813
|
+
timestamp: Date.now()
|
|
3814
|
+
});
|
|
3815
|
+
const response = { id: request.id, result };
|
|
3700
3816
|
this.sendEvent(response);
|
|
3701
3817
|
return response;
|
|
3702
3818
|
} catch (error) {
|
|
3703
|
-
|
|
3819
|
+
this.sendEvent({
|
|
3820
|
+
type: "rpc:end",
|
|
3821
|
+
level: "error",
|
|
3822
|
+
message: method,
|
|
3823
|
+
sessionId,
|
|
3824
|
+
payload: { durationMs: performance.now() - t0, error: String(error) },
|
|
3825
|
+
timestamp: Date.now()
|
|
3826
|
+
});
|
|
3827
|
+
const response = {
|
|
3704
3828
|
id: request.id,
|
|
3705
3829
|
error: {
|
|
3706
3830
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
3707
3831
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
3708
3832
|
}
|
|
3709
3833
|
};
|
|
3710
|
-
this.sendEvent(
|
|
3711
|
-
return
|
|
3834
|
+
this.sendEvent(response);
|
|
3835
|
+
return response;
|
|
3712
3836
|
}
|
|
3713
3837
|
}
|
|
3714
3838
|
/**
|
|
3715
|
-
*
|
|
3839
|
+
* Tears down all active MCP transports and stops the heartbeat timer.
|
|
3840
|
+
*
|
|
3841
|
+
* Disconnects are issued in parallel across all sessions. After calling
|
|
3842
|
+
* this method the manager instance should be discarded.
|
|
3843
|
+
*/
|
|
3844
|
+
async dispose() {
|
|
3845
|
+
this.active = false;
|
|
3846
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
3847
|
+
await Promise.all(
|
|
3848
|
+
Array.from(this.clients.values()).map((c) => c.disconnect())
|
|
3849
|
+
);
|
|
3850
|
+
this.clients.clear();
|
|
3851
|
+
}
|
|
3852
|
+
// -----------------------------------------------------------------------
|
|
3853
|
+
// RPC Dispatch (raw — called by handleRequest which adds timing)
|
|
3854
|
+
// -----------------------------------------------------------------------
|
|
3855
|
+
/**
|
|
3856
|
+
* Routes an RPC method name to the appropriate private handler.
|
|
3857
|
+
*
|
|
3858
|
+
* @throws {Error} When the method name is unrecognized.
|
|
3859
|
+
*/
|
|
3860
|
+
async dispatchImpl(request) {
|
|
3861
|
+
switch (request.method) {
|
|
3862
|
+
case "listSessions":
|
|
3863
|
+
return this.listSessions();
|
|
3864
|
+
case "connect":
|
|
3865
|
+
return this.connect(request.params);
|
|
3866
|
+
case "reconnect":
|
|
3867
|
+
return this.reconnect(request.params);
|
|
3868
|
+
case "disconnect":
|
|
3869
|
+
return this.disconnect(request.params);
|
|
3870
|
+
case "listTools":
|
|
3871
|
+
return this.listTools(request.params);
|
|
3872
|
+
case "setToolPolicy":
|
|
3873
|
+
return this.setToolPolicy(request.params);
|
|
3874
|
+
case "getToolPolicy":
|
|
3875
|
+
return this.getToolPolicy(request.params);
|
|
3876
|
+
case "updateSession":
|
|
3877
|
+
return this.updateSession(request.params);
|
|
3878
|
+
case "callTool":
|
|
3879
|
+
return this.callTool(request.params);
|
|
3880
|
+
case "getSession":
|
|
3881
|
+
return this.getSession(request.params);
|
|
3882
|
+
case "finishAuth":
|
|
3883
|
+
return this.finishAuth(request.params);
|
|
3884
|
+
case "listPrompts":
|
|
3885
|
+
return this.listPrompts(request.params);
|
|
3886
|
+
case "getPrompt":
|
|
3887
|
+
return this.getPrompt(request.params);
|
|
3888
|
+
case "listResources":
|
|
3889
|
+
return this.listResources(request.params);
|
|
3890
|
+
case "readResource":
|
|
3891
|
+
return this.readResource(request.params);
|
|
3892
|
+
default:
|
|
3893
|
+
throw new Error(`Unknown RPC method: ${request.method}`);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
// -----------------------------------------------------------------------
|
|
3897
|
+
// Session Query
|
|
3898
|
+
// -----------------------------------------------------------------------
|
|
3899
|
+
/**
|
|
3900
|
+
* Lists all sessions owned by the current user.
|
|
3901
|
+
*
|
|
3902
|
+
* Returns a lightweight view — session metadata only, no credential fields.
|
|
3716
3903
|
*/
|
|
3717
3904
|
async listSessions() {
|
|
3718
|
-
const
|
|
3905
|
+
const all = await sessions.list(this.userId);
|
|
3719
3906
|
return {
|
|
3720
|
-
sessions:
|
|
3907
|
+
sessions: all.map((s) => ({
|
|
3721
3908
|
sessionId: s.sessionId,
|
|
3722
3909
|
serverId: s.serverId,
|
|
3723
3910
|
serverName: s.serverName,
|
|
@@ -3726,215 +3913,203 @@ var SSEConnectionManager = class {
|
|
|
3726
3913
|
createdAt: s.createdAt,
|
|
3727
3914
|
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3728
3915
|
status: s.status ?? "pending",
|
|
3729
|
-
toolPolicy: s.toolPolicy
|
|
3916
|
+
toolPolicy: s.toolPolicy,
|
|
3917
|
+
enabled: s.enabled ?? true
|
|
3730
3918
|
}))
|
|
3731
3919
|
};
|
|
3732
3920
|
}
|
|
3733
3921
|
/**
|
|
3734
|
-
*
|
|
3922
|
+
* Restores and validates a previously persisted session.
|
|
3923
|
+
*
|
|
3924
|
+
* Loads the full session row (including credentials) from storage, creates
|
|
3925
|
+
* a new MCP transport, connects to the remote server, and emits the
|
|
3926
|
+
* discovered (policy-filtered) tool list via SSE.
|
|
3735
3927
|
*/
|
|
3736
|
-
async
|
|
3737
|
-
const
|
|
3738
|
-
|
|
3739
|
-
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
|
|
3740
|
-
const existingSessions = await sessions.list(this.userId);
|
|
3741
|
-
const duplicate = existingSessions.find(
|
|
3742
|
-
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
3743
|
-
);
|
|
3744
|
-
if (duplicate) {
|
|
3745
|
-
if (duplicate.status === "pending") {
|
|
3746
|
-
await this.getSession({ sessionId: duplicate.sessionId });
|
|
3747
|
-
return {
|
|
3748
|
-
sessionId: duplicate.sessionId,
|
|
3749
|
-
success: true
|
|
3750
|
-
};
|
|
3751
|
-
}
|
|
3752
|
-
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
3753
|
-
}
|
|
3754
|
-
const sessionId = await sessions.generateSessionId();
|
|
3928
|
+
async getSession(params) {
|
|
3929
|
+
const session = await this.requireSession(params.sessionId);
|
|
3930
|
+
this.sendEvent(validatingStateEvent(params.sessionId, session));
|
|
3755
3931
|
try {
|
|
3756
|
-
const
|
|
3757
|
-
|
|
3758
|
-
userId: this.userId,
|
|
3759
|
-
sessionId,
|
|
3760
|
-
serverId,
|
|
3761
|
-
serverName,
|
|
3762
|
-
serverUrl,
|
|
3763
|
-
callbackUrl,
|
|
3764
|
-
transportType,
|
|
3765
|
-
headers,
|
|
3766
|
-
clientId,
|
|
3767
|
-
clientSecret,
|
|
3768
|
-
...clientMetadata
|
|
3769
|
-
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
3770
|
-
});
|
|
3771
|
-
this.clients.set(sessionId, client);
|
|
3772
|
-
client.onConnectionEvent((event) => {
|
|
3773
|
-
this.emitConnectionEvent(event);
|
|
3774
|
-
});
|
|
3775
|
-
client.onObservabilityEvent((event) => {
|
|
3776
|
-
this.sendEvent(event);
|
|
3777
|
-
});
|
|
3932
|
+
const client = this.restoreClient(session);
|
|
3933
|
+
this.attachClientEvents(client);
|
|
3778
3934
|
await client.connect();
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
success: true
|
|
3783
|
-
};
|
|
3935
|
+
this.clients.set(params.sessionId, client);
|
|
3936
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3937
|
+
return { success: true, toolCount: result.tools.length };
|
|
3784
3938
|
} catch (error) {
|
|
3785
|
-
|
|
3786
|
-
this.clients.delete(sessionId);
|
|
3787
|
-
return {
|
|
3788
|
-
sessionId,
|
|
3789
|
-
success: true
|
|
3790
|
-
};
|
|
3791
|
-
}
|
|
3792
|
-
this.emitConnectionEvent({
|
|
3793
|
-
type: "error",
|
|
3794
|
-
sessionId,
|
|
3795
|
-
serverId,
|
|
3796
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3797
|
-
errorType: "connection",
|
|
3798
|
-
timestamp: Date.now()
|
|
3799
|
-
});
|
|
3800
|
-
this.clients.delete(sessionId);
|
|
3939
|
+
this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
|
|
3801
3940
|
throw error;
|
|
3802
3941
|
}
|
|
3803
3942
|
}
|
|
3943
|
+
// -----------------------------------------------------------------------
|
|
3944
|
+
// Connection Lifecycle
|
|
3945
|
+
// -----------------------------------------------------------------------
|
|
3804
3946
|
/**
|
|
3805
|
-
*
|
|
3806
|
-
*
|
|
3947
|
+
* Initiates a connection to a new MCP server.
|
|
3948
|
+
*
|
|
3949
|
+
* If a session for the same `serverId` or `serverUrl` already exists and
|
|
3950
|
+
* is still in a `pending` (OAuth) state, the existing session is resumed
|
|
3951
|
+
* instead of creating a duplicate.
|
|
3952
|
+
*
|
|
3953
|
+
* `UnauthorizedError` is treated as a pending-auth state and returned as
|
|
3954
|
+
* a successful result (the client will then redirect to the auth URL).
|
|
3807
3955
|
*/
|
|
3808
|
-
async
|
|
3809
|
-
const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
|
|
3956
|
+
async connect(params) {
|
|
3810
3957
|
const headers = normalizeHeaders(params.headers);
|
|
3811
|
-
const serverId =
|
|
3812
|
-
const
|
|
3813
|
-
|
|
3814
|
-
(
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
if (existingClient) {
|
|
3820
|
-
await existingClient.disconnect();
|
|
3821
|
-
this.clients.delete(duplicate.sessionId);
|
|
3958
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3959
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3960
|
+
if (existing) {
|
|
3961
|
+
if (existing.status === "pending") {
|
|
3962
|
+
return this.getSession({ sessionId: existing.sessionId }).then(() => ({
|
|
3963
|
+
sessionId: existing.sessionId,
|
|
3964
|
+
success: true
|
|
3965
|
+
}));
|
|
3822
3966
|
}
|
|
3967
|
+
throw new Error(
|
|
3968
|
+
`Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
|
|
3969
|
+
);
|
|
3823
3970
|
}
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3971
|
+
const sessionId = await sessions.generateSessionId();
|
|
3972
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3973
|
+
const client = new MCPClient({
|
|
3974
|
+
userId: this.userId,
|
|
3975
|
+
sessionId,
|
|
3976
|
+
serverId,
|
|
3977
|
+
serverName: params.serverName,
|
|
3978
|
+
serverUrl: params.serverUrl,
|
|
3979
|
+
callbackUrl: params.callbackUrl,
|
|
3980
|
+
transportType: params.transportType,
|
|
3981
|
+
headers,
|
|
3982
|
+
sessionStore: this.observedStore,
|
|
3983
|
+
...metadata
|
|
3984
|
+
});
|
|
3985
|
+
this.cacheClient(sessionId, client);
|
|
3986
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Reconnects to an MCP server by tearing down the active transport if one
|
|
3990
|
+
* exists and instantiating a fresh connection, reusing the existing session
|
|
3991
|
+
* (and its stored credentials / DCR client info) from the database.
|
|
3992
|
+
*/
|
|
3993
|
+
async reconnect(params) {
|
|
3994
|
+
const headers = normalizeHeaders(params.headers);
|
|
3995
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3996
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3997
|
+
const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
|
|
3998
|
+
if (existing) {
|
|
3999
|
+
const staleClient = this.clients.get(existing.sessionId);
|
|
4000
|
+
if (staleClient) {
|
|
4001
|
+
await staleClient.disconnect();
|
|
4002
|
+
this.clients.delete(existing.sessionId);
|
|
3853
4003
|
}
|
|
3854
|
-
this.emitConnectionEvent({
|
|
3855
|
-
type: "error",
|
|
3856
|
-
sessionId,
|
|
3857
|
-
serverId,
|
|
3858
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3859
|
-
errorType: "connection",
|
|
3860
|
-
timestamp: Date.now()
|
|
3861
|
-
});
|
|
3862
|
-
this.clients.delete(sessionId);
|
|
3863
|
-
throw error;
|
|
3864
4004
|
}
|
|
4005
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
4006
|
+
const client = new MCPClient({
|
|
4007
|
+
userId: this.userId,
|
|
4008
|
+
sessionId,
|
|
4009
|
+
serverId,
|
|
4010
|
+
serverName: params.serverName,
|
|
4011
|
+
serverUrl: params.serverUrl,
|
|
4012
|
+
callbackUrl: params.callbackUrl,
|
|
4013
|
+
transportType: params.transportType,
|
|
4014
|
+
headers,
|
|
4015
|
+
sessionStore: this.observedStore,
|
|
4016
|
+
...metadata
|
|
4017
|
+
});
|
|
4018
|
+
this.cacheClient(sessionId, client);
|
|
4019
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3865
4020
|
}
|
|
3866
4021
|
/**
|
|
3867
|
-
*
|
|
4022
|
+
* Disconnects from an MCP server.
|
|
4023
|
+
*
|
|
4024
|
+
* If an active in-memory transport exists it delegates to `clearSession()`
|
|
4025
|
+
* (which sends the server an HTTP DELETE per the Streamable spec). If no
|
|
4026
|
+
* active transport is available (orphaned session from a failed OAuth flow),
|
|
4027
|
+
* the session row is removed directly from storage.
|
|
3868
4028
|
*/
|
|
3869
4029
|
async disconnect(params) {
|
|
3870
|
-
const
|
|
3871
|
-
const client = this.clients.get(sessionId);
|
|
4030
|
+
const client = this.clients.get(params.sessionId);
|
|
3872
4031
|
if (client) {
|
|
3873
4032
|
await client.clearSession();
|
|
3874
|
-
this.clients.delete(sessionId);
|
|
4033
|
+
this.clients.delete(params.sessionId);
|
|
3875
4034
|
} else {
|
|
3876
|
-
await sessions.delete(this.userId, sessionId);
|
|
4035
|
+
await sessions.delete(this.userId, params.sessionId);
|
|
3877
4036
|
}
|
|
3878
4037
|
return { success: true };
|
|
3879
4038
|
}
|
|
4039
|
+
// -----------------------------------------------------------------------
|
|
4040
|
+
// OAuth
|
|
4041
|
+
// -----------------------------------------------------------------------
|
|
3880
4042
|
/**
|
|
3881
|
-
*
|
|
4043
|
+
* Completes the OAuth 2.1 authorization code flow for a pending session.
|
|
4044
|
+
*
|
|
4045
|
+
* Loads the stored session (with credentials), creates a fresh `MCPClient`,
|
|
4046
|
+
* and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
|
|
4047
|
+
* so the PKCE code verifier is available without a DB read.
|
|
4048
|
+
*
|
|
4049
|
+
* The session's stored `transportType` is intentionally **omitted** from the
|
|
4050
|
+
* client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
|
|
4051
|
+
* first, fall back to SSE), which is required for servers like Neon that
|
|
4052
|
+
* only support SSE transport.
|
|
3882
4053
|
*/
|
|
3883
|
-
async
|
|
3884
|
-
const
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
4054
|
+
async finishAuth(params) {
|
|
4055
|
+
const parsed = parseOAuthState(params.state);
|
|
4056
|
+
const sessionId = parsed?.sessionId ?? params.state;
|
|
4057
|
+
const session = await this.requireSession(sessionId);
|
|
4058
|
+
try {
|
|
4059
|
+
const clientId = session.clientId ?? void 0;
|
|
4060
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
4061
|
+
const client = new MCPClient({
|
|
4062
|
+
userId: this.userId,
|
|
4063
|
+
sessionId,
|
|
4064
|
+
serverId: session.serverId,
|
|
4065
|
+
serverName: session.serverName,
|
|
4066
|
+
serverUrl: session.serverUrl,
|
|
4067
|
+
callbackUrl: session.callbackUrl,
|
|
4068
|
+
headers: session.headers,
|
|
4069
|
+
clientId,
|
|
4070
|
+
clientSecret,
|
|
4071
|
+
hasSession: true,
|
|
4072
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
4073
|
+
sessionStore: this.observedStore
|
|
4074
|
+
});
|
|
4075
|
+
this.attachClientEvents(client);
|
|
4076
|
+
await runWithCodeVerifierState(
|
|
4077
|
+
session.codeVerifier ?? "",
|
|
4078
|
+
"S256",
|
|
4079
|
+
() => client.finishAuth(params.code, params.state)
|
|
4080
|
+
);
|
|
4081
|
+
this.clients.set(sessionId, client);
|
|
4082
|
+
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4083
|
+
return { success: true, toolCount: result.tools.length };
|
|
4084
|
+
} catch (error) {
|
|
4085
|
+
this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
|
|
4086
|
+
throw error;
|
|
3891
4087
|
}
|
|
3892
|
-
const client = new MCPClient({
|
|
3893
|
-
userId: this.userId,
|
|
3894
|
-
sessionId,
|
|
3895
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3896
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3897
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3898
|
-
// reloading during initialize().
|
|
3899
|
-
serverId: session.serverId,
|
|
3900
|
-
serverName: session.serverName,
|
|
3901
|
-
serverUrl: session.serverUrl,
|
|
3902
|
-
callbackUrl: session.callbackUrl,
|
|
3903
|
-
transportType: session.transportType,
|
|
3904
|
-
headers: session.headers
|
|
3905
|
-
});
|
|
3906
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3907
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3908
|
-
await client.connect();
|
|
3909
|
-
this.clients.set(sessionId, client);
|
|
3910
|
-
return client;
|
|
3911
4088
|
}
|
|
4089
|
+
// -----------------------------------------------------------------------
|
|
4090
|
+
// Tool Discovery & Policy
|
|
4091
|
+
// -----------------------------------------------------------------------
|
|
3912
4092
|
/**
|
|
3913
|
-
* Fetches
|
|
4093
|
+
* Fetches the complete tool list from the remote MCP server and emits a
|
|
4094
|
+
* `tools_discovered` SSE event with two lists:
|
|
3914
4095
|
*
|
|
3915
|
-
*
|
|
3916
|
-
* - `
|
|
3917
|
-
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
3918
|
-
* that blocked tools still appear as checkboxes in the dialog.
|
|
4096
|
+
* - `tools` — policy-filtered (what agents are allowed to call)
|
|
4097
|
+
* - `allTools` — complete, unfiltered (used by the management UI)
|
|
3919
4098
|
*
|
|
3920
|
-
* `fetchTools()`
|
|
3921
|
-
* `gateway.listTools()`
|
|
3922
|
-
* network round-trip is made regardless of how many callers follow.
|
|
4099
|
+
* Only a single network round-trip is made: `fetchTools()` populates the
|
|
4100
|
+
* in-memory cache, then `gateway.listTools()` reuses that cache.
|
|
3923
4101
|
*
|
|
3924
|
-
* @param sessionId - The session
|
|
3925
|
-
* @returns The session record and the
|
|
3926
|
-
* @throws {Error}
|
|
4102
|
+
* @param sessionId - The session to discover tools for.
|
|
4103
|
+
* @returns The session record and the filtered tool result.
|
|
4104
|
+
* @throws {Error} If the session does not exist in storage.
|
|
3927
4105
|
*/
|
|
3928
4106
|
async listPolicyFilteredTools(sessionId) {
|
|
3929
|
-
const session = await
|
|
3930
|
-
if (!session) {
|
|
3931
|
-
throw new Error("Session not found");
|
|
3932
|
-
}
|
|
4107
|
+
const session = await this.requireSession(sessionId);
|
|
3933
4108
|
const client = await this.getOrCreateClient(sessionId);
|
|
3934
4109
|
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
3935
4110
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
3936
4111
|
const result = await gateway.listTools();
|
|
3937
|
-
this.
|
|
4112
|
+
this.sendEvent({
|
|
3938
4113
|
type: "tools_discovered",
|
|
3939
4114
|
sessionId,
|
|
3940
4115
|
serverId: session.serverId ?? "unknown",
|
|
@@ -3946,254 +4121,254 @@ var SSEConnectionManager = class {
|
|
|
3946
4121
|
return { session, result };
|
|
3947
4122
|
}
|
|
3948
4123
|
/**
|
|
3949
|
-
* Returns the policy-filtered tool list for a session
|
|
3950
|
-
*
|
|
3951
|
-
*
|
|
4124
|
+
* Returns the policy-filtered tool list for a session.
|
|
4125
|
+
*
|
|
4126
|
+
* Delegates to {@link listPolicyFilteredTools}, which also emits a
|
|
4127
|
+
* `tools_discovered` SSE event to keep the client state current.
|
|
3952
4128
|
*/
|
|
3953
4129
|
async listTools(params) {
|
|
3954
|
-
const {
|
|
3955
|
-
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4130
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3956
4131
|
return { tools: result.tools };
|
|
3957
4132
|
}
|
|
3958
4133
|
/**
|
|
3959
|
-
*
|
|
4134
|
+
* Returns all raw tools annotated with their effective policy state
|
|
4135
|
+
* for display in the management UI.
|
|
3960
4136
|
*/
|
|
3961
4137
|
async getToolPolicy(params) {
|
|
3962
|
-
const
|
|
3963
|
-
const
|
|
3964
|
-
if (!session) {
|
|
3965
|
-
throw new Error("Session not found");
|
|
3966
|
-
}
|
|
3967
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4138
|
+
const session = await this.requireSession(params.sessionId);
|
|
4139
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3968
4140
|
const allTools = await client.fetchTools();
|
|
3969
|
-
const
|
|
4141
|
+
const policy = session.toolPolicy ?? {
|
|
3970
4142
|
mode: "all",
|
|
3971
4143
|
toolIds: [],
|
|
3972
4144
|
updatedAt: session.updatedAt ?? session.createdAt
|
|
3973
4145
|
};
|
|
3974
4146
|
const serverId = session.serverId ?? "unknown";
|
|
3975
|
-
const tools = allTools.tools.map((
|
|
3976
|
-
...
|
|
3977
|
-
toolId: createToolId(serverId,
|
|
3978
|
-
allowed: isToolAllowed(
|
|
4147
|
+
const tools = allTools.tools.map((t) => ({
|
|
4148
|
+
...t,
|
|
4149
|
+
toolId: createToolId(serverId, t.name),
|
|
4150
|
+
allowed: isToolAllowed(policy, t.name, session.serverId)
|
|
3979
4151
|
}));
|
|
3980
4152
|
return {
|
|
3981
|
-
toolPolicy,
|
|
4153
|
+
toolPolicy: policy,
|
|
3982
4154
|
tools,
|
|
3983
4155
|
toolCount: tools.length,
|
|
3984
|
-
allowedToolCount: tools.filter((
|
|
4156
|
+
allowedToolCount: tools.filter((t) => t.allowed).length
|
|
3985
4157
|
};
|
|
3986
4158
|
}
|
|
3987
4159
|
/**
|
|
3988
|
-
* Persists a new tool access policy
|
|
3989
|
-
*
|
|
3990
|
-
*
|
|
3991
|
-
* Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
|
|
3992
|
-
* so the management UI can immediately reflect the new checkbox states without
|
|
3993
|
-
* an additional round-trip to the server.
|
|
4160
|
+
* Persists a new tool access policy and broadcasts the updated filtered
|
|
4161
|
+
* tool list via a `tools_discovered` SSE event.
|
|
3994
4162
|
*
|
|
3995
|
-
* @
|
|
3996
|
-
*
|
|
4163
|
+
* @throws {Error} If the session does not exist or the policy references
|
|
4164
|
+
* tool IDs that don't match any known tool.
|
|
3997
4165
|
*/
|
|
3998
4166
|
async setToolPolicy(params) {
|
|
3999
|
-
const
|
|
4000
|
-
const
|
|
4001
|
-
if (!session) {
|
|
4002
|
-
throw new Error("Session not found");
|
|
4003
|
-
}
|
|
4004
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4167
|
+
const session = await this.requireSession(params.sessionId);
|
|
4168
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4005
4169
|
const allTools = await client.fetchTools();
|
|
4006
|
-
const
|
|
4007
|
-
validateToolPolicyAgainstTools(
|
|
4008
|
-
await sessions.update(this.userId, sessionId, { toolPolicy });
|
|
4009
|
-
const
|
|
4010
|
-
|
|
4170
|
+
const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
|
|
4171
|
+
validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
|
|
4172
|
+
await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
|
|
4173
|
+
const filtered = createToolPolicyGateway(
|
|
4174
|
+
this.userId,
|
|
4175
|
+
params.sessionId,
|
|
4176
|
+
client
|
|
4177
|
+
).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
|
|
4178
|
+
this.sendEvent({
|
|
4011
4179
|
type: "tools_discovered",
|
|
4012
|
-
sessionId,
|
|
4180
|
+
sessionId: params.sessionId,
|
|
4013
4181
|
serverId: session.serverId ?? "unknown",
|
|
4014
|
-
toolCount:
|
|
4015
|
-
tools:
|
|
4182
|
+
toolCount: filtered.length,
|
|
4183
|
+
tools: filtered,
|
|
4016
4184
|
allTools: allTools.tools,
|
|
4017
4185
|
timestamp: Date.now()
|
|
4018
4186
|
});
|
|
4019
|
-
return {
|
|
4020
|
-
success: true,
|
|
4021
|
-
toolPolicy,
|
|
4022
|
-
tools: filteredTools,
|
|
4023
|
-
toolCount: filteredTools.length
|
|
4024
|
-
};
|
|
4187
|
+
return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
|
|
4025
4188
|
}
|
|
4026
4189
|
/**
|
|
4027
|
-
*
|
|
4028
|
-
*
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
...result,
|
|
4037
|
-
_meta: {
|
|
4038
|
-
...meta,
|
|
4039
|
-
sessionId
|
|
4040
|
-
}
|
|
4041
|
-
};
|
|
4042
|
-
}
|
|
4043
|
-
/**
|
|
4044
|
-
* Restore and validate an existing session
|
|
4190
|
+
* Enables or disables a session for agent tool discovery.
|
|
4191
|
+
*
|
|
4192
|
+
* Disabled sessions retain their OAuth tokens and connection metadata
|
|
4193
|
+
* but are hidden from `MultiSessionClient.connect()` and blocked from
|
|
4194
|
+
* RPC tool access. Re-enabling does not require re-authentication.
|
|
4195
|
+
*
|
|
4196
|
+
* @param params - `{ sessionId, enabled: boolean }`
|
|
4197
|
+
* @returns `{ success: true }`
|
|
4198
|
+
* @throws {Error} If the session does not exist.
|
|
4045
4199
|
*/
|
|
4046
|
-
async
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
throw new Error("Session not found");
|
|
4051
|
-
}
|
|
4052
|
-
this.emitConnectionEvent({
|
|
4053
|
-
type: "state_changed",
|
|
4054
|
-
sessionId,
|
|
4055
|
-
serverId: session.serverId ?? "unknown",
|
|
4056
|
-
serverName: session.serverName ?? "Unknown",
|
|
4057
|
-
serverUrl: session.serverUrl,
|
|
4058
|
-
state: "VALIDATING",
|
|
4059
|
-
previousState: "DISCONNECTED",
|
|
4060
|
-
timestamp: Date.now()
|
|
4061
|
-
});
|
|
4062
|
-
try {
|
|
4063
|
-
const clientMetadata = await this.getResolvedClientMetadata();
|
|
4064
|
-
const client = new MCPClient({
|
|
4065
|
-
userId: this.userId,
|
|
4066
|
-
sessionId,
|
|
4067
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4068
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4069
|
-
// original transport/connection metadata instead of relying on lazy
|
|
4070
|
-
// reloading during initialize().
|
|
4071
|
-
serverId: session.serverId,
|
|
4072
|
-
serverName: session.serverName,
|
|
4073
|
-
serverUrl: session.serverUrl,
|
|
4074
|
-
callbackUrl: session.callbackUrl,
|
|
4075
|
-
transportType: session.transportType,
|
|
4076
|
-
headers: session.headers,
|
|
4077
|
-
...clientMetadata
|
|
4078
|
-
});
|
|
4079
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4080
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
4081
|
-
await client.connect();
|
|
4082
|
-
this.clients.set(sessionId, client);
|
|
4083
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4084
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4085
|
-
} catch (error) {
|
|
4086
|
-
this.emitConnectionEvent({
|
|
4087
|
-
type: "error",
|
|
4088
|
-
sessionId,
|
|
4089
|
-
serverId: session.serverId ?? "unknown",
|
|
4090
|
-
error: error instanceof Error ? error.message : "Validation failed",
|
|
4091
|
-
errorType: "validation",
|
|
4092
|
-
timestamp: Date.now()
|
|
4093
|
-
});
|
|
4094
|
-
throw error;
|
|
4095
|
-
}
|
|
4200
|
+
async updateSession(params) {
|
|
4201
|
+
await this.requireSession(params.sessionId);
|
|
4202
|
+
await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
|
|
4203
|
+
return { success: true };
|
|
4096
4204
|
}
|
|
4205
|
+
// -----------------------------------------------------------------------
|
|
4206
|
+
// Tool Execution
|
|
4207
|
+
// -----------------------------------------------------------------------
|
|
4097
4208
|
/**
|
|
4098
|
-
*
|
|
4209
|
+
* Proxies a tool invocation to the remote MCP server.
|
|
4210
|
+
*
|
|
4211
|
+
* Resolves (or creates) the transport for the given session, runs the call
|
|
4212
|
+
* through the tool-policy gateway, and injects `sessionId` into the result
|
|
4213
|
+
* metadata so the client can route the response without scanning all sessions.
|
|
4099
4214
|
*/
|
|
4100
|
-
async
|
|
4101
|
-
const
|
|
4102
|
-
const
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
}
|
|
4109
|
-
try {
|
|
4110
|
-
const client = new MCPClient({
|
|
4111
|
-
userId: this.userId,
|
|
4112
|
-
sessionId,
|
|
4113
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4114
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4115
|
-
// original connection metadata instead of relying on lazy
|
|
4116
|
-
// reloading during initialize().
|
|
4117
|
-
serverId: session.serverId,
|
|
4118
|
-
serverName: session.serverName,
|
|
4119
|
-
serverUrl: session.serverUrl,
|
|
4120
|
-
callbackUrl: session.callbackUrl,
|
|
4121
|
-
// NOTE: transportType is intentionally omitted here.
|
|
4122
|
-
// The session's stored transportType is a placeholder ('streamable-http')
|
|
4123
|
-
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
4124
|
-
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
4125
|
-
// Neon that only support SSE transport.
|
|
4126
|
-
headers: session.headers
|
|
4127
|
-
});
|
|
4128
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4129
|
-
await client.finishAuth(code, oauthState);
|
|
4130
|
-
this.clients.set(sessionId, client);
|
|
4131
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4132
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4133
|
-
} catch (error) {
|
|
4134
|
-
this.emitConnectionEvent({
|
|
4135
|
-
type: "error",
|
|
4136
|
-
sessionId,
|
|
4137
|
-
serverId: session.serverId ?? "unknown",
|
|
4138
|
-
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
4139
|
-
errorType: "auth",
|
|
4140
|
-
timestamp: Date.now()
|
|
4141
|
-
});
|
|
4142
|
-
throw error;
|
|
4143
|
-
}
|
|
4215
|
+
async callTool(params) {
|
|
4216
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4217
|
+
const result = await createToolPolicyGateway(
|
|
4218
|
+
this.userId,
|
|
4219
|
+
params.sessionId,
|
|
4220
|
+
client
|
|
4221
|
+
).callTool(params.toolName, params.toolArgs);
|
|
4222
|
+
return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
|
|
4144
4223
|
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4224
|
+
// -----------------------------------------------------------------------
|
|
4225
|
+
// Prompts
|
|
4226
|
+
// -----------------------------------------------------------------------
|
|
4227
|
+
/** Lists all prompts exposed by the remote MCP server. */
|
|
4148
4228
|
async listPrompts(params) {
|
|
4149
|
-
const
|
|
4150
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4229
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4151
4230
|
const result = await client.listPrompts();
|
|
4152
4231
|
return { prompts: result.prompts };
|
|
4153
4232
|
}
|
|
4154
|
-
/**
|
|
4155
|
-
* Get a specific prompt
|
|
4156
|
-
*/
|
|
4233
|
+
/** Retrieves a specific prompt by name with optional arguments. */
|
|
4157
4234
|
async getPrompt(params) {
|
|
4158
|
-
const
|
|
4159
|
-
|
|
4160
|
-
return await client.getPrompt(name, args);
|
|
4235
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4236
|
+
return client.getPrompt(params.name, params.args);
|
|
4161
4237
|
}
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4238
|
+
// -----------------------------------------------------------------------
|
|
4239
|
+
// Resources
|
|
4240
|
+
// -----------------------------------------------------------------------
|
|
4241
|
+
/** Lists all resources exposed by the remote MCP server. */
|
|
4165
4242
|
async listResources(params) {
|
|
4166
|
-
const
|
|
4167
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4243
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4168
4244
|
const result = await client.listResources();
|
|
4169
4245
|
return { resources: result.resources };
|
|
4170
4246
|
}
|
|
4247
|
+
/** Reads a specific resource identified by URI. */
|
|
4248
|
+
async readResource(params) {
|
|
4249
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4250
|
+
return client.readResource(params.uri);
|
|
4251
|
+
}
|
|
4252
|
+
// -----------------------------------------------------------------------
|
|
4253
|
+
// Internal Helpers
|
|
4254
|
+
// -----------------------------------------------------------------------
|
|
4255
|
+
/** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
|
|
4256
|
+
async getResolvedClientMetadata(request) {
|
|
4257
|
+
let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
|
|
4258
|
+
if (this.options.getClientMetadata) {
|
|
4259
|
+
metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
|
|
4260
|
+
}
|
|
4261
|
+
return metadata;
|
|
4262
|
+
}
|
|
4263
|
+
/** Ensures the given session exists in storage and throws otherwise. */
|
|
4264
|
+
async requireSession(sessionId) {
|
|
4265
|
+
const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
|
|
4266
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
4267
|
+
return session;
|
|
4268
|
+
}
|
|
4269
|
+
/** Finds an existing session matching the given serverId or serverUrl. */
|
|
4270
|
+
async findExistingSession(serverId, serverUrl) {
|
|
4271
|
+
const all = await sessions.list(this.userId);
|
|
4272
|
+
return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
|
|
4273
|
+
}
|
|
4274
|
+
/** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
|
|
4275
|
+
normalizeServerId(raw) {
|
|
4276
|
+
return raw && raw.length <= 12 ? raw : generateServerId();
|
|
4277
|
+
}
|
|
4171
4278
|
/**
|
|
4172
|
-
*
|
|
4279
|
+
* Returns the cached in-memory transport for `sessionId`, or creates one
|
|
4280
|
+
* from the persisted session row (with credentials) and connects it.
|
|
4173
4281
|
*/
|
|
4174
|
-
async
|
|
4175
|
-
const
|
|
4176
|
-
|
|
4177
|
-
|
|
4282
|
+
async getOrCreateClient(sessionId) {
|
|
4283
|
+
const existing = this.clients.get(sessionId);
|
|
4284
|
+
if (existing) return existing;
|
|
4285
|
+
const session = await this.requireSession(sessionId);
|
|
4286
|
+
if (session.enabled === false) {
|
|
4287
|
+
throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
|
|
4288
|
+
}
|
|
4289
|
+
const client = this.restoreClient(session);
|
|
4290
|
+
this.attachClientEvents(client);
|
|
4291
|
+
await client.connect();
|
|
4292
|
+
this.clients.set(sessionId, client);
|
|
4293
|
+
return client;
|
|
4178
4294
|
}
|
|
4179
4295
|
/**
|
|
4180
|
-
*
|
|
4296
|
+
* Builds an `MCPClient` from a stored session row.
|
|
4297
|
+
*
|
|
4298
|
+
* Extracts `clientId` and `clientSecret` from the session's credential
|
|
4299
|
+
* fields and passes `hasSession: true` + `cachedCredentials` so the client
|
|
4300
|
+
* can skip redundant existence checks and credential reads.
|
|
4181
4301
|
*/
|
|
4182
|
-
|
|
4183
|
-
|
|
4302
|
+
restoreClient(session) {
|
|
4303
|
+
const clientId = session.clientId ?? void 0;
|
|
4304
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
4305
|
+
return new MCPClient({
|
|
4306
|
+
userId: this.userId,
|
|
4307
|
+
sessionId: session.sessionId,
|
|
4308
|
+
serverId: session.serverId,
|
|
4309
|
+
serverName: session.serverName,
|
|
4310
|
+
serverUrl: session.serverUrl,
|
|
4311
|
+
callbackUrl: session.callbackUrl,
|
|
4312
|
+
transportType: session.transportType,
|
|
4313
|
+
headers: session.headers,
|
|
4314
|
+
clientId,
|
|
4315
|
+
clientSecret,
|
|
4316
|
+
hasSession: true,
|
|
4317
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
4318
|
+
sessionStore: this.observedStore
|
|
4319
|
+
});
|
|
4184
4320
|
}
|
|
4185
4321
|
/**
|
|
4186
|
-
*
|
|
4322
|
+
* Wires connection and observability events from the client into the
|
|
4323
|
+
* unified observability channel. Connection events go to `sendEvent`
|
|
4324
|
+
* (SSE stream); observability events go to `emitObs` (user callback
|
|
4325
|
+
* + SSE stream).
|
|
4187
4326
|
*/
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4327
|
+
attachClientEvents(client) {
|
|
4328
|
+
client.onConnectionEvent((e) => this.sendEvent(e));
|
|
4329
|
+
client.onObservabilityEvent((e) => this.sendEvent(e));
|
|
4330
|
+
}
|
|
4331
|
+
/**
|
|
4332
|
+
* Registers the client in the in-memory cache and attaches its event
|
|
4333
|
+
* listeners to the unified observability channel.
|
|
4334
|
+
*/
|
|
4335
|
+
cacheClient(sessionId, client) {
|
|
4336
|
+
this.attachClientEvents(client);
|
|
4337
|
+
this.clients.set(sessionId, client);
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Attempts a `client.connect()` and, on success, discovers tools.
|
|
4341
|
+
*
|
|
4342
|
+
* If the server responds with `UnauthorizedError`, the session is
|
|
4343
|
+
* treated as pending OAuth — a successful result is returned with
|
|
4344
|
+
* the sessionId so the browser client can redirect to the auth URL.
|
|
4345
|
+
*
|
|
4346
|
+
* For any other error an `error` connection event is emitted and the
|
|
4347
|
+
* client is removed from the in-memory cache before re-throwing.
|
|
4348
|
+
*/
|
|
4349
|
+
async connectAndDiscover(client, sessionId, serverId) {
|
|
4350
|
+
try {
|
|
4351
|
+
await client.connect();
|
|
4352
|
+
await this.listPolicyFilteredTools(sessionId);
|
|
4353
|
+
return { sessionId, success: true };
|
|
4354
|
+
} catch (error) {
|
|
4355
|
+
if (error instanceof UnauthorizedError) {
|
|
4356
|
+
this.clients.delete(sessionId);
|
|
4357
|
+
return { sessionId, success: true };
|
|
4358
|
+
}
|
|
4359
|
+
this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
|
|
4360
|
+
this.clients.delete(sessionId);
|
|
4361
|
+
throw error;
|
|
4192
4362
|
}
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
this.
|
|
4363
|
+
}
|
|
4364
|
+
/** Starts the periodic heartbeat timer. */
|
|
4365
|
+
startHeartbeat() {
|
|
4366
|
+
const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
4367
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4368
|
+
if (this.active) {
|
|
4369
|
+
this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
|
|
4370
|
+
}
|
|
4371
|
+
}, ms);
|
|
4197
4372
|
}
|
|
4198
4373
|
};
|
|
4199
4374
|
function createSSEHandler(options) {
|
|
@@ -4222,8 +4397,7 @@ function createSSEHandler(options) {
|
|
|
4222
4397
|
});
|
|
4223
4398
|
req.on("end", async () => {
|
|
4224
4399
|
try {
|
|
4225
|
-
|
|
4226
|
-
await manager.handleRequest(request);
|
|
4400
|
+
await manager.handleRequest(JSON.parse(body));
|
|
4227
4401
|
} catch {
|
|
4228
4402
|
}
|
|
4229
4403
|
});
|
|
@@ -4436,6 +4610,9 @@ var SSEClient = class {
|
|
|
4436
4610
|
async getToolPolicy(sessionId) {
|
|
4437
4611
|
return this.sendRequest("getToolPolicy", { sessionId });
|
|
4438
4612
|
}
|
|
4613
|
+
async updateSession(sessionId, enabled) {
|
|
4614
|
+
return this.sendRequest("updateSession", { sessionId, enabled });
|
|
4615
|
+
}
|
|
4439
4616
|
async listTools(sessionId) {
|
|
4440
4617
|
return this.sendRequest("listTools", { sessionId });
|
|
4441
4618
|
}
|
|
@@ -6367,6 +6544,8 @@ exports.createNextMcpHandler = createNextMcpHandler;
|
|
|
6367
6544
|
exports.createRegexSearchToolDefinition = createRegexSearchToolDefinition;
|
|
6368
6545
|
exports.createSSEHandler = createSSEHandler;
|
|
6369
6546
|
exports.createSearchToolDefinition = createSearchToolDefinition;
|
|
6547
|
+
exports.decryptObject = decryptObject;
|
|
6548
|
+
exports.encryptObject = encryptObject;
|
|
6370
6549
|
exports.executeMetaTool = executeMetaTool;
|
|
6371
6550
|
exports.findToolByName = findToolByName;
|
|
6372
6551
|
exports.getToolUiResourceUri = getToolUiResourceUri;
|
|
@@ -6380,5 +6559,6 @@ exports.onSessionMutation = onSessionMutation;
|
|
|
6380
6559
|
exports.resolveMetaToolProxy = resolveMetaToolProxy;
|
|
6381
6560
|
exports.sanitizeServerLabel = sanitizeServerLabel;
|
|
6382
6561
|
exports.sessions = sessions;
|
|
6562
|
+
exports.withDbObservability = withDbObservability;
|
|
6383
6563
|
//# sourceMappingURL=index.js.map
|
|
6384
6564
|
//# sourceMappingURL=index.js.map
|