@mcp-ts/sdk 2.5.2 → 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-B3GPsPsi.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-gNdSQTSz.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 -863
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -858
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CB4oDrQX.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-xj3iQIv6.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 +1054 -864
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1046 -859
- 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-CsKVXbQB.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-C-Mw1_BQ.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-6SmXege4.d.mts → types-DX71u9gR.d.mts} +11 -3
- package/dist/{types-6SmXege4.d.ts → types-DX71u9gR.d.ts} +11 -3
- 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 -629
- 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 +149 -29
- 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 -1
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,26 +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
|
-
|
|
2107
|
+
if (this._clientId) {
|
|
2108
|
+
return {
|
|
2109
|
+
client_id: this._clientId,
|
|
2110
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2111
|
+
};
|
|
2018
2112
|
}
|
|
2019
|
-
|
|
2020
|
-
|
|
2113
|
+
const data = await this.getCredentials();
|
|
2114
|
+
if (this._cachedTokens === void 0) {
|
|
2115
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2021
2116
|
}
|
|
2022
|
-
if (
|
|
2023
|
-
|
|
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
|
+
};
|
|
2024
2126
|
}
|
|
2025
|
-
return
|
|
2026
|
-
client_id: this._clientId,
|
|
2027
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2028
|
-
};
|
|
2127
|
+
return void 0;
|
|
2029
2128
|
}
|
|
2030
2129
|
/**
|
|
2031
2130
|
* Stores OAuth client information
|
|
@@ -2042,11 +2141,30 @@ var StorageOAuthClientProvider = class {
|
|
|
2042
2141
|
*/
|
|
2043
2142
|
async saveTokens(tokens) {
|
|
2044
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
|
+
}
|
|
2045
2161
|
}
|
|
2046
2162
|
get authUrl() {
|
|
2047
2163
|
return this._authUrl;
|
|
2048
2164
|
}
|
|
2049
2165
|
async state() {
|
|
2166
|
+
this._cachedCodeVerifier = void 0;
|
|
2167
|
+
this._hasCodeVerifier = false;
|
|
2050
2168
|
const nonce = nanoid.nanoid(32);
|
|
2051
2169
|
await this.patchCredentials({
|
|
2052
2170
|
oauthState: {
|
|
@@ -2067,7 +2185,7 @@ var StorageOAuthClientProvider = class {
|
|
|
2067
2185
|
if (parsed.sessionId !== this.sessionId) {
|
|
2068
2186
|
return { valid: false, error: "OAuth state mismatch" };
|
|
2069
2187
|
}
|
|
2070
|
-
const data = await
|
|
2188
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
2071
2189
|
if (!data) {
|
|
2072
2190
|
return { valid: false, error: "Session not found" };
|
|
2073
2191
|
}
|
|
@@ -2092,49 +2210,77 @@ var StorageOAuthClientProvider = class {
|
|
|
2092
2210
|
}
|
|
2093
2211
|
async redirectToAuthorization(authUrl) {
|
|
2094
2212
|
this._authUrl = authUrl.toString();
|
|
2095
|
-
|
|
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
|
+
}
|
|
2096
2226
|
if (this.onRedirectCallback) {
|
|
2097
2227
|
this.onRedirectCallback(authUrl.toString());
|
|
2098
2228
|
}
|
|
2099
2229
|
}
|
|
2100
2230
|
async invalidateCredentials(scope) {
|
|
2101
2231
|
if (scope === "all") {
|
|
2102
|
-
|
|
2232
|
+
this._cachedTokens = void 0;
|
|
2233
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
2103
2234
|
} else {
|
|
2104
2235
|
const updates = {};
|
|
2105
2236
|
if (scope === "client") {
|
|
2106
2237
|
updates.clientInformation = null;
|
|
2107
2238
|
updates.clientId = null;
|
|
2108
2239
|
} else if (scope === "tokens") {
|
|
2240
|
+
this._cachedTokens = void 0;
|
|
2109
2241
|
updates.tokens = null;
|
|
2110
2242
|
} else if (scope === "verifier") {
|
|
2243
|
+
this._cachedCodeVerifier = void 0;
|
|
2244
|
+
this._hasCodeVerifier = false;
|
|
2111
2245
|
updates.codeVerifier = null;
|
|
2112
2246
|
}
|
|
2113
2247
|
await this.patchCredentials(updates);
|
|
2114
2248
|
}
|
|
2115
2249
|
}
|
|
2116
2250
|
async saveCodeVerifier(verifier) {
|
|
2117
|
-
|
|
2118
|
-
if (data.codeVerifier) {
|
|
2251
|
+
if (this._hasCodeVerifier) {
|
|
2119
2252
|
return;
|
|
2120
2253
|
}
|
|
2121
|
-
|
|
2254
|
+
this._cachedCodeVerifier = verifier;
|
|
2255
|
+
this._hasCodeVerifier = true;
|
|
2122
2256
|
}
|
|
2123
2257
|
async codeVerifier() {
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2258
|
+
if (this._cachedCodeVerifier) {
|
|
2259
|
+
return this._cachedCodeVerifier;
|
|
2260
|
+
}
|
|
2261
|
+
const ctx = codeVerifierContext.getStore();
|
|
2262
|
+
if (ctx?.verifier) {
|
|
2263
|
+
return ctx.verifier;
|
|
2127
2264
|
}
|
|
2128
|
-
|
|
2129
|
-
|
|
2265
|
+
const data = await this.getCredentials();
|
|
2266
|
+
if (data.codeVerifier) {
|
|
2267
|
+
return data.codeVerifier;
|
|
2130
2268
|
}
|
|
2131
|
-
|
|
2269
|
+
throw new Error("No code verifier found");
|
|
2132
2270
|
}
|
|
2133
2271
|
async deleteCodeVerifier() {
|
|
2134
|
-
|
|
2272
|
+
this._cachedCodeVerifier = void 0;
|
|
2273
|
+
this._hasCodeVerifier = false;
|
|
2274
|
+
await this.patchCredentials({
|
|
2275
|
+
codeVerifier: null
|
|
2276
|
+
});
|
|
2135
2277
|
}
|
|
2136
2278
|
async tokens() {
|
|
2279
|
+
if (this._cachedTokens !== void 0) {
|
|
2280
|
+
return this._cachedTokens ?? void 0;
|
|
2281
|
+
}
|
|
2137
2282
|
const data = await this.getCredentials();
|
|
2283
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2138
2284
|
if (data.clientId && !this._clientId) {
|
|
2139
2285
|
this._clientId = data.clientId;
|
|
2140
2286
|
}
|
|
@@ -2297,23 +2443,10 @@ var MCPClient = class {
|
|
|
2297
2443
|
__publicField(this, "client");
|
|
2298
2444
|
__publicField(this, "oauthProvider", null);
|
|
2299
2445
|
__publicField(this, "transport", null);
|
|
2300
|
-
__publicField(this, "
|
|
2301
|
-
__publicField(this, "serverId");
|
|
2302
|
-
__publicField(this, "sessionId");
|
|
2303
|
-
__publicField(this, "serverName");
|
|
2304
|
-
__publicField(this, "transportType");
|
|
2305
|
-
__publicField(this, "serverUrl");
|
|
2306
|
-
__publicField(this, "callbackUrl");
|
|
2307
|
-
__publicField(this, "onRedirect");
|
|
2308
|
-
__publicField(this, "clientId");
|
|
2309
|
-
__publicField(this, "clientSecret");
|
|
2310
|
-
__publicField(this, "headers");
|
|
2311
|
-
/** OAuth Client Metadata */
|
|
2312
|
-
__publicField(this, "clientName");
|
|
2313
|
-
__publicField(this, "clientUri");
|
|
2314
|
-
__publicField(this, "logoUri");
|
|
2315
|
-
__publicField(this, "policyUri");
|
|
2446
|
+
__publicField(this, "config");
|
|
2316
2447
|
__publicField(this, "createdAt");
|
|
2448
|
+
__publicField(this, "_serverInfo");
|
|
2449
|
+
__publicField(this, "_store");
|
|
2317
2450
|
/** Event emitters for connection lifecycle */
|
|
2318
2451
|
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
2319
2452
|
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
@@ -2329,21 +2462,8 @@ var MCPClient = class {
|
|
|
2329
2462
|
* the memory when the client is no longer needed.
|
|
2330
2463
|
*/
|
|
2331
2464
|
__publicField(this, "cachedTools", null);
|
|
2332
|
-
this.
|
|
2333
|
-
this.
|
|
2334
|
-
this.callbackUrl = options.callbackUrl;
|
|
2335
|
-
this.onRedirect = options.onRedirect;
|
|
2336
|
-
this.userId = options.userId;
|
|
2337
|
-
this.serverId = options.serverId;
|
|
2338
|
-
this.sessionId = options.sessionId;
|
|
2339
|
-
this.transportType = options.transportType;
|
|
2340
|
-
this.clientId = options.clientId;
|
|
2341
|
-
this.clientSecret = options.clientSecret;
|
|
2342
|
-
this.headers = options.headers;
|
|
2343
|
-
this.clientName = options.clientName;
|
|
2344
|
-
this.clientUri = options.clientUri;
|
|
2345
|
-
this.logoUri = options.logoUri;
|
|
2346
|
-
this.policyUri = options.policyUri;
|
|
2465
|
+
this.config = { ...options };
|
|
2466
|
+
this._store = options.sessionStore ?? sessions;
|
|
2347
2467
|
this.client = new index_js.Client(
|
|
2348
2468
|
{
|
|
2349
2469
|
name: MCP_CLIENT_NAME,
|
|
@@ -2360,6 +2480,21 @@ var MCPClient = class {
|
|
|
2360
2480
|
}
|
|
2361
2481
|
);
|
|
2362
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
|
+
}
|
|
2363
2498
|
/**
|
|
2364
2499
|
* Emit a connection state change event
|
|
2365
2500
|
* @private
|
|
@@ -2367,13 +2502,13 @@ var MCPClient = class {
|
|
|
2367
2502
|
emitStateChange(newState) {
|
|
2368
2503
|
const previousState = this.currentState;
|
|
2369
2504
|
this.currentState = newState;
|
|
2370
|
-
if (!this.serverId) return;
|
|
2505
|
+
if (!this.config.serverId) return;
|
|
2371
2506
|
this._onConnectionEvent.fire({
|
|
2372
2507
|
type: "state_changed",
|
|
2373
|
-
sessionId: this.sessionId,
|
|
2374
|
-
serverId: this.serverId,
|
|
2375
|
-
serverName: this.serverName || this.serverId,
|
|
2376
|
-
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 || "",
|
|
2377
2512
|
createdAt: this.createdAt,
|
|
2378
2513
|
state: newState,
|
|
2379
2514
|
previousState,
|
|
@@ -2384,8 +2519,8 @@ var MCPClient = class {
|
|
|
2384
2519
|
level: "info",
|
|
2385
2520
|
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
2386
2521
|
displayMessage: `State changed to ${newState}`,
|
|
2387
|
-
sessionId: this.sessionId,
|
|
2388
|
-
serverId: this.serverId,
|
|
2522
|
+
sessionId: this.config.sessionId,
|
|
2523
|
+
serverId: this.config.serverId,
|
|
2389
2524
|
payload: { previousState, newState },
|
|
2390
2525
|
timestamp: Date.now(),
|
|
2391
2526
|
id: nanoid.nanoid()
|
|
@@ -2396,11 +2531,11 @@ var MCPClient = class {
|
|
|
2396
2531
|
* @private
|
|
2397
2532
|
*/
|
|
2398
2533
|
emitError(error, errorType = "unknown") {
|
|
2399
|
-
if (!this.serverId) return;
|
|
2534
|
+
if (!this.config.serverId) return;
|
|
2400
2535
|
this._onConnectionEvent.fire({
|
|
2401
2536
|
type: "error",
|
|
2402
|
-
sessionId: this.sessionId,
|
|
2403
|
-
serverId: this.serverId,
|
|
2537
|
+
sessionId: this.config.sessionId,
|
|
2538
|
+
serverId: this.config.serverId,
|
|
2404
2539
|
error,
|
|
2405
2540
|
errorType,
|
|
2406
2541
|
timestamp: Date.now()
|
|
@@ -2410,8 +2545,8 @@ var MCPClient = class {
|
|
|
2410
2545
|
level: "error",
|
|
2411
2546
|
message: error,
|
|
2412
2547
|
displayMessage: error,
|
|
2413
|
-
sessionId: this.sessionId,
|
|
2414
|
-
serverId: this.serverId,
|
|
2548
|
+
sessionId: this.config.sessionId,
|
|
2549
|
+
serverId: this.config.serverId,
|
|
2415
2550
|
payload: { errorType, error },
|
|
2416
2551
|
timestamp: Date.now(),
|
|
2417
2552
|
id: nanoid.nanoid()
|
|
@@ -2422,11 +2557,11 @@ var MCPClient = class {
|
|
|
2422
2557
|
* @private
|
|
2423
2558
|
*/
|
|
2424
2559
|
emitProgress(message) {
|
|
2425
|
-
if (!this.serverId) return;
|
|
2560
|
+
if (!this.config.serverId) return;
|
|
2426
2561
|
this._onConnectionEvent.fire({
|
|
2427
2562
|
type: "progress",
|
|
2428
|
-
sessionId: this.sessionId,
|
|
2429
|
-
serverId: this.serverId,
|
|
2563
|
+
sessionId: this.config.sessionId,
|
|
2564
|
+
serverId: this.config.serverId,
|
|
2430
2565
|
message,
|
|
2431
2566
|
timestamp: Date.now()
|
|
2432
2567
|
});
|
|
@@ -2444,14 +2579,14 @@ var MCPClient = class {
|
|
|
2444
2579
|
* @private
|
|
2445
2580
|
*/
|
|
2446
2581
|
getTransport(type) {
|
|
2447
|
-
if (!this.serverUrl) {
|
|
2582
|
+
if (!this.config.serverUrl) {
|
|
2448
2583
|
throw new Error("Server URL is required to create transport");
|
|
2449
2584
|
}
|
|
2450
|
-
const baseUrl = new URL(this.serverUrl);
|
|
2451
|
-
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");
|
|
2452
2587
|
const transportOptions = {
|
|
2453
2588
|
...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
|
|
2454
|
-
...this.headers && { requestInit: { headers: this.headers } },
|
|
2589
|
+
...this.config.headers && { requestInit: { headers: this.config.headers } },
|
|
2455
2590
|
/**
|
|
2456
2591
|
* Custom fetch implementation to handle connection timeouts.
|
|
2457
2592
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
@@ -2489,78 +2624,74 @@ var MCPClient = class {
|
|
|
2489
2624
|
* @private
|
|
2490
2625
|
*/
|
|
2491
2626
|
async ensureSession() {
|
|
2492
|
-
if (this.oauthProvider)
|
|
2493
|
-
return;
|
|
2494
|
-
}
|
|
2627
|
+
if (this.oauthProvider) return;
|
|
2495
2628
|
this.emitStateChange("INITIALIZING");
|
|
2496
2629
|
this.emitProgress("Loading session configuration...");
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
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}`);
|
|
2502
2634
|
}
|
|
2503
|
-
this.serverUrl = this.serverUrl ||
|
|
2504
|
-
this.callbackUrl = this.callbackUrl ||
|
|
2505
|
-
this.serverName = this.serverName ||
|
|
2506
|
-
this.serverId = this.serverId ||
|
|
2507
|
-
this.headers = this.headers ||
|
|
2508
|
-
this.createdAt =
|
|
2509
|
-
}
|
|
2510
|
-
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) {
|
|
2511
2643
|
throw new Error("Missing required connection metadata");
|
|
2512
2644
|
}
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
this.
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
this.
|
|
2531
|
-
|
|
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
|
+
});
|
|
2532
2667
|
}
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
}
|
|
2538
|
-
|
|
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) {
|
|
2539
2675
|
this.createdAt = Date.now();
|
|
2540
2676
|
const updatedAt = this.createdAt;
|
|
2541
2677
|
this._onObservabilityEvent.fire({
|
|
2542
2678
|
type: "mcp:client:session_created",
|
|
2543
2679
|
level: "info",
|
|
2544
|
-
message: `Creating pending session ${this.sessionId} for connection setup`,
|
|
2545
|
-
sessionId: this.sessionId,
|
|
2546
|
-
serverId: this.serverId,
|
|
2680
|
+
message: `Creating pending session ${this.config.sessionId} for connection setup`,
|
|
2681
|
+
sessionId: this.config.sessionId,
|
|
2682
|
+
serverId: this.config.serverId,
|
|
2547
2683
|
timestamp: Date.now(),
|
|
2548
2684
|
id: nanoid.nanoid()
|
|
2549
2685
|
});
|
|
2550
|
-
await
|
|
2551
|
-
|
|
2552
|
-
userId: this.userId,
|
|
2553
|
-
serverId: this.serverId,
|
|
2554
|
-
serverName: this.serverName,
|
|
2555
|
-
serverUrl: this.serverUrl,
|
|
2556
|
-
callbackUrl: this.callbackUrl,
|
|
2557
|
-
transportType: this.transportType || "streamable-http",
|
|
2558
|
-
headers: this.headers,
|
|
2559
|
-
createdAt: this.createdAt,
|
|
2686
|
+
await this._store.create({
|
|
2687
|
+
...this.session,
|
|
2560
2688
|
updatedAt,
|
|
2561
2689
|
status: "pending"
|
|
2562
2690
|
});
|
|
2563
2691
|
}
|
|
2692
|
+
if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
|
|
2693
|
+
await this.oauthProvider.initializeCredentials();
|
|
2694
|
+
}
|
|
2564
2695
|
}
|
|
2565
2696
|
/**
|
|
2566
2697
|
* Saves current session state to the session store
|
|
@@ -2568,34 +2699,14 @@ var MCPClient = class {
|
|
|
2568
2699
|
* @param status - Session lifecycle status used by storage cleanup
|
|
2569
2700
|
* @private
|
|
2570
2701
|
*/
|
|
2571
|
-
async saveSession(status = "active"
|
|
2572
|
-
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) {
|
|
2573
2704
|
return;
|
|
2574
2705
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
userId: this.userId,
|
|
2578
|
-
serverId: this.serverId,
|
|
2579
|
-
serverName: this.serverName,
|
|
2580
|
-
serverUrl: this.serverUrl,
|
|
2581
|
-
callbackUrl: this.callbackUrl,
|
|
2582
|
-
transportType: this.transportType || "streamable-http",
|
|
2583
|
-
headers: this.headers,
|
|
2584
|
-
createdAt: this.createdAt || Date.now(),
|
|
2585
|
-
updatedAt: Date.now(),
|
|
2706
|
+
await this._store.update(this.config.userId, this.config.sessionId, {
|
|
2707
|
+
...this.session,
|
|
2586
2708
|
status
|
|
2587
|
-
};
|
|
2588
|
-
if (status === "active") {
|
|
2589
|
-
sessionData.authUrl = null;
|
|
2590
|
-
}
|
|
2591
|
-
if (existingSession === void 0) {
|
|
2592
|
-
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2593
|
-
}
|
|
2594
|
-
if (existingSession) {
|
|
2595
|
-
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2596
|
-
} else {
|
|
2597
|
-
await sessions.create(sessionData);
|
|
2598
|
-
}
|
|
2709
|
+
});
|
|
2599
2710
|
}
|
|
2600
2711
|
/**
|
|
2601
2712
|
* Removes transient setup/auth sessions without masking the original error.
|
|
@@ -2603,7 +2714,7 @@ var MCPClient = class {
|
|
|
2603
2714
|
*/
|
|
2604
2715
|
async deleteTransientSession() {
|
|
2605
2716
|
try {
|
|
2606
|
-
await
|
|
2717
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2607
2718
|
} catch {
|
|
2608
2719
|
}
|
|
2609
2720
|
}
|
|
@@ -2613,7 +2724,7 @@ var MCPClient = class {
|
|
|
2613
2724
|
* @private
|
|
2614
2725
|
*/
|
|
2615
2726
|
async tryConnect() {
|
|
2616
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2727
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2617
2728
|
let lastError;
|
|
2618
2729
|
for (const currentType of transportsToTry) {
|
|
2619
2730
|
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
@@ -2621,6 +2732,7 @@ var MCPClient = class {
|
|
|
2621
2732
|
const transport = this.getTransport(currentType);
|
|
2622
2733
|
this.transport = transport;
|
|
2623
2734
|
await this.client.connect(transport);
|
|
2735
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2624
2736
|
return { transportType: currentType };
|
|
2625
2737
|
} catch (error) {
|
|
2626
2738
|
lastError = error;
|
|
@@ -2636,8 +2748,8 @@ var MCPClient = class {
|
|
|
2636
2748
|
this._onObservabilityEvent.fire({
|
|
2637
2749
|
level: "warn",
|
|
2638
2750
|
message: `Transport ${currentType} failed, falling back`,
|
|
2639
|
-
sessionId: this.sessionId,
|
|
2640
|
-
serverId: this.serverId,
|
|
2751
|
+
sessionId: this.config.sessionId,
|
|
2752
|
+
serverId: this.config.serverId,
|
|
2641
2753
|
metadata: {
|
|
2642
2754
|
failedTransport: currentType,
|
|
2643
2755
|
error: errorMessage
|
|
@@ -2680,15 +2792,15 @@ var MCPClient = class {
|
|
|
2680
2792
|
try {
|
|
2681
2793
|
this.emitStateChange("CONNECTING");
|
|
2682
2794
|
const { transportType } = await this.tryConnect();
|
|
2683
|
-
this.transportType = transportType;
|
|
2795
|
+
this.config.transportType = transportType;
|
|
2684
2796
|
this.emitStateChange("CONNECTED");
|
|
2685
2797
|
this.emitProgress("Connected successfully");
|
|
2686
2798
|
this._onObservabilityEvent.fire({
|
|
2687
2799
|
type: "mcp:client:session_saved",
|
|
2688
2800
|
level: "info",
|
|
2689
|
-
message: `Saving active session ${this.sessionId} (connect success)`,
|
|
2690
|
-
sessionId: this.sessionId,
|
|
2691
|
-
serverId: this.serverId,
|
|
2801
|
+
message: `Saving active session ${this.config.sessionId} (connect success)`,
|
|
2802
|
+
sessionId: this.config.sessionId,
|
|
2803
|
+
serverId: this.config.serverId,
|
|
2692
2804
|
timestamp: Date.now(),
|
|
2693
2805
|
id: nanoid.nanoid()
|
|
2694
2806
|
});
|
|
@@ -2711,23 +2823,23 @@ var MCPClient = class {
|
|
|
2711
2823
|
this._onObservabilityEvent.fire({
|
|
2712
2824
|
type: "mcp:client:session_saved",
|
|
2713
2825
|
level: "info",
|
|
2714
|
-
message: `Saving pending OAuth session ${this.sessionId}`,
|
|
2715
|
-
sessionId: this.sessionId,
|
|
2716
|
-
serverId: this.serverId,
|
|
2826
|
+
message: `Saving pending OAuth session ${this.config.sessionId}`,
|
|
2827
|
+
sessionId: this.config.sessionId,
|
|
2828
|
+
serverId: this.config.serverId,
|
|
2717
2829
|
timestamp: Date.now(),
|
|
2718
2830
|
id: nanoid.nanoid()
|
|
2719
2831
|
});
|
|
2720
2832
|
await this.saveSession("pending");
|
|
2721
|
-
if (this.serverId) {
|
|
2833
|
+
if (this.config.serverId) {
|
|
2722
2834
|
this._onConnectionEvent.fire({
|
|
2723
2835
|
type: "auth_required",
|
|
2724
|
-
sessionId: this.sessionId,
|
|
2725
|
-
serverId: this.serverId,
|
|
2836
|
+
sessionId: this.config.sessionId,
|
|
2837
|
+
serverId: this.config.serverId,
|
|
2726
2838
|
authUrl,
|
|
2727
2839
|
timestamp: Date.now()
|
|
2728
2840
|
});
|
|
2729
|
-
if (authUrl && this.onRedirect) {
|
|
2730
|
-
this.onRedirect(authUrl);
|
|
2841
|
+
if (authUrl && this.config.onRedirect) {
|
|
2842
|
+
this.config.onRedirect(authUrl);
|
|
2731
2843
|
}
|
|
2732
2844
|
}
|
|
2733
2845
|
throw new UnauthorizedError("OAuth authorization required");
|
|
@@ -2736,9 +2848,9 @@ var MCPClient = class {
|
|
|
2736
2848
|
this.emitError(errorMessage, "connection");
|
|
2737
2849
|
this.emitStateChange("FAILED");
|
|
2738
2850
|
try {
|
|
2739
|
-
const existingSession = await
|
|
2851
|
+
const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2740
2852
|
if (!existingSession || existingSession.status !== "active") {
|
|
2741
|
-
await
|
|
2853
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2742
2854
|
}
|
|
2743
2855
|
} catch {
|
|
2744
2856
|
}
|
|
@@ -2763,16 +2875,16 @@ var MCPClient = class {
|
|
|
2763
2875
|
throw new Error(error);
|
|
2764
2876
|
}
|
|
2765
2877
|
if (state) {
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
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");
|
|
2770
2883
|
this.emitStateChange("FAILED");
|
|
2771
|
-
throw
|
|
2884
|
+
throw error;
|
|
2772
2885
|
}
|
|
2773
|
-
await this.oauthProvider.consumeState(state);
|
|
2774
2886
|
}
|
|
2775
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2887
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2776
2888
|
let lastError;
|
|
2777
2889
|
let tokensExchanged = false;
|
|
2778
2890
|
let authenticatedStateEmitted = false;
|
|
@@ -2799,14 +2911,15 @@ var MCPClient = class {
|
|
|
2799
2911
|
}
|
|
2800
2912
|
}
|
|
2801
2913
|
await this.client.connect(this.transport);
|
|
2802
|
-
this.
|
|
2914
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2915
|
+
this.config.transportType = currentType;
|
|
2803
2916
|
this.emitStateChange("CONNECTED");
|
|
2804
2917
|
this._onObservabilityEvent.fire({
|
|
2805
2918
|
type: "mcp:client:session_saved",
|
|
2806
2919
|
level: "info",
|
|
2807
|
-
message: `Saving active session ${this.sessionId} (OAuth complete)`,
|
|
2808
|
-
sessionId: this.sessionId,
|
|
2809
|
-
serverId: this.serverId,
|
|
2920
|
+
message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
|
|
2921
|
+
sessionId: this.config.sessionId,
|
|
2922
|
+
serverId: this.config.serverId,
|
|
2810
2923
|
timestamp: Date.now(),
|
|
2811
2924
|
id: nanoid.nanoid()
|
|
2812
2925
|
});
|
|
@@ -2879,11 +2992,11 @@ var MCPClient = class {
|
|
|
2879
2992
|
this.emitStateChange("DISCOVERING");
|
|
2880
2993
|
try {
|
|
2881
2994
|
const result = await this.fetchTools();
|
|
2882
|
-
if (this.serverId) {
|
|
2995
|
+
if (this.config.serverId) {
|
|
2883
2996
|
this._onConnectionEvent.fire({
|
|
2884
2997
|
type: "tools_discovered",
|
|
2885
|
-
sessionId: this.sessionId,
|
|
2886
|
-
serverId: this.serverId,
|
|
2998
|
+
sessionId: this.config.sessionId,
|
|
2999
|
+
serverId: this.config.serverId,
|
|
2887
3000
|
toolCount: result.tools.length,
|
|
2888
3001
|
tools: result.tools,
|
|
2889
3002
|
timestamp: Date.now()
|
|
@@ -2923,8 +3036,8 @@ var MCPClient = class {
|
|
|
2923
3036
|
level: "info",
|
|
2924
3037
|
message: `Tool ${toolName} called successfully`,
|
|
2925
3038
|
displayMessage: `Called tool ${toolName}`,
|
|
2926
|
-
sessionId: this.sessionId,
|
|
2927
|
-
serverId: this.serverId,
|
|
3039
|
+
sessionId: this.config.sessionId,
|
|
3040
|
+
serverId: this.config.serverId,
|
|
2928
3041
|
payload: {
|
|
2929
3042
|
toolName,
|
|
2930
3043
|
args: toolArgs
|
|
@@ -2940,8 +3053,8 @@ var MCPClient = class {
|
|
|
2940
3053
|
level: "error",
|
|
2941
3054
|
message: errorMessage,
|
|
2942
3055
|
displayMessage: `Failed to call tool ${toolName}`,
|
|
2943
|
-
sessionId: this.sessionId,
|
|
2944
|
-
serverId: this.serverId,
|
|
3056
|
+
sessionId: this.config.sessionId,
|
|
3057
|
+
serverId: this.config.serverId,
|
|
2945
3058
|
payload: {
|
|
2946
3059
|
errorType: "tool_execution",
|
|
2947
3060
|
error: errorMessage,
|
|
@@ -3089,8 +3202,8 @@ var MCPClient = class {
|
|
|
3089
3202
|
type: "mcp:client:error",
|
|
3090
3203
|
level: "warn",
|
|
3091
3204
|
message: "Initialization failed during clearSession",
|
|
3092
|
-
sessionId: this.sessionId,
|
|
3093
|
-
serverId: this.serverId,
|
|
3205
|
+
sessionId: this.config.sessionId,
|
|
3206
|
+
serverId: this.config.serverId,
|
|
3094
3207
|
payload: { error: String(error) },
|
|
3095
3208
|
timestamp: Date.now(),
|
|
3096
3209
|
id: nanoid.nanoid()
|
|
@@ -3099,7 +3212,7 @@ var MCPClient = class {
|
|
|
3099
3212
|
if (this.oauthProvider) {
|
|
3100
3213
|
await this.oauthProvider.invalidateCredentials("all");
|
|
3101
3214
|
}
|
|
3102
|
-
await
|
|
3215
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
3103
3216
|
await this.disconnect();
|
|
3104
3217
|
}
|
|
3105
3218
|
/**
|
|
@@ -3133,19 +3246,19 @@ var MCPClient = class {
|
|
|
3133
3246
|
}
|
|
3134
3247
|
this.oauthProvider = null;
|
|
3135
3248
|
this.transport = null;
|
|
3136
|
-
if (this.serverId) {
|
|
3249
|
+
if (this.config.serverId) {
|
|
3137
3250
|
this._onConnectionEvent.fire({
|
|
3138
3251
|
type: "disconnected",
|
|
3139
|
-
sessionId: this.sessionId,
|
|
3140
|
-
serverId: this.serverId,
|
|
3252
|
+
sessionId: this.config.sessionId,
|
|
3253
|
+
serverId: this.config.serverId,
|
|
3141
3254
|
timestamp: Date.now()
|
|
3142
3255
|
});
|
|
3143
3256
|
this._onObservabilityEvent.fire({
|
|
3144
3257
|
type: "mcp:client:disconnect",
|
|
3145
3258
|
level: "info",
|
|
3146
|
-
message: `Disconnected from ${this.serverId}`,
|
|
3147
|
-
sessionId: this.sessionId,
|
|
3148
|
-
serverId: this.serverId,
|
|
3259
|
+
message: `Disconnected from ${this.config.serverId}`,
|
|
3260
|
+
sessionId: this.config.sessionId,
|
|
3261
|
+
serverId: this.config.serverId,
|
|
3149
3262
|
payload: {},
|
|
3150
3263
|
timestamp: Date.now(),
|
|
3151
3264
|
id: nanoid.nanoid()
|
|
@@ -3170,42 +3283,52 @@ var MCPClient = class {
|
|
|
3170
3283
|
* @returns Server URL or empty string if not set
|
|
3171
3284
|
*/
|
|
3172
3285
|
getServerUrl() {
|
|
3173
|
-
return this.serverUrl || "";
|
|
3286
|
+
return this.config.serverUrl || "";
|
|
3174
3287
|
}
|
|
3175
3288
|
/**
|
|
3176
3289
|
* Gets the OAuth callback URL
|
|
3177
3290
|
* @returns Callback URL or empty string if not set
|
|
3178
3291
|
*/
|
|
3179
3292
|
getCallbackUrl() {
|
|
3180
|
-
return this.callbackUrl || "";
|
|
3293
|
+
return this.config.callbackUrl || "";
|
|
3181
3294
|
}
|
|
3182
3295
|
/**
|
|
3183
3296
|
* Gets the transport type being used
|
|
3184
3297
|
* @returns Transport type (defaults to 'streamable-http')
|
|
3185
3298
|
*/
|
|
3186
3299
|
getTransportType() {
|
|
3187
|
-
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;
|
|
3188
3309
|
}
|
|
3189
3310
|
/**
|
|
3190
|
-
* 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.
|
|
3191
3314
|
* @returns Server name or undefined
|
|
3192
3315
|
*/
|
|
3193
3316
|
getServerName() {
|
|
3194
|
-
return this.serverName;
|
|
3317
|
+
return this.config.serverName;
|
|
3195
3318
|
}
|
|
3196
3319
|
/**
|
|
3197
3320
|
* Gets the server ID
|
|
3198
3321
|
* @returns Server ID or undefined
|
|
3199
3322
|
*/
|
|
3200
3323
|
getServerId() {
|
|
3201
|
-
return this.serverId;
|
|
3324
|
+
return this.config.serverId;
|
|
3202
3325
|
}
|
|
3203
3326
|
/**
|
|
3204
3327
|
* Gets the session ID
|
|
3205
3328
|
* @returns Session ID
|
|
3206
3329
|
*/
|
|
3207
3330
|
getSessionId() {
|
|
3208
|
-
return this.sessionId;
|
|
3331
|
+
return this.config.sessionId;
|
|
3209
3332
|
}
|
|
3210
3333
|
};
|
|
3211
3334
|
|
|
@@ -3232,6 +3355,14 @@ var ToolPolicyGateway = class {
|
|
|
3232
3355
|
getServerId() {
|
|
3233
3356
|
return this.client.getServerId?.();
|
|
3234
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
|
+
}
|
|
3235
3366
|
/**
|
|
3236
3367
|
* Returns the human-readable server name from the underlying client, if available.
|
|
3237
3368
|
*/
|
|
@@ -3360,6 +3491,7 @@ var MultiSessionClient = class {
|
|
|
3360
3491
|
__publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
|
|
3361
3492
|
__publicField(this, "userId");
|
|
3362
3493
|
__publicField(this, "options");
|
|
3494
|
+
__publicField(this, "_store");
|
|
3363
3495
|
this.userId = userId;
|
|
3364
3496
|
this.options = {
|
|
3365
3497
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
@@ -3367,6 +3499,7 @@ var MultiSessionClient = class {
|
|
|
3367
3499
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
3368
3500
|
...options
|
|
3369
3501
|
};
|
|
3502
|
+
this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
|
|
3370
3503
|
}
|
|
3371
3504
|
// -----------------------------------------------------------------------
|
|
3372
3505
|
// Public API
|
|
@@ -3450,9 +3583,9 @@ var MultiSessionClient = class {
|
|
|
3450
3583
|
* to querying the storage backend via `sessions.list(userId)`.
|
|
3451
3584
|
*/
|
|
3452
3585
|
async fetchActiveSessions() {
|
|
3453
|
-
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);
|
|
3454
3587
|
return sessionList.filter(
|
|
3455
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
3588
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
|
|
3456
3589
|
);
|
|
3457
3590
|
}
|
|
3458
3591
|
/**
|
|
@@ -3525,8 +3658,13 @@ var MultiSessionClient = class {
|
|
|
3525
3658
|
callbackUrl: session.callbackUrl,
|
|
3526
3659
|
serverName: session.serverName,
|
|
3527
3660
|
transportType: session.transportType,
|
|
3528
|
-
headers: session.headers
|
|
3661
|
+
headers: session.headers,
|
|
3662
|
+
hasSession: true,
|
|
3663
|
+
sessionStore: this._store
|
|
3529
3664
|
});
|
|
3665
|
+
if (this.options.onObservabilityEvent) {
|
|
3666
|
+
client.onObservabilityEvent(this.options.onObservabilityEvent);
|
|
3667
|
+
}
|
|
3530
3668
|
const timeoutMs = this.options.timeout;
|
|
3531
3669
|
let timeoutTimer;
|
|
3532
3670
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3585,130 +3723,188 @@ function isConnectionEvent(event) {
|
|
|
3585
3723
|
}
|
|
3586
3724
|
|
|
3587
3725
|
// src/server/handlers/sse-handler.ts
|
|
3588
|
-
var
|
|
3726
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
3589
3727
|
function normalizeHeaders(headers) {
|
|
3590
3728
|
if (!headers || typeof headers !== "object") return void 0;
|
|
3591
|
-
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);
|
|
3592
3730
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3593
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
|
+
}
|
|
3594
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
|
+
*/
|
|
3595
3762
|
constructor(options, sendEvent) {
|
|
3596
3763
|
this.options = options;
|
|
3597
3764
|
this.sendEvent = sendEvent;
|
|
3765
|
+
// -----------------------------------------------------------------------
|
|
3766
|
+
// State
|
|
3767
|
+
// -----------------------------------------------------------------------
|
|
3598
3768
|
__publicField(this, "userId");
|
|
3769
|
+
/** Active MCP transports keyed by sessionId. */
|
|
3599
3770
|
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
3771
|
+
/** Instrumented session store — always wraps `sessions` with DB observability. */
|
|
3772
|
+
__publicField(this, "observedStore");
|
|
3600
3773
|
__publicField(this, "heartbeatTimer");
|
|
3601
|
-
__publicField(this, "
|
|
3774
|
+
__publicField(this, "active", true);
|
|
3602
3775
|
this.userId = options.userId;
|
|
3776
|
+
this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
|
|
3603
3777
|
this.startHeartbeat();
|
|
3604
3778
|
}
|
|
3779
|
+
// -----------------------------------------------------------------------
|
|
3780
|
+
// Public API
|
|
3781
|
+
// -----------------------------------------------------------------------
|
|
3605
3782
|
/**
|
|
3606
|
-
*
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
}
|
|
3617
|
-
return metadata;
|
|
3618
|
-
}
|
|
3619
|
-
/**
|
|
3620
|
-
* Start heartbeat to keep connection alive
|
|
3621
|
-
*/
|
|
3622
|
-
startHeartbeat() {
|
|
3623
|
-
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
3624
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3625
|
-
if (this.isActive) {
|
|
3626
|
-
this.sendEvent({
|
|
3627
|
-
level: "debug",
|
|
3628
|
-
message: "heartbeat",
|
|
3629
|
-
timestamp: Date.now()
|
|
3630
|
-
});
|
|
3631
|
-
}
|
|
3632
|
-
}, interval);
|
|
3633
|
-
}
|
|
3634
|
-
/**
|
|
3635
|
-
* Handle incoming RPC requests
|
|
3636
|
-
* 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).
|
|
3637
3793
|
*/
|
|
3638
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
|
+
});
|
|
3639
3805
|
try {
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
break;
|
|
3651
|
-
case "disconnect":
|
|
3652
|
-
result = await this.disconnect(request.params);
|
|
3653
|
-
break;
|
|
3654
|
-
case "listTools":
|
|
3655
|
-
result = await this.listTools(request.params);
|
|
3656
|
-
break;
|
|
3657
|
-
case "setToolPolicy":
|
|
3658
|
-
result = await this.setToolPolicy(request.params);
|
|
3659
|
-
break;
|
|
3660
|
-
case "getToolPolicy":
|
|
3661
|
-
result = await this.getToolPolicy(request.params);
|
|
3662
|
-
break;
|
|
3663
|
-
case "callTool":
|
|
3664
|
-
result = await this.callTool(request.params);
|
|
3665
|
-
break;
|
|
3666
|
-
case "getSession":
|
|
3667
|
-
result = await this.getSession(request.params);
|
|
3668
|
-
break;
|
|
3669
|
-
case "finishAuth":
|
|
3670
|
-
result = await this.finishAuth(request.params);
|
|
3671
|
-
break;
|
|
3672
|
-
case "listPrompts":
|
|
3673
|
-
result = await this.listPrompts(request.params);
|
|
3674
|
-
break;
|
|
3675
|
-
case "getPrompt":
|
|
3676
|
-
result = await this.getPrompt(request.params);
|
|
3677
|
-
break;
|
|
3678
|
-
case "listResources":
|
|
3679
|
-
result = await this.listResources(request.params);
|
|
3680
|
-
break;
|
|
3681
|
-
case "readResource":
|
|
3682
|
-
result = await this.readResource(request.params);
|
|
3683
|
-
break;
|
|
3684
|
-
default:
|
|
3685
|
-
throw new Error(`Unknown method: ${request.method}`);
|
|
3686
|
-
}
|
|
3687
|
-
const response = {
|
|
3688
|
-
id: request.id,
|
|
3689
|
-
result
|
|
3690
|
-
};
|
|
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 };
|
|
3691
3816
|
this.sendEvent(response);
|
|
3692
3817
|
return response;
|
|
3693
3818
|
} catch (error) {
|
|
3694
|
-
|
|
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 = {
|
|
3695
3828
|
id: request.id,
|
|
3696
3829
|
error: {
|
|
3697
3830
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
3698
3831
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
3699
3832
|
}
|
|
3700
3833
|
};
|
|
3701
|
-
this.sendEvent(
|
|
3702
|
-
return
|
|
3834
|
+
this.sendEvent(response);
|
|
3835
|
+
return response;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
/**
|
|
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}`);
|
|
3703
3894
|
}
|
|
3704
3895
|
}
|
|
3896
|
+
// -----------------------------------------------------------------------
|
|
3897
|
+
// Session Query
|
|
3898
|
+
// -----------------------------------------------------------------------
|
|
3705
3899
|
/**
|
|
3706
|
-
*
|
|
3900
|
+
* Lists all sessions owned by the current user.
|
|
3901
|
+
*
|
|
3902
|
+
* Returns a lightweight view — session metadata only, no credential fields.
|
|
3707
3903
|
*/
|
|
3708
3904
|
async listSessions() {
|
|
3709
|
-
const
|
|
3905
|
+
const all = await sessions.list(this.userId);
|
|
3710
3906
|
return {
|
|
3711
|
-
sessions:
|
|
3907
|
+
sessions: all.map((s) => ({
|
|
3712
3908
|
sessionId: s.sessionId,
|
|
3713
3909
|
serverId: s.serverId,
|
|
3714
3910
|
serverName: s.serverName,
|
|
@@ -3717,211 +3913,203 @@ var SSEConnectionManager = class {
|
|
|
3717
3913
|
createdAt: s.createdAt,
|
|
3718
3914
|
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3719
3915
|
status: s.status ?? "pending",
|
|
3720
|
-
toolPolicy: s.toolPolicy
|
|
3916
|
+
toolPolicy: s.toolPolicy,
|
|
3917
|
+
enabled: s.enabled ?? true
|
|
3721
3918
|
}))
|
|
3722
3919
|
};
|
|
3723
3920
|
}
|
|
3724
3921
|
/**
|
|
3725
|
-
*
|
|
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.
|
|
3726
3927
|
*/
|
|
3727
|
-
async
|
|
3728
|
-
const
|
|
3729
|
-
|
|
3730
|
-
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
|
|
3731
|
-
const existingSessions = await sessions.list(this.userId);
|
|
3732
|
-
const duplicate = existingSessions.find(
|
|
3733
|
-
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
3734
|
-
);
|
|
3735
|
-
if (duplicate) {
|
|
3736
|
-
if (duplicate.status === "pending") {
|
|
3737
|
-
await this.getSession({ sessionId: duplicate.sessionId });
|
|
3738
|
-
return {
|
|
3739
|
-
sessionId: duplicate.sessionId,
|
|
3740
|
-
success: true
|
|
3741
|
-
};
|
|
3742
|
-
}
|
|
3743
|
-
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
3744
|
-
}
|
|
3745
|
-
const sessionId = await sessions.generateSessionId();
|
|
3928
|
+
async getSession(params) {
|
|
3929
|
+
const session = await this.requireSession(params.sessionId);
|
|
3930
|
+
this.sendEvent(validatingStateEvent(params.sessionId, session));
|
|
3746
3931
|
try {
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
userId: this.userId,
|
|
3750
|
-
sessionId,
|
|
3751
|
-
serverId,
|
|
3752
|
-
serverName,
|
|
3753
|
-
serverUrl,
|
|
3754
|
-
callbackUrl,
|
|
3755
|
-
transportType,
|
|
3756
|
-
headers,
|
|
3757
|
-
...clientMetadata
|
|
3758
|
-
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
3759
|
-
});
|
|
3760
|
-
this.clients.set(sessionId, client);
|
|
3761
|
-
client.onConnectionEvent((event) => {
|
|
3762
|
-
this.emitConnectionEvent(event);
|
|
3763
|
-
});
|
|
3764
|
-
client.onObservabilityEvent((event) => {
|
|
3765
|
-
this.sendEvent(event);
|
|
3766
|
-
});
|
|
3932
|
+
const client = this.restoreClient(session);
|
|
3933
|
+
this.attachClientEvents(client);
|
|
3767
3934
|
await client.connect();
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
success: true
|
|
3772
|
-
};
|
|
3935
|
+
this.clients.set(params.sessionId, client);
|
|
3936
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3937
|
+
return { success: true, toolCount: result.tools.length };
|
|
3773
3938
|
} catch (error) {
|
|
3774
|
-
|
|
3775
|
-
this.clients.delete(sessionId);
|
|
3776
|
-
return {
|
|
3777
|
-
sessionId,
|
|
3778
|
-
success: true
|
|
3779
|
-
};
|
|
3780
|
-
}
|
|
3781
|
-
this.emitConnectionEvent({
|
|
3782
|
-
type: "error",
|
|
3783
|
-
sessionId,
|
|
3784
|
-
serverId,
|
|
3785
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3786
|
-
errorType: "connection",
|
|
3787
|
-
timestamp: Date.now()
|
|
3788
|
-
});
|
|
3789
|
-
this.clients.delete(sessionId);
|
|
3939
|
+
this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
|
|
3790
3940
|
throw error;
|
|
3791
3941
|
}
|
|
3792
3942
|
}
|
|
3943
|
+
// -----------------------------------------------------------------------
|
|
3944
|
+
// Connection Lifecycle
|
|
3945
|
+
// -----------------------------------------------------------------------
|
|
3793
3946
|
/**
|
|
3794
|
-
*
|
|
3795
|
-
*
|
|
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).
|
|
3796
3955
|
*/
|
|
3797
|
-
async
|
|
3798
|
-
const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType } = params;
|
|
3956
|
+
async connect(params) {
|
|
3799
3957
|
const headers = normalizeHeaders(params.headers);
|
|
3800
|
-
const serverId =
|
|
3801
|
-
const
|
|
3802
|
-
|
|
3803
|
-
(
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
if (existingClient) {
|
|
3809
|
-
await existingClient.disconnect();
|
|
3810
|
-
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
|
+
}));
|
|
3811
3966
|
}
|
|
3967
|
+
throw new Error(
|
|
3968
|
+
`Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
|
|
3969
|
+
);
|
|
3812
3970
|
}
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
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);
|
|
3840
4003
|
}
|
|
3841
|
-
this.emitConnectionEvent({
|
|
3842
|
-
type: "error",
|
|
3843
|
-
sessionId,
|
|
3844
|
-
serverId,
|
|
3845
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3846
|
-
errorType: "connection",
|
|
3847
|
-
timestamp: Date.now()
|
|
3848
|
-
});
|
|
3849
|
-
this.clients.delete(sessionId);
|
|
3850
|
-
throw error;
|
|
3851
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);
|
|
3852
4020
|
}
|
|
3853
4021
|
/**
|
|
3854
|
-
*
|
|
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.
|
|
3855
4028
|
*/
|
|
3856
4029
|
async disconnect(params) {
|
|
3857
|
-
const
|
|
3858
|
-
const client = this.clients.get(sessionId);
|
|
4030
|
+
const client = this.clients.get(params.sessionId);
|
|
3859
4031
|
if (client) {
|
|
3860
4032
|
await client.clearSession();
|
|
3861
|
-
this.clients.delete(sessionId);
|
|
4033
|
+
this.clients.delete(params.sessionId);
|
|
3862
4034
|
} else {
|
|
3863
|
-
await sessions.delete(this.userId, sessionId);
|
|
4035
|
+
await sessions.delete(this.userId, params.sessionId);
|
|
3864
4036
|
}
|
|
3865
4037
|
return { success: true };
|
|
3866
4038
|
}
|
|
4039
|
+
// -----------------------------------------------------------------------
|
|
4040
|
+
// OAuth
|
|
4041
|
+
// -----------------------------------------------------------------------
|
|
3867
4042
|
/**
|
|
3868
|
-
*
|
|
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.
|
|
3869
4053
|
*/
|
|
3870
|
-
async
|
|
3871
|
-
const
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
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;
|
|
3878
4087
|
}
|
|
3879
|
-
const client = new MCPClient({
|
|
3880
|
-
userId: this.userId,
|
|
3881
|
-
sessionId,
|
|
3882
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3883
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3884
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3885
|
-
// reloading during initialize().
|
|
3886
|
-
serverId: session.serverId,
|
|
3887
|
-
serverName: session.serverName,
|
|
3888
|
-
serverUrl: session.serverUrl,
|
|
3889
|
-
callbackUrl: session.callbackUrl,
|
|
3890
|
-
transportType: session.transportType,
|
|
3891
|
-
headers: session.headers
|
|
3892
|
-
});
|
|
3893
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3894
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3895
|
-
await client.connect();
|
|
3896
|
-
this.clients.set(sessionId, client);
|
|
3897
|
-
return client;
|
|
3898
4088
|
}
|
|
4089
|
+
// -----------------------------------------------------------------------
|
|
4090
|
+
// Tool Discovery & Policy
|
|
4091
|
+
// -----------------------------------------------------------------------
|
|
3899
4092
|
/**
|
|
3900
|
-
* Fetches
|
|
4093
|
+
* Fetches the complete tool list from the remote MCP server and emits a
|
|
4094
|
+
* `tools_discovered` SSE event with two lists:
|
|
3901
4095
|
*
|
|
3902
|
-
*
|
|
3903
|
-
* - `
|
|
3904
|
-
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
3905
|
-
* 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)
|
|
3906
4098
|
*
|
|
3907
|
-
* `fetchTools()`
|
|
3908
|
-
* `gateway.listTools()`
|
|
3909
|
-
* 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.
|
|
3910
4101
|
*
|
|
3911
|
-
* @param sessionId - The session
|
|
3912
|
-
* @returns The session record and the
|
|
3913
|
-
* @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.
|
|
3914
4105
|
*/
|
|
3915
4106
|
async listPolicyFilteredTools(sessionId) {
|
|
3916
|
-
const session = await
|
|
3917
|
-
if (!session) {
|
|
3918
|
-
throw new Error("Session not found");
|
|
3919
|
-
}
|
|
4107
|
+
const session = await this.requireSession(sessionId);
|
|
3920
4108
|
const client = await this.getOrCreateClient(sessionId);
|
|
3921
4109
|
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
3922
4110
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
3923
4111
|
const result = await gateway.listTools();
|
|
3924
|
-
this.
|
|
4112
|
+
this.sendEvent({
|
|
3925
4113
|
type: "tools_discovered",
|
|
3926
4114
|
sessionId,
|
|
3927
4115
|
serverId: session.serverId ?? "unknown",
|
|
@@ -3933,254 +4121,254 @@ var SSEConnectionManager = class {
|
|
|
3933
4121
|
return { session, result };
|
|
3934
4122
|
}
|
|
3935
4123
|
/**
|
|
3936
|
-
* Returns the policy-filtered tool list for a session
|
|
3937
|
-
*
|
|
3938
|
-
*
|
|
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.
|
|
3939
4128
|
*/
|
|
3940
4129
|
async listTools(params) {
|
|
3941
|
-
const {
|
|
3942
|
-
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4130
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3943
4131
|
return { tools: result.tools };
|
|
3944
4132
|
}
|
|
3945
4133
|
/**
|
|
3946
|
-
*
|
|
4134
|
+
* Returns all raw tools annotated with their effective policy state
|
|
4135
|
+
* for display in the management UI.
|
|
3947
4136
|
*/
|
|
3948
4137
|
async getToolPolicy(params) {
|
|
3949
|
-
const
|
|
3950
|
-
const
|
|
3951
|
-
if (!session) {
|
|
3952
|
-
throw new Error("Session not found");
|
|
3953
|
-
}
|
|
3954
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4138
|
+
const session = await this.requireSession(params.sessionId);
|
|
4139
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3955
4140
|
const allTools = await client.fetchTools();
|
|
3956
|
-
const
|
|
4141
|
+
const policy = session.toolPolicy ?? {
|
|
3957
4142
|
mode: "all",
|
|
3958
4143
|
toolIds: [],
|
|
3959
4144
|
updatedAt: session.updatedAt ?? session.createdAt
|
|
3960
4145
|
};
|
|
3961
4146
|
const serverId = session.serverId ?? "unknown";
|
|
3962
|
-
const tools = allTools.tools.map((
|
|
3963
|
-
...
|
|
3964
|
-
toolId: createToolId(serverId,
|
|
3965
|
-
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)
|
|
3966
4151
|
}));
|
|
3967
4152
|
return {
|
|
3968
|
-
toolPolicy,
|
|
4153
|
+
toolPolicy: policy,
|
|
3969
4154
|
tools,
|
|
3970
4155
|
toolCount: tools.length,
|
|
3971
|
-
allowedToolCount: tools.filter((
|
|
4156
|
+
allowedToolCount: tools.filter((t) => t.allowed).length
|
|
3972
4157
|
};
|
|
3973
4158
|
}
|
|
3974
4159
|
/**
|
|
3975
|
-
* Persists a new tool access policy
|
|
3976
|
-
*
|
|
3977
|
-
*
|
|
3978
|
-
* Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
|
|
3979
|
-
* so the management UI can immediately reflect the new checkbox states without
|
|
3980
|
-
* 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.
|
|
3981
4162
|
*
|
|
3982
|
-
* @
|
|
3983
|
-
*
|
|
4163
|
+
* @throws {Error} If the session does not exist or the policy references
|
|
4164
|
+
* tool IDs that don't match any known tool.
|
|
3984
4165
|
*/
|
|
3985
4166
|
async setToolPolicy(params) {
|
|
3986
|
-
const
|
|
3987
|
-
const
|
|
3988
|
-
if (!session) {
|
|
3989
|
-
throw new Error("Session not found");
|
|
3990
|
-
}
|
|
3991
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4167
|
+
const session = await this.requireSession(params.sessionId);
|
|
4168
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3992
4169
|
const allTools = await client.fetchTools();
|
|
3993
|
-
const
|
|
3994
|
-
validateToolPolicyAgainstTools(
|
|
3995
|
-
await sessions.update(this.userId, sessionId, { toolPolicy });
|
|
3996
|
-
const
|
|
3997
|
-
|
|
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({
|
|
3998
4179
|
type: "tools_discovered",
|
|
3999
|
-
sessionId,
|
|
4180
|
+
sessionId: params.sessionId,
|
|
4000
4181
|
serverId: session.serverId ?? "unknown",
|
|
4001
|
-
toolCount:
|
|
4002
|
-
tools:
|
|
4182
|
+
toolCount: filtered.length,
|
|
4183
|
+
tools: filtered,
|
|
4003
4184
|
allTools: allTools.tools,
|
|
4004
4185
|
timestamp: Date.now()
|
|
4005
4186
|
});
|
|
4006
|
-
return {
|
|
4007
|
-
success: true,
|
|
4008
|
-
toolPolicy,
|
|
4009
|
-
tools: filteredTools,
|
|
4010
|
-
toolCount: filteredTools.length
|
|
4011
|
-
};
|
|
4012
|
-
}
|
|
4013
|
-
/**
|
|
4014
|
-
* Proxies a tool invocation to the remote MCP server.
|
|
4015
|
-
* Resolves the client for the given session and delegates to the tool router.
|
|
4016
|
-
*/
|
|
4017
|
-
async callTool(params) {
|
|
4018
|
-
const { sessionId, toolName, toolArgs } = params;
|
|
4019
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4020
|
-
const result = await createToolPolicyGateway(this.userId, sessionId, client).callTool(toolName, toolArgs);
|
|
4021
|
-
const meta = result._meta || {};
|
|
4022
|
-
return {
|
|
4023
|
-
...result,
|
|
4024
|
-
_meta: {
|
|
4025
|
-
...meta,
|
|
4026
|
-
sessionId
|
|
4027
|
-
}
|
|
4028
|
-
};
|
|
4187
|
+
return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
|
|
4029
4188
|
}
|
|
4030
4189
|
/**
|
|
4031
|
-
*
|
|
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.
|
|
4032
4199
|
*/
|
|
4033
|
-
async
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
throw new Error("Session not found");
|
|
4038
|
-
}
|
|
4039
|
-
this.emitConnectionEvent({
|
|
4040
|
-
type: "state_changed",
|
|
4041
|
-
sessionId,
|
|
4042
|
-
serverId: session.serverId ?? "unknown",
|
|
4043
|
-
serverName: session.serverName ?? "Unknown",
|
|
4044
|
-
serverUrl: session.serverUrl,
|
|
4045
|
-
state: "VALIDATING",
|
|
4046
|
-
previousState: "DISCONNECTED",
|
|
4047
|
-
timestamp: Date.now()
|
|
4048
|
-
});
|
|
4049
|
-
try {
|
|
4050
|
-
const clientMetadata = await this.getResolvedClientMetadata();
|
|
4051
|
-
const client = new MCPClient({
|
|
4052
|
-
userId: this.userId,
|
|
4053
|
-
sessionId,
|
|
4054
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4055
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4056
|
-
// original transport/connection metadata instead of relying on lazy
|
|
4057
|
-
// reloading during initialize().
|
|
4058
|
-
serverId: session.serverId,
|
|
4059
|
-
serverName: session.serverName,
|
|
4060
|
-
serverUrl: session.serverUrl,
|
|
4061
|
-
callbackUrl: session.callbackUrl,
|
|
4062
|
-
transportType: session.transportType,
|
|
4063
|
-
headers: session.headers,
|
|
4064
|
-
...clientMetadata
|
|
4065
|
-
});
|
|
4066
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4067
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
4068
|
-
await client.connect();
|
|
4069
|
-
this.clients.set(sessionId, client);
|
|
4070
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4071
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4072
|
-
} catch (error) {
|
|
4073
|
-
this.emitConnectionEvent({
|
|
4074
|
-
type: "error",
|
|
4075
|
-
sessionId,
|
|
4076
|
-
serverId: session.serverId ?? "unknown",
|
|
4077
|
-
error: error instanceof Error ? error.message : "Validation failed",
|
|
4078
|
-
errorType: "validation",
|
|
4079
|
-
timestamp: Date.now()
|
|
4080
|
-
});
|
|
4081
|
-
throw error;
|
|
4082
|
-
}
|
|
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 };
|
|
4083
4204
|
}
|
|
4205
|
+
// -----------------------------------------------------------------------
|
|
4206
|
+
// Tool Execution
|
|
4207
|
+
// -----------------------------------------------------------------------
|
|
4084
4208
|
/**
|
|
4085
|
-
*
|
|
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.
|
|
4086
4214
|
*/
|
|
4087
|
-
async
|
|
4088
|
-
const
|
|
4089
|
-
const
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
}
|
|
4096
|
-
try {
|
|
4097
|
-
const client = new MCPClient({
|
|
4098
|
-
userId: this.userId,
|
|
4099
|
-
sessionId,
|
|
4100
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4101
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4102
|
-
// original connection metadata instead of relying on lazy
|
|
4103
|
-
// reloading during initialize().
|
|
4104
|
-
serverId: session.serverId,
|
|
4105
|
-
serverName: session.serverName,
|
|
4106
|
-
serverUrl: session.serverUrl,
|
|
4107
|
-
callbackUrl: session.callbackUrl,
|
|
4108
|
-
// NOTE: transportType is intentionally omitted here.
|
|
4109
|
-
// The session's stored transportType is a placeholder ('streamable-http')
|
|
4110
|
-
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
4111
|
-
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
4112
|
-
// Neon that only support SSE transport.
|
|
4113
|
-
headers: session.headers
|
|
4114
|
-
});
|
|
4115
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4116
|
-
await client.finishAuth(code, oauthState);
|
|
4117
|
-
this.clients.set(sessionId, client);
|
|
4118
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4119
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4120
|
-
} catch (error) {
|
|
4121
|
-
this.emitConnectionEvent({
|
|
4122
|
-
type: "error",
|
|
4123
|
-
sessionId,
|
|
4124
|
-
serverId: session.serverId ?? "unknown",
|
|
4125
|
-
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
4126
|
-
errorType: "auth",
|
|
4127
|
-
timestamp: Date.now()
|
|
4128
|
-
});
|
|
4129
|
-
throw error;
|
|
4130
|
-
}
|
|
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 } };
|
|
4131
4223
|
}
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4224
|
+
// -----------------------------------------------------------------------
|
|
4225
|
+
// Prompts
|
|
4226
|
+
// -----------------------------------------------------------------------
|
|
4227
|
+
/** Lists all prompts exposed by the remote MCP server. */
|
|
4135
4228
|
async listPrompts(params) {
|
|
4136
|
-
const
|
|
4137
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4229
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4138
4230
|
const result = await client.listPrompts();
|
|
4139
4231
|
return { prompts: result.prompts };
|
|
4140
4232
|
}
|
|
4141
|
-
/**
|
|
4142
|
-
* Get a specific prompt
|
|
4143
|
-
*/
|
|
4233
|
+
/** Retrieves a specific prompt by name with optional arguments. */
|
|
4144
4234
|
async getPrompt(params) {
|
|
4145
|
-
const
|
|
4146
|
-
|
|
4147
|
-
return await client.getPrompt(name, args);
|
|
4235
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4236
|
+
return client.getPrompt(params.name, params.args);
|
|
4148
4237
|
}
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4238
|
+
// -----------------------------------------------------------------------
|
|
4239
|
+
// Resources
|
|
4240
|
+
// -----------------------------------------------------------------------
|
|
4241
|
+
/** Lists all resources exposed by the remote MCP server. */
|
|
4152
4242
|
async listResources(params) {
|
|
4153
|
-
const
|
|
4154
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4243
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4155
4244
|
const result = await client.listResources();
|
|
4156
4245
|
return { resources: result.resources };
|
|
4157
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
|
+
}
|
|
4158
4278
|
/**
|
|
4159
|
-
*
|
|
4279
|
+
* Returns the cached in-memory transport for `sessionId`, or creates one
|
|
4280
|
+
* from the persisted session row (with credentials) and connects it.
|
|
4160
4281
|
*/
|
|
4161
|
-
async
|
|
4162
|
-
const
|
|
4163
|
-
|
|
4164
|
-
|
|
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;
|
|
4294
|
+
}
|
|
4295
|
+
/**
|
|
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.
|
|
4301
|
+
*/
|
|
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
|
+
});
|
|
4165
4320
|
}
|
|
4166
4321
|
/**
|
|
4167
|
-
*
|
|
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).
|
|
4168
4326
|
*/
|
|
4169
|
-
|
|
4170
|
-
this.sendEvent(
|
|
4327
|
+
attachClientEvents(client) {
|
|
4328
|
+
client.onConnectionEvent((e) => this.sendEvent(e));
|
|
4329
|
+
client.onObservabilityEvent((e) => this.sendEvent(e));
|
|
4171
4330
|
}
|
|
4172
4331
|
/**
|
|
4173
|
-
*
|
|
4332
|
+
* Registers the client in the in-memory cache and attaches its event
|
|
4333
|
+
* listeners to the unified observability channel.
|
|
4174
4334
|
*/
|
|
4175
|
-
|
|
4176
|
-
this.
|
|
4177
|
-
|
|
4178
|
-
|
|
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;
|
|
4179
4362
|
}
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
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);
|
|
4184
4372
|
}
|
|
4185
4373
|
};
|
|
4186
4374
|
function createSSEHandler(options) {
|
|
@@ -4209,8 +4397,7 @@ function createSSEHandler(options) {
|
|
|
4209
4397
|
});
|
|
4210
4398
|
req.on("end", async () => {
|
|
4211
4399
|
try {
|
|
4212
|
-
|
|
4213
|
-
await manager.handleRequest(request);
|
|
4400
|
+
await manager.handleRequest(JSON.parse(body));
|
|
4214
4401
|
} catch {
|
|
4215
4402
|
}
|
|
4216
4403
|
});
|
|
@@ -4423,6 +4610,9 @@ var SSEClient = class {
|
|
|
4423
4610
|
async getToolPolicy(sessionId) {
|
|
4424
4611
|
return this.sendRequest("getToolPolicy", { sessionId });
|
|
4425
4612
|
}
|
|
4613
|
+
async updateSession(sessionId, enabled) {
|
|
4614
|
+
return this.sendRequest("updateSession", { sessionId, enabled });
|
|
4615
|
+
}
|
|
4426
4616
|
async listTools(sessionId) {
|
|
4427
4617
|
return this.sendRequest("listTools", { sessionId });
|
|
4428
4618
|
}
|
|
@@ -6354,6 +6544,8 @@ exports.createNextMcpHandler = createNextMcpHandler;
|
|
|
6354
6544
|
exports.createRegexSearchToolDefinition = createRegexSearchToolDefinition;
|
|
6355
6545
|
exports.createSSEHandler = createSSEHandler;
|
|
6356
6546
|
exports.createSearchToolDefinition = createSearchToolDefinition;
|
|
6547
|
+
exports.decryptObject = decryptObject;
|
|
6548
|
+
exports.encryptObject = encryptObject;
|
|
6357
6549
|
exports.executeMetaTool = executeMetaTool;
|
|
6358
6550
|
exports.findToolByName = findToolByName;
|
|
6359
6551
|
exports.getToolUiResourceUri = getToolUiResourceUri;
|
|
@@ -6367,5 +6559,6 @@ exports.onSessionMutation = onSessionMutation;
|
|
|
6367
6559
|
exports.resolveMetaToolProxy = resolveMetaToolProxy;
|
|
6368
6560
|
exports.sanitizeServerLabel = sanitizeServerLabel;
|
|
6369
6561
|
exports.sessions = sessions;
|
|
6562
|
+
exports.withDbObservability = withDbObservability;
|
|
6370
6563
|
//# sourceMappingURL=index.js.map
|
|
6371
6564
|
//# sourceMappingURL=index.js.map
|