@mcp-ts/sdk 2.5.3 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -574
- package/dist/adapters/agui-adapter.d.mts +4 -4
- package/dist/adapters/agui-adapter.d.ts +4 -4
- package/dist/adapters/agui-middleware.d.mts +4 -4
- package/dist/adapters/agui-middleware.d.ts +4 -4
- package/dist/adapters/ai-adapter.d.mts +4 -4
- package/dist/adapters/ai-adapter.d.ts +4 -4
- package/dist/adapters/langchain-adapter.d.mts +4 -4
- package/dist/adapters/langchain-adapter.d.ts +4 -4
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -0
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +10 -18
- package/dist/client/react.d.ts +10 -18
- package/dist/client/react.js +17 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +17 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +3 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +3 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1056 -876
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -871
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
- package/dist/server/index.d.mts +338 -88
- package/dist/server/index.d.ts +338 -88
- package/dist/server/index.js +1053 -876
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1045 -871
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
- package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
- package/package.json +2 -1
- package/src/client/core/sse-client.ts +5 -0
- package/src/client/react/use-mcp.ts +27 -28
- package/src/server/handlers/sse-handler.ts +671 -633
- package/src/server/index.ts +2 -1
- package/src/server/mcp/multi-session-client.ts +34 -10
- package/src/server/mcp/oauth-client.ts +197 -205
- package/src/server/mcp/storage-oauth-provider.ts +148 -41
- package/src/server/mcp/tool-policy-gateway.ts +11 -1
- package/src/server/storage/file-backend.ts +25 -21
- package/src/server/storage/index.ts +71 -0
- package/src/server/storage/memory-backend.ts +24 -12
- package/src/server/storage/neon-backend.ts +70 -72
- package/src/server/storage/redis-backend.ts +26 -36
- package/src/server/storage/sqlite-backend.ts +25 -39
- package/src/server/storage/supabase-backend.ts +45 -54
- package/src/server/storage/types.ts +22 -2
- package/src/shared/types.ts +13 -3
package/dist/index.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/
|
|
|
4
4
|
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
5
5
|
import { UnauthorizedError as UnauthorizedError$1 } from '@modelcontextprotocol/sdk/client/auth.js';
|
|
6
6
|
import { ListToolsResultSchema, CallToolResultSchema, ListPromptsResultSchema, GetPromptResultSchema, ListResourcesResultSchema, ReadResourceResultSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
7
|
+
import { AsyncLocalStorage } from 'async_hooks';
|
|
7
8
|
import * as fs2 from 'fs';
|
|
8
9
|
import { promises } from 'fs';
|
|
9
10
|
import * as path from 'path';
|
|
@@ -277,7 +278,6 @@ var RedisStorageBackend = class {
|
|
|
277
278
|
constructor(redis2) {
|
|
278
279
|
this.redis = redis2;
|
|
279
280
|
__publicField(this, "KEY_PREFIX", "mcp:session:");
|
|
280
|
-
__publicField(this, "CREDENTIALS_KEY_PREFIX", "mcp:credentials:");
|
|
281
281
|
__publicField(this, "USER_ID_KEY_PREFIX", "mcp:userId:");
|
|
282
282
|
__publicField(this, "USER_ID_KEY_SUFFIX", ":sessions");
|
|
283
283
|
}
|
|
@@ -296,9 +296,6 @@ var RedisStorageBackend = class {
|
|
|
296
296
|
getSessionKey(userId, sessionId) {
|
|
297
297
|
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
298
298
|
}
|
|
299
|
-
getCredentialsKey(userId, sessionId) {
|
|
300
|
-
return `${this.CREDENTIALS_KEY_PREFIX}${userId}:${sessionId}`;
|
|
301
|
-
}
|
|
302
299
|
/**
|
|
303
300
|
* Generates Redis key for tracking all sessions for a user
|
|
304
301
|
* @private
|
|
@@ -388,48 +385,54 @@ var RedisStorageBackend = class {
|
|
|
388
385
|
}
|
|
389
386
|
async patchCredentials(userId, sessionId, data) {
|
|
390
387
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
if (!sessionExists) {
|
|
388
|
+
const currentStr = await this.redis.get(sessionKey);
|
|
389
|
+
if (!currentStr) {
|
|
394
390
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
395
391
|
}
|
|
396
|
-
const
|
|
397
|
-
const
|
|
398
|
-
const effectiveTtl =
|
|
399
|
-
|
|
400
|
-
const current = currentStr ? JSON.parse(currentStr) : { sessionId, userId };
|
|
401
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
402
|
-
if (effectiveTtl > 0) {
|
|
403
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials), "EX", effectiveTtl);
|
|
404
|
-
} else {
|
|
405
|
-
await this.redis.set(credentialsKey, JSON.stringify(credentials));
|
|
406
|
-
}
|
|
392
|
+
const current = JSON.parse(currentStr);
|
|
393
|
+
const updated = { ...current, ...data, sessionId, userId };
|
|
394
|
+
const effectiveTtl = resolveSessionRedisTtlSeconds(updated);
|
|
395
|
+
await this.redis.set(sessionKey, JSON.stringify(updated), "EX", effectiveTtl);
|
|
407
396
|
}
|
|
408
|
-
async get(userId, sessionId) {
|
|
397
|
+
async get(userId, sessionId, options) {
|
|
409
398
|
try {
|
|
410
399
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
411
400
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
412
|
-
if (!sessionDataStr)
|
|
413
|
-
|
|
401
|
+
if (!sessionDataStr) return null;
|
|
402
|
+
const session = normalizeStoredSession(JSON.parse(sessionDataStr));
|
|
403
|
+
if (!options?.includeCredentials) {
|
|
404
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
405
|
+
return sessionOnly;
|
|
414
406
|
}
|
|
415
|
-
|
|
416
|
-
return normalizeStoredSession(session);
|
|
407
|
+
return session;
|
|
417
408
|
} catch (error) {
|
|
418
409
|
console.error("[RedisStorageBackend] Failed to get session:", error);
|
|
419
410
|
return null;
|
|
420
411
|
}
|
|
421
412
|
}
|
|
422
413
|
async getCredentials(userId, sessionId) {
|
|
423
|
-
const session = await this.get(userId, sessionId);
|
|
414
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
424
415
|
if (!session) return null;
|
|
425
|
-
const
|
|
426
|
-
return
|
|
416
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
417
|
+
return {
|
|
418
|
+
sessionId,
|
|
419
|
+
userId,
|
|
420
|
+
clientInformation,
|
|
421
|
+
tokens,
|
|
422
|
+
codeVerifier,
|
|
423
|
+
codeVerifierChallenge,
|
|
424
|
+
codeVerifierNonce,
|
|
425
|
+
clientId,
|
|
426
|
+
oauthState
|
|
427
|
+
};
|
|
427
428
|
}
|
|
428
429
|
async clearCredentials(userId, sessionId) {
|
|
429
430
|
await this.patchCredentials(userId, sessionId, {
|
|
430
431
|
clientInformation: null,
|
|
431
432
|
tokens: null,
|
|
432
433
|
codeVerifier: null,
|
|
434
|
+
codeVerifierChallenge: null,
|
|
435
|
+
codeVerifierNonce: null,
|
|
433
436
|
clientId: null,
|
|
434
437
|
oauthState: null
|
|
435
438
|
});
|
|
@@ -462,10 +465,9 @@ var RedisStorageBackend = class {
|
|
|
462
465
|
async delete(userId, sessionId) {
|
|
463
466
|
try {
|
|
464
467
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
465
|
-
const credentialsKey = this.getCredentialsKey(userId, sessionId);
|
|
466
468
|
const userIdKey = this.getUserIdKey(userId);
|
|
467
469
|
await this.redis.srem(userIdKey, sessionId);
|
|
468
|
-
await this.redis.del(sessionKey
|
|
470
|
+
await this.redis.del(sessionKey);
|
|
469
471
|
} catch (error) {
|
|
470
472
|
console.error("[RedisStorageBackend] Failed to remove session:", error);
|
|
471
473
|
}
|
|
@@ -496,9 +498,8 @@ var RedisStorageBackend = class {
|
|
|
496
498
|
async clearAll() {
|
|
497
499
|
try {
|
|
498
500
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
499
|
-
const credentialKeys = await this.scanKeys(`${this.CREDENTIALS_KEY_PREFIX}*`);
|
|
500
501
|
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
501
|
-
const allKeys = [...keys, ...
|
|
502
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
502
503
|
if (allKeys.length > 0) {
|
|
503
504
|
await this.redis.del(...allKeys);
|
|
504
505
|
}
|
|
@@ -522,7 +523,6 @@ var RedisStorageBackend = class {
|
|
|
522
523
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
523
524
|
if (staleSessionIds.length > 0) {
|
|
524
525
|
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
525
|
-
await this.redis.del(...staleSessionIds.map((sessionId) => this.getCredentialsKey(userId, sessionId)));
|
|
526
526
|
}
|
|
527
527
|
const remainingCount = await this.redis.scard(userIdKey);
|
|
528
528
|
if (remainingCount === 0) {
|
|
@@ -547,7 +547,6 @@ var MemoryStorageBackend = class {
|
|
|
547
547
|
constructor() {
|
|
548
548
|
// Map<userId:sessionId, Session>
|
|
549
549
|
__publicField(this, "sessions", /* @__PURE__ */ new Map());
|
|
550
|
-
__publicField(this, "credentials", /* @__PURE__ */ new Map());
|
|
551
550
|
// Map<userId, Set<sessionId>>
|
|
552
551
|
__publicField(this, "userIdSessions", /* @__PURE__ */ new Map());
|
|
553
552
|
}
|
|
@@ -585,26 +584,46 @@ var MemoryStorageBackend = class {
|
|
|
585
584
|
}
|
|
586
585
|
async patchCredentials(userId, sessionId, data) {
|
|
587
586
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
588
|
-
|
|
587
|
+
const current = this.sessions.get(sessionKey);
|
|
588
|
+
if (!current) {
|
|
589
589
|
throw new Error(`Session ${sessionId} not found`);
|
|
590
590
|
}
|
|
591
|
-
|
|
592
|
-
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
591
|
+
this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
593
592
|
}
|
|
594
|
-
async get(userId, sessionId) {
|
|
593
|
+
async get(userId, sessionId, options) {
|
|
595
594
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
596
|
-
|
|
595
|
+
const session = this.sessions.get(sessionKey) || null;
|
|
596
|
+
if (!session) return session;
|
|
597
|
+
if (!options?.includeCredentials) {
|
|
598
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
599
|
+
return sessionOnly;
|
|
600
|
+
}
|
|
601
|
+
return session;
|
|
597
602
|
}
|
|
598
603
|
async getCredentials(userId, sessionId) {
|
|
599
604
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
600
|
-
|
|
601
|
-
|
|
605
|
+
const session = this.sessions.get(sessionKey);
|
|
606
|
+
if (!session) return null;
|
|
607
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
608
|
+
return {
|
|
609
|
+
sessionId,
|
|
610
|
+
userId,
|
|
611
|
+
clientInformation,
|
|
612
|
+
tokens,
|
|
613
|
+
codeVerifier,
|
|
614
|
+
codeVerifierChallenge,
|
|
615
|
+
codeVerifierNonce,
|
|
616
|
+
clientId,
|
|
617
|
+
oauthState
|
|
618
|
+
};
|
|
602
619
|
}
|
|
603
620
|
async clearCredentials(userId, sessionId) {
|
|
604
621
|
await this.patchCredentials(userId, sessionId, {
|
|
605
622
|
clientInformation: null,
|
|
606
623
|
tokens: null,
|
|
607
624
|
codeVerifier: null,
|
|
625
|
+
codeVerifierChallenge: null,
|
|
626
|
+
codeVerifierNonce: null,
|
|
608
627
|
clientId: null,
|
|
609
628
|
oauthState: null
|
|
610
629
|
});
|
|
@@ -628,7 +647,6 @@ var MemoryStorageBackend = class {
|
|
|
628
647
|
async delete(userId, sessionId) {
|
|
629
648
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
630
649
|
this.sessions.delete(sessionKey);
|
|
631
|
-
this.credentials.delete(sessionKey);
|
|
632
650
|
const set = this.userIdSessions.get(userId);
|
|
633
651
|
if (set) {
|
|
634
652
|
set.delete(sessionId);
|
|
@@ -642,14 +660,12 @@ var MemoryStorageBackend = class {
|
|
|
642
660
|
}
|
|
643
661
|
async clearAll() {
|
|
644
662
|
this.sessions.clear();
|
|
645
|
-
this.credentials.clear();
|
|
646
663
|
this.userIdSessions.clear();
|
|
647
664
|
}
|
|
648
665
|
async cleanupExpired() {
|
|
649
666
|
for (const [key, session] of this.sessions.entries()) {
|
|
650
667
|
if (!isSessionExpired(session)) continue;
|
|
651
668
|
this.sessions.delete(key);
|
|
652
|
-
this.credentials.delete(key);
|
|
653
669
|
const set = this.userIdSessions.get(session.userId);
|
|
654
670
|
if (set) {
|
|
655
671
|
set.delete(session.sessionId);
|
|
@@ -669,7 +685,6 @@ var FileStorageBackend = class {
|
|
|
669
685
|
constructor(options = {}) {
|
|
670
686
|
__publicField(this, "filePath");
|
|
671
687
|
__publicField(this, "memoryCache", null);
|
|
672
|
-
__publicField(this, "credentialsCache", null);
|
|
673
688
|
__publicField(this, "initialized", false);
|
|
674
689
|
this.filePath = options.path || "./sessions.json";
|
|
675
690
|
}
|
|
@@ -684,22 +699,15 @@ var FileStorageBackend = class {
|
|
|
684
699
|
const data = await promises.readFile(this.filePath, "utf-8");
|
|
685
700
|
const json = JSON.parse(data);
|
|
686
701
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
687
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
688
702
|
if (Array.isArray(json.sessions)) {
|
|
689
703
|
json.sessions.forEach((s) => {
|
|
690
704
|
const session = normalizeStoredSession(s);
|
|
691
705
|
this.memoryCache.set(this.getSessionKey(session.userId || "unknown", session.sessionId), session);
|
|
692
706
|
});
|
|
693
707
|
}
|
|
694
|
-
if (Array.isArray(json.credentials)) {
|
|
695
|
-
json.credentials.forEach((c) => {
|
|
696
|
-
this.credentialsCache.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
708
|
} catch (error) {
|
|
700
709
|
if (error.code === "ENOENT") {
|
|
701
710
|
this.memoryCache = /* @__PURE__ */ new Map();
|
|
702
|
-
this.credentialsCache = /* @__PURE__ */ new Map();
|
|
703
711
|
await this.flush();
|
|
704
712
|
} else {
|
|
705
713
|
console.error("[FileStorage] Failed to load sessions:", error);
|
|
@@ -713,10 +721,9 @@ var FileStorageBackend = class {
|
|
|
713
721
|
if (!this.initialized) await this.init();
|
|
714
722
|
}
|
|
715
723
|
async flush() {
|
|
716
|
-
if (!this.memoryCache
|
|
724
|
+
if (!this.memoryCache) return;
|
|
717
725
|
await promises.writeFile(this.filePath, JSON.stringify({
|
|
718
|
-
sessions: Array.from(this.memoryCache.values())
|
|
719
|
-
credentials: Array.from(this.credentialsCache.values())
|
|
726
|
+
sessions: Array.from(this.memoryCache.values())
|
|
720
727
|
}, null, 2), "utf-8");
|
|
721
728
|
}
|
|
722
729
|
getSessionKey(userId, sessionId) {
|
|
@@ -751,29 +758,49 @@ var FileStorageBackend = class {
|
|
|
751
758
|
async patchCredentials(userId, sessionId, data) {
|
|
752
759
|
await this.ensureInitialized();
|
|
753
760
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
754
|
-
|
|
761
|
+
const current = this.memoryCache.get(sessionKey);
|
|
762
|
+
if (!current) {
|
|
755
763
|
throw new Error(`Session ${sessionId} not found`);
|
|
756
764
|
}
|
|
757
|
-
|
|
758
|
-
this.credentialsCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
765
|
+
this.memoryCache.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
759
766
|
await this.flush();
|
|
760
767
|
}
|
|
761
|
-
async get(userId, sessionId) {
|
|
768
|
+
async get(userId, sessionId, options) {
|
|
762
769
|
await this.ensureInitialized();
|
|
763
770
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
764
|
-
|
|
771
|
+
const session = this.memoryCache.get(sessionKey) || null;
|
|
772
|
+
if (!session) return null;
|
|
773
|
+
if (!options?.includeCredentials) {
|
|
774
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
775
|
+
return sessionOnly;
|
|
776
|
+
}
|
|
777
|
+
return session;
|
|
765
778
|
}
|
|
766
779
|
async getCredentials(userId, sessionId) {
|
|
767
780
|
await this.ensureInitialized();
|
|
768
781
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
769
|
-
|
|
770
|
-
|
|
782
|
+
const session = this.memoryCache.get(sessionKey);
|
|
783
|
+
if (!session) return null;
|
|
784
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
785
|
+
return {
|
|
786
|
+
sessionId,
|
|
787
|
+
userId,
|
|
788
|
+
clientInformation,
|
|
789
|
+
tokens,
|
|
790
|
+
codeVerifier,
|
|
791
|
+
codeVerifierChallenge,
|
|
792
|
+
codeVerifierNonce,
|
|
793
|
+
clientId,
|
|
794
|
+
oauthState
|
|
795
|
+
};
|
|
771
796
|
}
|
|
772
797
|
async clearCredentials(userId, sessionId) {
|
|
773
798
|
await this.patchCredentials(userId, sessionId, {
|
|
774
799
|
clientInformation: null,
|
|
775
800
|
tokens: null,
|
|
776
801
|
codeVerifier: null,
|
|
802
|
+
codeVerifierChallenge: null,
|
|
803
|
+
codeVerifierNonce: null,
|
|
777
804
|
clientId: null,
|
|
778
805
|
oauthState: null
|
|
779
806
|
});
|
|
@@ -790,7 +817,6 @@ var FileStorageBackend = class {
|
|
|
790
817
|
await this.ensureInitialized();
|
|
791
818
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
792
819
|
const deleted = this.memoryCache.delete(sessionKey);
|
|
793
|
-
this.credentialsCache.delete(sessionKey);
|
|
794
820
|
if (deleted) {
|
|
795
821
|
await this.flush();
|
|
796
822
|
}
|
|
@@ -802,7 +828,6 @@ var FileStorageBackend = class {
|
|
|
802
828
|
async clearAll() {
|
|
803
829
|
await this.ensureInitialized();
|
|
804
830
|
this.memoryCache.clear();
|
|
805
|
-
this.credentialsCache.clear();
|
|
806
831
|
await this.flush();
|
|
807
832
|
}
|
|
808
833
|
async cleanupExpired() {
|
|
@@ -811,7 +836,6 @@ var FileStorageBackend = class {
|
|
|
811
836
|
for (const [key, session] of this.memoryCache.entries()) {
|
|
812
837
|
if (!isSessionExpired(session)) continue;
|
|
813
838
|
this.memoryCache.delete(key);
|
|
814
|
-
this.credentialsCache.delete(key);
|
|
815
839
|
changed = true;
|
|
816
840
|
}
|
|
817
841
|
if (changed) {
|
|
@@ -825,12 +849,10 @@ var SqliteStorage = class {
|
|
|
825
849
|
constructor(options = {}) {
|
|
826
850
|
__publicField(this, "db", null);
|
|
827
851
|
__publicField(this, "table");
|
|
828
|
-
__publicField(this, "credentialsTable");
|
|
829
852
|
__publicField(this, "initialized", false);
|
|
830
853
|
__publicField(this, "dbPath");
|
|
831
854
|
this.dbPath = options.path || "./sessions.db";
|
|
832
855
|
this.table = options.table || "mcp_sessions";
|
|
833
|
-
this.credentialsTable = `${this.table}_credentials`;
|
|
834
856
|
}
|
|
835
857
|
async init() {
|
|
836
858
|
if (this.initialized) return;
|
|
@@ -850,13 +872,6 @@ var SqliteStorage = class {
|
|
|
850
872
|
expiresAt INTEGER
|
|
851
873
|
);
|
|
852
874
|
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
853
|
-
CREATE TABLE IF NOT EXISTS ${this.credentialsTable} (
|
|
854
|
-
sessionId TEXT NOT NULL,
|
|
855
|
-
userId TEXT NOT NULL,
|
|
856
|
-
data TEXT NOT NULL,
|
|
857
|
-
PRIMARY KEY (userId, sessionId),
|
|
858
|
-
FOREIGN KEY (sessionId) REFERENCES ${this.table}(sessionId) ON DELETE CASCADE
|
|
859
|
-
);
|
|
860
875
|
`);
|
|
861
876
|
this.initialized = true;
|
|
862
877
|
console.log(`[mcp-ts][Storage] SQLite: \u2713 database at ${this.dbPath} verified.`);
|
|
@@ -913,41 +928,54 @@ var SqliteStorage = class {
|
|
|
913
928
|
}
|
|
914
929
|
async patchCredentials(userId, sessionId, data) {
|
|
915
930
|
this.ensureInitialized();
|
|
916
|
-
|
|
931
|
+
const currentSession = await this.get(userId, sessionId, { includeCredentials: true });
|
|
932
|
+
if (!currentSession) {
|
|
917
933
|
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
918
934
|
}
|
|
919
|
-
const
|
|
920
|
-
const credentials = { ...current, ...data, sessionId, userId };
|
|
935
|
+
const updated = { ...currentSession, ...data, sessionId, userId };
|
|
921
936
|
const stmt = this.db.prepare(
|
|
922
|
-
`
|
|
923
|
-
VALUES (?, ?, ?)
|
|
924
|
-
ON CONFLICT(userId, sessionId) DO UPDATE SET data = excluded.data`
|
|
937
|
+
`UPDATE ${this.table} SET data = ? WHERE sessionId = ? AND userId = ?`
|
|
925
938
|
);
|
|
926
|
-
stmt.run(
|
|
939
|
+
stmt.run(JSON.stringify(updated), sessionId, userId);
|
|
927
940
|
}
|
|
928
|
-
async get(userId, sessionId) {
|
|
941
|
+
async get(userId, sessionId, options) {
|
|
929
942
|
this.ensureInitialized();
|
|
930
943
|
const stmt = this.db.prepare(
|
|
931
944
|
`SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
932
945
|
);
|
|
933
946
|
const row = stmt.get(sessionId, userId);
|
|
934
947
|
if (!row) return null;
|
|
935
|
-
|
|
948
|
+
const session = normalizeStoredSession(JSON.parse(row.data));
|
|
949
|
+
if (!options?.includeCredentials) {
|
|
950
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
951
|
+
return sessionOnly;
|
|
952
|
+
}
|
|
953
|
+
return session;
|
|
936
954
|
}
|
|
937
955
|
async getCredentials(userId, sessionId) {
|
|
938
956
|
this.ensureInitialized();
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
957
|
+
const session = await this.get(userId, sessionId, { includeCredentials: true });
|
|
958
|
+
if (!session) return null;
|
|
959
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
960
|
+
return {
|
|
961
|
+
sessionId,
|
|
962
|
+
userId,
|
|
963
|
+
clientInformation,
|
|
964
|
+
tokens,
|
|
965
|
+
codeVerifier,
|
|
966
|
+
codeVerifierChallenge,
|
|
967
|
+
codeVerifierNonce,
|
|
968
|
+
clientId,
|
|
969
|
+
oauthState
|
|
970
|
+
};
|
|
945
971
|
}
|
|
946
972
|
async clearCredentials(userId, sessionId) {
|
|
947
973
|
await this.patchCredentials(userId, sessionId, {
|
|
948
974
|
clientInformation: null,
|
|
949
975
|
tokens: null,
|
|
950
976
|
codeVerifier: null,
|
|
977
|
+
codeVerifierChallenge: null,
|
|
978
|
+
codeVerifierNonce: null,
|
|
951
979
|
clientId: null,
|
|
952
980
|
oauthState: null
|
|
953
981
|
});
|
|
@@ -973,9 +1001,6 @@ var SqliteStorage = class {
|
|
|
973
1001
|
const stmt = this.db.prepare(
|
|
974
1002
|
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
975
1003
|
);
|
|
976
|
-
this.db.prepare(
|
|
977
|
-
`DELETE FROM ${this.credentialsTable} WHERE sessionId = ? AND userId = ?`
|
|
978
|
-
).run(sessionId, userId);
|
|
979
1004
|
stmt.run(sessionId, userId);
|
|
980
1005
|
}
|
|
981
1006
|
async listAllIds() {
|
|
@@ -986,7 +1011,6 @@ var SqliteStorage = class {
|
|
|
986
1011
|
}
|
|
987
1012
|
async clearAll() {
|
|
988
1013
|
this.ensureInitialized();
|
|
989
|
-
this.db.prepare(`DELETE FROM ${this.credentialsTable}`).run();
|
|
990
1014
|
const stmt = this.db.prepare(`DELETE FROM ${this.table}`);
|
|
991
1015
|
stmt.run();
|
|
992
1016
|
}
|
|
@@ -1000,14 +1024,6 @@ var SqliteStorage = class {
|
|
|
1000
1024
|
deleteStmt.run(row.sessionId, row.userId);
|
|
1001
1025
|
}
|
|
1002
1026
|
}
|
|
1003
|
-
this.db.prepare(
|
|
1004
|
-
`DELETE FROM ${this.credentialsTable}
|
|
1005
|
-
WHERE NOT EXISTS (
|
|
1006
|
-
SELECT 1 FROM ${this.table}
|
|
1007
|
-
WHERE ${this.table}.sessionId = ${this.credentialsTable}.sessionId
|
|
1008
|
-
AND ${this.table}.userId = ${this.credentialsTable}.userId
|
|
1009
|
-
)`
|
|
1010
|
-
).run();
|
|
1011
1027
|
}
|
|
1012
1028
|
async disconnect() {
|
|
1013
1029
|
if (this.db) {
|
|
@@ -1089,7 +1105,6 @@ var SupabaseStorageBackend = class {
|
|
|
1089
1105
|
}
|
|
1090
1106
|
async init() {
|
|
1091
1107
|
await this.assertTable("mcp_sessions", "session_id");
|
|
1092
|
-
await this.assertTable("mcp_credentials", "session_id");
|
|
1093
1108
|
console.log("[mcp-ts][Storage] Supabase: storage tables verified.");
|
|
1094
1109
|
}
|
|
1095
1110
|
async assertTable(table, column) {
|
|
@@ -1120,18 +1135,13 @@ var SupabaseStorageBackend = class {
|
|
|
1120
1135
|
headers: decryptObject(row.headers),
|
|
1121
1136
|
authUrl: row.auth_url,
|
|
1122
1137
|
status: row.status ?? "pending",
|
|
1123
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
clientInformation: decryptObject(row?.client_information),
|
|
1131
|
-
tokens: decryptObject(row?.tokens),
|
|
1132
|
-
codeVerifier: decryptObject(row?.code_verifier),
|
|
1133
|
-
clientId: row?.client_id,
|
|
1134
|
-
oauthState: row?.oauth_state
|
|
1138
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1139
|
+
clientInformation: decryptObject(row.client_information),
|
|
1140
|
+
tokens: decryptObject(row.tokens),
|
|
1141
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1142
|
+
clientId: row.client_id,
|
|
1143
|
+
oauthState: row.oauth_state,
|
|
1144
|
+
enabled: row.enabled ?? true
|
|
1135
1145
|
};
|
|
1136
1146
|
}
|
|
1137
1147
|
hasCredentialData(data) {
|
|
@@ -1189,6 +1199,7 @@ var SupabaseStorageBackend = class {
|
|
|
1189
1199
|
if ("headers" in data) updateData.headers = encryptObject(data.headers);
|
|
1190
1200
|
if ("authUrl" in data) updateData.auth_url = data.authUrl ?? null;
|
|
1191
1201
|
if ("toolPolicy" in data) updateData.tool_policy = normalizeToolPolicy(data.toolPolicy);
|
|
1202
|
+
if ("enabled" in data) updateData.enabled = data.enabled;
|
|
1192
1203
|
const shouldUpdateSession = Object.keys(updateData).some((key) => key !== "updated_at");
|
|
1193
1204
|
let updatedRows = null;
|
|
1194
1205
|
if (shouldUpdateSession) {
|
|
@@ -1210,23 +1221,22 @@ var SupabaseStorageBackend = class {
|
|
|
1210
1221
|
}
|
|
1211
1222
|
async patchCredentials(userId, sessionId, data) {
|
|
1212
1223
|
if (!this.hasCredentialData(data)) return;
|
|
1213
|
-
const
|
|
1214
|
-
user_id: userId,
|
|
1215
|
-
session_id: sessionId,
|
|
1224
|
+
const updateData = {
|
|
1216
1225
|
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1217
1226
|
};
|
|
1218
|
-
if ("clientInformation" in data)
|
|
1219
|
-
if ("tokens" in data)
|
|
1220
|
-
if ("codeVerifier" in data)
|
|
1221
|
-
if ("clientId" in data)
|
|
1222
|
-
if ("oauthState" in data)
|
|
1223
|
-
const { error } = await this.supabase.from("
|
|
1227
|
+
if ("clientInformation" in data) updateData.client_information = data.clientInformation == null ? null : encryptObject(data.clientInformation);
|
|
1228
|
+
if ("tokens" in data) updateData.tokens = data.tokens == null ? null : encryptObject(data.tokens);
|
|
1229
|
+
if ("codeVerifier" in data) updateData.code_verifier = data.codeVerifier == null ? null : encryptObject(data.codeVerifier);
|
|
1230
|
+
if ("clientId" in data) updateData.client_id = data.clientId ?? null;
|
|
1231
|
+
if ("oauthState" in data) updateData.oauth_state = data.oauthState ?? null;
|
|
1232
|
+
const { error } = await this.supabase.from("mcp_sessions").update(updateData).eq("user_id", userId).eq("session_id", sessionId);
|
|
1224
1233
|
if (error) {
|
|
1225
1234
|
throw new Error(`Failed to update credentials: ${error.message}`);
|
|
1226
1235
|
}
|
|
1227
1236
|
}
|
|
1228
|
-
async get(userId, sessionId) {
|
|
1229
|
-
const
|
|
1237
|
+
async get(userId, sessionId, options) {
|
|
1238
|
+
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";
|
|
1239
|
+
const { data, error } = await this.supabase.from("mcp_sessions").select(selection).eq("user_id", userId).eq("session_id", sessionId).maybeSingle();
|
|
1230
1240
|
if (error) {
|
|
1231
1241
|
console.error("[SupabaseStorage] Failed to get session:", error);
|
|
1232
1242
|
return null;
|
|
@@ -1235,19 +1245,21 @@ var SupabaseStorageBackend = class {
|
|
|
1235
1245
|
return this.mapRowToSessionData(data);
|
|
1236
1246
|
}
|
|
1237
1247
|
async getCredentials(userId, sessionId) {
|
|
1238
|
-
const { data, error } = await this.supabase.from("
|
|
1248
|
+
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();
|
|
1239
1249
|
if (error) {
|
|
1240
1250
|
console.error("[SupabaseStorage] Failed to get credentials:", error);
|
|
1241
1251
|
return null;
|
|
1242
1252
|
}
|
|
1243
|
-
if (data)
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1253
|
+
if (!data) return null;
|
|
1254
|
+
return {
|
|
1255
|
+
sessionId,
|
|
1256
|
+
userId,
|
|
1257
|
+
clientInformation: decryptObject(data.client_information),
|
|
1258
|
+
tokens: decryptObject(data.tokens),
|
|
1259
|
+
codeVerifier: decryptObject(data.code_verifier),
|
|
1260
|
+
clientId: data.client_id,
|
|
1261
|
+
oauthState: data.oauth_state
|
|
1262
|
+
};
|
|
1251
1263
|
}
|
|
1252
1264
|
async list(userId) {
|
|
1253
1265
|
const { data, error } = await this.supabase.from("mcp_sessions").select("*").eq("user_id", userId);
|
|
@@ -1258,7 +1270,14 @@ var SupabaseStorageBackend = class {
|
|
|
1258
1270
|
return data.map((row) => this.mapRowToSessionData(row));
|
|
1259
1271
|
}
|
|
1260
1272
|
async clearCredentials(userId, sessionId) {
|
|
1261
|
-
const { error } = await this.supabase.from("
|
|
1273
|
+
const { error } = await this.supabase.from("mcp_sessions").update({
|
|
1274
|
+
client_information: null,
|
|
1275
|
+
tokens: null,
|
|
1276
|
+
code_verifier: null,
|
|
1277
|
+
client_id: null,
|
|
1278
|
+
oauth_state: null,
|
|
1279
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1280
|
+
}).eq("user_id", userId).eq("session_id", sessionId);
|
|
1262
1281
|
if (error) {
|
|
1263
1282
|
throw new Error(`Failed to clear credentials: ${error.message}`);
|
|
1264
1283
|
}
|
|
@@ -1286,10 +1305,6 @@ var SupabaseStorageBackend = class {
|
|
|
1286
1305
|
return data.map((row) => row.session_id);
|
|
1287
1306
|
}
|
|
1288
1307
|
async clearAll() {
|
|
1289
|
-
const { error: credentialsError } = await this.supabase.from("mcp_credentials").delete().neq("session_id", "");
|
|
1290
|
-
if (credentialsError) {
|
|
1291
|
-
console.error("[SupabaseStorage] Failed to clear credentials:", credentialsError);
|
|
1292
|
-
}
|
|
1293
1308
|
const { error } = await this.supabase.from("mcp_sessions").delete().neq("session_id", "");
|
|
1294
1309
|
if (error) {
|
|
1295
1310
|
console.error("[SupabaseStorage] Failed to clear sessions:", error);
|
|
@@ -1315,16 +1330,12 @@ var NeonStorageBackend = class {
|
|
|
1315
1330
|
constructor(sql, options = {}) {
|
|
1316
1331
|
this.sql = sql;
|
|
1317
1332
|
__publicField(this, "tableName");
|
|
1318
|
-
__publicField(this, "credentialsTableName");
|
|
1319
1333
|
const schema = options.schema || "public";
|
|
1320
1334
|
const table = options.table || "mcp_sessions";
|
|
1321
|
-
const credentialsTable = options.credentialsTable || "mcp_credentials";
|
|
1322
1335
|
this.tableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(table)}`;
|
|
1323
|
-
this.credentialsTableName = `${this.quoteIdentifier(schema)}.${this.quoteIdentifier(credentialsTable)}`;
|
|
1324
1336
|
}
|
|
1325
1337
|
async init() {
|
|
1326
1338
|
await this.assertTable(this.tableName, "mcp_sessions");
|
|
1327
|
-
await this.assertTable(this.credentialsTableName, "mcp_credentials");
|
|
1328
1339
|
console.log("[mcp-ts][Storage] Neon: storage tables verified.");
|
|
1329
1340
|
}
|
|
1330
1341
|
async assertTable(qualifiedName, displayName) {
|
|
@@ -1362,18 +1373,13 @@ var NeonStorageBackend = class {
|
|
|
1362
1373
|
headers: decryptObject(row.headers),
|
|
1363
1374
|
authUrl: row.auth_url ?? void 0,
|
|
1364
1375
|
status: row.status ?? "pending",
|
|
1365
|
-
toolPolicy: normalizeToolPolicy(row.tool_policy)
|
|
1366
|
-
};
|
|
1367
|
-
}
|
|
1368
|
-
mapRowToCredentials(row, userId, sessionId) {
|
|
1369
|
-
return {
|
|
1370
|
-
sessionId,
|
|
1371
|
-
userId,
|
|
1376
|
+
toolPolicy: normalizeToolPolicy(row.tool_policy),
|
|
1372
1377
|
clientInformation: decryptObject(row.client_information),
|
|
1373
1378
|
tokens: decryptObject(row.tokens),
|
|
1374
1379
|
codeVerifier: decryptObject(row.code_verifier),
|
|
1375
1380
|
clientId: row.client_id ?? void 0,
|
|
1376
|
-
oauthState: row.oauth_state
|
|
1381
|
+
oauthState: row.oauth_state,
|
|
1382
|
+
enabled: row.enabled ?? true
|
|
1377
1383
|
};
|
|
1378
1384
|
}
|
|
1379
1385
|
hasCredentialData(data) {
|
|
@@ -1443,7 +1449,7 @@ var NeonStorageBackend = class {
|
|
|
1443
1449
|
const updatedSession = { ...currentSession, ...data };
|
|
1444
1450
|
const status = updatedSession.status ?? "pending";
|
|
1445
1451
|
const expiresAt = resolveSessionExpiresAt(status);
|
|
1446
|
-
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;
|
|
1452
|
+
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;
|
|
1447
1453
|
if (shouldUpdateSession) {
|
|
1448
1454
|
const setClauses = [];
|
|
1449
1455
|
const values = [];
|
|
@@ -1466,6 +1472,9 @@ var NeonStorageBackend = class {
|
|
|
1466
1472
|
const toolPolicy = normalizeToolPolicy(updatedSession.toolPolicy, policyUpdatedAt) ?? { mode: "all", toolIds: [], updatedAt: policyUpdatedAt };
|
|
1467
1473
|
addSet("tool_policy", toolPolicy);
|
|
1468
1474
|
}
|
|
1475
|
+
if ("enabled" in data) {
|
|
1476
|
+
addSet("enabled", updatedSession.enabled);
|
|
1477
|
+
}
|
|
1469
1478
|
setClauses.push("updated_at = now()");
|
|
1470
1479
|
const updatedRows = await this.sql.query(
|
|
1471
1480
|
`UPDATE ${this.tableName}
|
|
@@ -1481,45 +1490,45 @@ var NeonStorageBackend = class {
|
|
|
1481
1490
|
}
|
|
1482
1491
|
async patchCredentials(userId, sessionId, data) {
|
|
1483
1492
|
if (!this.hasCredentialData(data)) return;
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
"codeVerifier" in data,
|
|
1514
|
-
"clientId" in data,
|
|
1515
|
-
"oauthState" in data
|
|
1516
|
-
]
|
|
1493
|
+
const setClauses = [];
|
|
1494
|
+
const values = [];
|
|
1495
|
+
let paramIndex = 1;
|
|
1496
|
+
const addSet = (column, value) => {
|
|
1497
|
+
setClauses.push(`${column} = $${paramIndex++}`);
|
|
1498
|
+
values.push(value);
|
|
1499
|
+
};
|
|
1500
|
+
if ("clientInformation" in data) {
|
|
1501
|
+
addSet("client_information", data.clientInformation == null ? null : encryptObject(data.clientInformation));
|
|
1502
|
+
}
|
|
1503
|
+
if ("tokens" in data) {
|
|
1504
|
+
addSet("tokens", data.tokens == null ? null : encryptObject(data.tokens));
|
|
1505
|
+
}
|
|
1506
|
+
if ("codeVerifier" in data) {
|
|
1507
|
+
addSet("code_verifier", data.codeVerifier == null ? null : encryptObject(data.codeVerifier));
|
|
1508
|
+
}
|
|
1509
|
+
if ("clientId" in data) {
|
|
1510
|
+
addSet("client_id", data.clientId ?? null);
|
|
1511
|
+
}
|
|
1512
|
+
if ("oauthState" in data) {
|
|
1513
|
+
addSet("oauth_state", data.oauthState ?? null);
|
|
1514
|
+
}
|
|
1515
|
+
setClauses.push("updated_at = now()");
|
|
1516
|
+
const updatedRows = await this.sql.query(
|
|
1517
|
+
`UPDATE ${this.tableName}
|
|
1518
|
+
SET ${setClauses.join(", ")}
|
|
1519
|
+
WHERE user_id = $${paramIndex++} AND session_id = $${paramIndex++}
|
|
1520
|
+
RETURNING id`,
|
|
1521
|
+
[...values, userId, sessionId]
|
|
1517
1522
|
);
|
|
1523
|
+
if (updatedRows.length === 0) {
|
|
1524
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
1525
|
+
}
|
|
1518
1526
|
}
|
|
1519
|
-
async get(userId, sessionId) {
|
|
1527
|
+
async get(userId, sessionId, options) {
|
|
1520
1528
|
try {
|
|
1529
|
+
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";
|
|
1521
1530
|
const rows = await this.sql.query(
|
|
1522
|
-
`SELECT
|
|
1531
|
+
`SELECT ${selection} FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1523
1532
|
[userId, sessionId]
|
|
1524
1533
|
);
|
|
1525
1534
|
if (!rows[0]) return null;
|
|
@@ -1531,18 +1540,22 @@ var NeonStorageBackend = class {
|
|
|
1531
1540
|
}
|
|
1532
1541
|
async getCredentials(userId, sessionId) {
|
|
1533
1542
|
try {
|
|
1534
|
-
const
|
|
1535
|
-
`SELECT
|
|
1536
|
-
|
|
1537
|
-
);
|
|
1538
|
-
if (credentialRows[0]) {
|
|
1539
|
-
return this.mapRowToCredentials(credentialRows[0], userId, sessionId);
|
|
1540
|
-
}
|
|
1541
|
-
const sessionRows = await this.sql.query(
|
|
1542
|
-
`SELECT id FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1543
|
+
const rows = await this.sql.query(
|
|
1544
|
+
`SELECT client_information, tokens, code_verifier, client_id, oauth_state
|
|
1545
|
+
FROM ${this.tableName} WHERE user_id = $1 AND session_id = $2`,
|
|
1543
1546
|
[userId, sessionId]
|
|
1544
1547
|
);
|
|
1545
|
-
|
|
1548
|
+
if (!rows[0]) return null;
|
|
1549
|
+
const row = rows[0];
|
|
1550
|
+
return {
|
|
1551
|
+
sessionId,
|
|
1552
|
+
userId,
|
|
1553
|
+
clientInformation: decryptObject(row.client_information),
|
|
1554
|
+
tokens: decryptObject(row.tokens),
|
|
1555
|
+
codeVerifier: decryptObject(row.code_verifier),
|
|
1556
|
+
clientId: row.client_id ?? void 0,
|
|
1557
|
+
oauthState: row.oauth_state
|
|
1558
|
+
};
|
|
1546
1559
|
} catch (error) {
|
|
1547
1560
|
console.error("[NeonStorage] Failed to get credentials:", error);
|
|
1548
1561
|
return null;
|
|
@@ -1563,7 +1576,9 @@ var NeonStorageBackend = class {
|
|
|
1563
1576
|
async clearCredentials(userId, sessionId) {
|
|
1564
1577
|
try {
|
|
1565
1578
|
await this.sql.query(
|
|
1566
|
-
`
|
|
1579
|
+
`UPDATE ${this.tableName}
|
|
1580
|
+
SET client_information = null, tokens = null, code_verifier = null, client_id = null, oauth_state = null, updated_at = now()
|
|
1581
|
+
WHERE user_id = $1 AND session_id = $2`,
|
|
1567
1582
|
[userId, sessionId]
|
|
1568
1583
|
);
|
|
1569
1584
|
} catch (error) {
|
|
@@ -1605,7 +1620,6 @@ var NeonStorageBackend = class {
|
|
|
1605
1620
|
}
|
|
1606
1621
|
async clearAll() {
|
|
1607
1622
|
try {
|
|
1608
|
-
await this.sql.query(`DELETE FROM ${this.credentialsTableName}`);
|
|
1609
1623
|
await this.sql.query(`DELETE FROM ${this.tableName}`);
|
|
1610
1624
|
} catch (error) {
|
|
1611
1625
|
console.error("[NeonStorage] Failed to clear sessions:", error);
|
|
@@ -1847,6 +1861,60 @@ function onSessionMutation(listener) {
|
|
|
1847
1861
|
sessionMutationListeners.delete(listener);
|
|
1848
1862
|
};
|
|
1849
1863
|
}
|
|
1864
|
+
function withDbObservability(store, emit) {
|
|
1865
|
+
const readMethods = /* @__PURE__ */ new Set([
|
|
1866
|
+
"get",
|
|
1867
|
+
"getCredentials",
|
|
1868
|
+
"list",
|
|
1869
|
+
"listIds",
|
|
1870
|
+
"listAllIds"
|
|
1871
|
+
]);
|
|
1872
|
+
return new Proxy(store, {
|
|
1873
|
+
get(target, prop, receiver) {
|
|
1874
|
+
const original = Reflect.get(target, prop, receiver);
|
|
1875
|
+
if (typeof original !== "function") return original;
|
|
1876
|
+
return (...args) => {
|
|
1877
|
+
const method = prop;
|
|
1878
|
+
const isRead = readMethods.has(prop);
|
|
1879
|
+
const start = performance.now();
|
|
1880
|
+
const emitEvent = (error) => {
|
|
1881
|
+
emit({
|
|
1882
|
+
type: isRead ? "db:read" : "db:write",
|
|
1883
|
+
level: error ? "error" : "debug",
|
|
1884
|
+
message: `${method}(${args.map(
|
|
1885
|
+
(a) => typeof a === "string" ? a.slice(0, 24) : typeof a
|
|
1886
|
+
).join(", ")})`,
|
|
1887
|
+
sessionId: typeof args[1] === "string" ? args[1] : void 0,
|
|
1888
|
+
payload: {
|
|
1889
|
+
method,
|
|
1890
|
+
argTypes: args.map((a) => typeof a),
|
|
1891
|
+
durationMs: performance.now() - start,
|
|
1892
|
+
...error ? { error } : {}
|
|
1893
|
+
},
|
|
1894
|
+
timestamp: Date.now()
|
|
1895
|
+
});
|
|
1896
|
+
};
|
|
1897
|
+
try {
|
|
1898
|
+
const result = original.apply(target, args);
|
|
1899
|
+
if (result instanceof Promise) {
|
|
1900
|
+
return result.then((r) => {
|
|
1901
|
+
emitEvent();
|
|
1902
|
+
return r;
|
|
1903
|
+
}).catch((e) => {
|
|
1904
|
+
emitEvent(String(e));
|
|
1905
|
+
throw e;
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
emitEvent();
|
|
1909
|
+
return result;
|
|
1910
|
+
} catch (e) {
|
|
1911
|
+
emitEvent(String(e));
|
|
1912
|
+
throw e;
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1850
1918
|
var sessions = new Proxy({}, {
|
|
1851
1919
|
get(_target, prop) {
|
|
1852
1920
|
return async (...args) => {
|
|
@@ -1866,6 +1934,24 @@ var sessions = new Proxy({}, {
|
|
|
1866
1934
|
});
|
|
1867
1935
|
|
|
1868
1936
|
// src/server/mcp/storage-oauth-provider.ts
|
|
1937
|
+
var codeVerifierContext = new AsyncLocalStorage();
|
|
1938
|
+
function runWithCodeVerifierState(verifier, method, fn) {
|
|
1939
|
+
return codeVerifierContext.run({ verifier, method }, fn);
|
|
1940
|
+
}
|
|
1941
|
+
function base64UrlEncode(bytes) {
|
|
1942
|
+
let binary = "";
|
|
1943
|
+
for (const byte of bytes) {
|
|
1944
|
+
binary += String.fromCharCode(byte);
|
|
1945
|
+
}
|
|
1946
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1947
|
+
}
|
|
1948
|
+
async function createCodeChallenge(verifier) {
|
|
1949
|
+
const digest = await crypto.subtle.digest(
|
|
1950
|
+
"SHA-256",
|
|
1951
|
+
new TextEncoder().encode(verifier)
|
|
1952
|
+
);
|
|
1953
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
1954
|
+
}
|
|
1869
1955
|
var StorageOAuthClientProvider = class {
|
|
1870
1956
|
/**
|
|
1871
1957
|
* Creates a new session-backed OAuth provider
|
|
@@ -1881,8 +1967,12 @@ var StorageOAuthClientProvider = class {
|
|
|
1881
1967
|
__publicField(this, "logoUri");
|
|
1882
1968
|
__publicField(this, "policyUri");
|
|
1883
1969
|
__publicField(this, "clientSecret");
|
|
1970
|
+
__publicField(this, "_store");
|
|
1884
1971
|
__publicField(this, "_authUrl");
|
|
1885
1972
|
__publicField(this, "_clientId");
|
|
1973
|
+
__publicField(this, "_cachedCodeVerifier");
|
|
1974
|
+
__publicField(this, "_hasCodeVerifier", false);
|
|
1975
|
+
__publicField(this, "_cachedTokens");
|
|
1886
1976
|
__publicField(this, "onRedirectCallback");
|
|
1887
1977
|
this.userId = options.userId;
|
|
1888
1978
|
this.serverId = options.serverId;
|
|
@@ -1894,6 +1984,8 @@ var StorageOAuthClientProvider = class {
|
|
|
1894
1984
|
this.policyUri = options.policyUri;
|
|
1895
1985
|
this._clientId = options.clientId;
|
|
1896
1986
|
this.clientSecret = options.clientSecret;
|
|
1987
|
+
this._cachedTokens = options.cachedTokens;
|
|
1988
|
+
this._store = options.sessionStore ?? sessions;
|
|
1897
1989
|
this.onRedirectCallback = options.onRedirect;
|
|
1898
1990
|
}
|
|
1899
1991
|
get clientMetadata() {
|
|
@@ -1921,7 +2013,7 @@ var StorageOAuthClientProvider = class {
|
|
|
1921
2013
|
* @private
|
|
1922
2014
|
*/
|
|
1923
2015
|
async getCredentials() {
|
|
1924
|
-
const data = await
|
|
2016
|
+
const data = await this._store.getCredentials(this.userId, this.sessionId);
|
|
1925
2017
|
if (!data) {
|
|
1926
2018
|
return { userId: this.userId, sessionId: this.sessionId };
|
|
1927
2019
|
}
|
|
@@ -1934,35 +2026,33 @@ var StorageOAuthClientProvider = class {
|
|
|
1934
2026
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
1935
2027
|
*/
|
|
1936
2028
|
async patchCredentials(data) {
|
|
1937
|
-
await
|
|
2029
|
+
await this._store.patchCredentials(this.userId, this.sessionId, data);
|
|
1938
2030
|
}
|
|
1939
2031
|
/**
|
|
1940
|
-
* Retrieves stored OAuth client information
|
|
2032
|
+
* Retrieves stored OAuth client information.
|
|
1941
2033
|
*/
|
|
1942
2034
|
async clientInformation() {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
await this.patchCredentials({
|
|
1949
|
-
clientId: this._clientId,
|
|
1950
|
-
clientInformation: {
|
|
1951
|
-
client_id: this._clientId,
|
|
1952
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
1953
|
-
}
|
|
1954
|
-
});
|
|
2035
|
+
if (this._clientId) {
|
|
2036
|
+
return {
|
|
2037
|
+
client_id: this._clientId,
|
|
2038
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2039
|
+
};
|
|
1955
2040
|
}
|
|
1956
|
-
|
|
1957
|
-
|
|
2041
|
+
const data = await this.getCredentials();
|
|
2042
|
+
if (this._cachedTokens === void 0) {
|
|
2043
|
+
this._cachedTokens = data.tokens ?? null;
|
|
1958
2044
|
}
|
|
1959
|
-
if (
|
|
1960
|
-
|
|
2045
|
+
if (data.clientId) {
|
|
2046
|
+
this._clientId = data.clientId;
|
|
2047
|
+
if (data.clientInformation) {
|
|
2048
|
+
return data.clientInformation;
|
|
2049
|
+
}
|
|
2050
|
+
return {
|
|
2051
|
+
client_id: data.clientId,
|
|
2052
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2053
|
+
};
|
|
1961
2054
|
}
|
|
1962
|
-
return
|
|
1963
|
-
client_id: this._clientId,
|
|
1964
|
-
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
1965
|
-
};
|
|
2055
|
+
return void 0;
|
|
1966
2056
|
}
|
|
1967
2057
|
/**
|
|
1968
2058
|
* Stores OAuth client information
|
|
@@ -1979,11 +2069,30 @@ var StorageOAuthClientProvider = class {
|
|
|
1979
2069
|
*/
|
|
1980
2070
|
async saveTokens(tokens) {
|
|
1981
2071
|
await this.patchCredentials({ tokens });
|
|
2072
|
+
this._cachedTokens = tokens;
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* Persists static client credentials to DB immediately on creation.
|
|
2076
|
+
* Ensures getOrCreateClient() finds them on rehydration, even when the
|
|
2077
|
+
* server allows anonymous connect and never triggers DCR.
|
|
2078
|
+
*/
|
|
2079
|
+
async initializeCredentials() {
|
|
2080
|
+
if (this._clientId) {
|
|
2081
|
+
await this.patchCredentials({
|
|
2082
|
+
clientId: this._clientId,
|
|
2083
|
+
clientInformation: {
|
|
2084
|
+
client_id: this._clientId,
|
|
2085
|
+
...this.clientSecret ? { client_secret: this.clientSecret } : {}
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
1982
2089
|
}
|
|
1983
2090
|
get authUrl() {
|
|
1984
2091
|
return this._authUrl;
|
|
1985
2092
|
}
|
|
1986
2093
|
async state() {
|
|
2094
|
+
this._cachedCodeVerifier = void 0;
|
|
2095
|
+
this._hasCodeVerifier = false;
|
|
1987
2096
|
const nonce = nanoid(32);
|
|
1988
2097
|
await this.patchCredentials({
|
|
1989
2098
|
oauthState: {
|
|
@@ -2004,7 +2113,7 @@ var StorageOAuthClientProvider = class {
|
|
|
2004
2113
|
if (parsed.sessionId !== this.sessionId) {
|
|
2005
2114
|
return { valid: false, error: "OAuth state mismatch" };
|
|
2006
2115
|
}
|
|
2007
|
-
const data = await
|
|
2116
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
2008
2117
|
if (!data) {
|
|
2009
2118
|
return { valid: false, error: "Session not found" };
|
|
2010
2119
|
}
|
|
@@ -2029,49 +2138,77 @@ var StorageOAuthClientProvider = class {
|
|
|
2029
2138
|
}
|
|
2030
2139
|
async redirectToAuthorization(authUrl) {
|
|
2031
2140
|
this._authUrl = authUrl.toString();
|
|
2032
|
-
|
|
2141
|
+
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
|
2142
|
+
const state = authUrl.searchParams.get("state");
|
|
2143
|
+
if (this._cachedCodeVerifier && codeChallenge && state) {
|
|
2144
|
+
const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
|
|
2145
|
+
if (expectedChallenge === codeChallenge) {
|
|
2146
|
+
const parsed = parseOAuthState(state);
|
|
2147
|
+
if (parsed) {
|
|
2148
|
+
await this.patchCredentials({
|
|
2149
|
+
codeVerifier: this._cachedCodeVerifier
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2033
2154
|
if (this.onRedirectCallback) {
|
|
2034
2155
|
this.onRedirectCallback(authUrl.toString());
|
|
2035
2156
|
}
|
|
2036
2157
|
}
|
|
2037
2158
|
async invalidateCredentials(scope) {
|
|
2038
2159
|
if (scope === "all") {
|
|
2039
|
-
|
|
2160
|
+
this._cachedTokens = void 0;
|
|
2161
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
2040
2162
|
} else {
|
|
2041
2163
|
const updates = {};
|
|
2042
2164
|
if (scope === "client") {
|
|
2043
2165
|
updates.clientInformation = null;
|
|
2044
2166
|
updates.clientId = null;
|
|
2045
2167
|
} else if (scope === "tokens") {
|
|
2168
|
+
this._cachedTokens = void 0;
|
|
2046
2169
|
updates.tokens = null;
|
|
2047
2170
|
} else if (scope === "verifier") {
|
|
2171
|
+
this._cachedCodeVerifier = void 0;
|
|
2172
|
+
this._hasCodeVerifier = false;
|
|
2048
2173
|
updates.codeVerifier = null;
|
|
2049
2174
|
}
|
|
2050
2175
|
await this.patchCredentials(updates);
|
|
2051
2176
|
}
|
|
2052
2177
|
}
|
|
2053
2178
|
async saveCodeVerifier(verifier) {
|
|
2054
|
-
|
|
2055
|
-
if (data.codeVerifier) {
|
|
2179
|
+
if (this._hasCodeVerifier) {
|
|
2056
2180
|
return;
|
|
2057
2181
|
}
|
|
2058
|
-
|
|
2182
|
+
this._cachedCodeVerifier = verifier;
|
|
2183
|
+
this._hasCodeVerifier = true;
|
|
2059
2184
|
}
|
|
2060
2185
|
async codeVerifier() {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
this._clientId = data.clientId;
|
|
2186
|
+
if (this._cachedCodeVerifier) {
|
|
2187
|
+
return this._cachedCodeVerifier;
|
|
2064
2188
|
}
|
|
2065
|
-
|
|
2066
|
-
|
|
2189
|
+
const ctx = codeVerifierContext.getStore();
|
|
2190
|
+
if (ctx?.verifier) {
|
|
2191
|
+
return ctx.verifier;
|
|
2192
|
+
}
|
|
2193
|
+
const data = await this.getCredentials();
|
|
2194
|
+
if (data.codeVerifier) {
|
|
2195
|
+
return data.codeVerifier;
|
|
2067
2196
|
}
|
|
2068
|
-
|
|
2197
|
+
throw new Error("No code verifier found");
|
|
2069
2198
|
}
|
|
2070
2199
|
async deleteCodeVerifier() {
|
|
2071
|
-
|
|
2200
|
+
this._cachedCodeVerifier = void 0;
|
|
2201
|
+
this._hasCodeVerifier = false;
|
|
2202
|
+
await this.patchCredentials({
|
|
2203
|
+
codeVerifier: null
|
|
2204
|
+
});
|
|
2072
2205
|
}
|
|
2073
2206
|
async tokens() {
|
|
2207
|
+
if (this._cachedTokens !== void 0) {
|
|
2208
|
+
return this._cachedTokens ?? void 0;
|
|
2209
|
+
}
|
|
2074
2210
|
const data = await this.getCredentials();
|
|
2211
|
+
this._cachedTokens = data.tokens ?? null;
|
|
2075
2212
|
if (data.clientId && !this._clientId) {
|
|
2076
2213
|
this._clientId = data.clientId;
|
|
2077
2214
|
}
|
|
@@ -2232,23 +2369,10 @@ var MCPClient = class {
|
|
|
2232
2369
|
__publicField(this, "client");
|
|
2233
2370
|
__publicField(this, "oauthProvider", null);
|
|
2234
2371
|
__publicField(this, "transport", null);
|
|
2235
|
-
__publicField(this, "
|
|
2236
|
-
__publicField(this, "serverId");
|
|
2237
|
-
__publicField(this, "sessionId");
|
|
2238
|
-
__publicField(this, "serverName");
|
|
2239
|
-
__publicField(this, "transportType");
|
|
2240
|
-
__publicField(this, "serverUrl");
|
|
2241
|
-
__publicField(this, "callbackUrl");
|
|
2242
|
-
__publicField(this, "onRedirect");
|
|
2243
|
-
__publicField(this, "clientId");
|
|
2244
|
-
__publicField(this, "clientSecret");
|
|
2245
|
-
__publicField(this, "headers");
|
|
2246
|
-
/** OAuth Client Metadata */
|
|
2247
|
-
__publicField(this, "clientName");
|
|
2248
|
-
__publicField(this, "clientUri");
|
|
2249
|
-
__publicField(this, "logoUri");
|
|
2250
|
-
__publicField(this, "policyUri");
|
|
2372
|
+
__publicField(this, "config");
|
|
2251
2373
|
__publicField(this, "createdAt");
|
|
2374
|
+
__publicField(this, "_serverInfo");
|
|
2375
|
+
__publicField(this, "_store");
|
|
2252
2376
|
/** Event emitters for connection lifecycle */
|
|
2253
2377
|
__publicField(this, "_onConnectionEvent", new Emitter());
|
|
2254
2378
|
__publicField(this, "onConnectionEvent", this._onConnectionEvent.event);
|
|
@@ -2264,21 +2388,8 @@ var MCPClient = class {
|
|
|
2264
2388
|
* the memory when the client is no longer needed.
|
|
2265
2389
|
*/
|
|
2266
2390
|
__publicField(this, "cachedTools", null);
|
|
2267
|
-
this.
|
|
2268
|
-
this.
|
|
2269
|
-
this.callbackUrl = options.callbackUrl;
|
|
2270
|
-
this.onRedirect = options.onRedirect;
|
|
2271
|
-
this.userId = options.userId;
|
|
2272
|
-
this.serverId = options.serverId;
|
|
2273
|
-
this.sessionId = options.sessionId;
|
|
2274
|
-
this.transportType = options.transportType;
|
|
2275
|
-
this.clientId = options.clientId;
|
|
2276
|
-
this.clientSecret = options.clientSecret;
|
|
2277
|
-
this.headers = options.headers;
|
|
2278
|
-
this.clientName = options.clientName;
|
|
2279
|
-
this.clientUri = options.clientUri;
|
|
2280
|
-
this.logoUri = options.logoUri;
|
|
2281
|
-
this.policyUri = options.policyUri;
|
|
2391
|
+
this.config = { ...options };
|
|
2392
|
+
this._store = options.sessionStore ?? sessions;
|
|
2282
2393
|
this.client = new Client(
|
|
2283
2394
|
{
|
|
2284
2395
|
name: MCP_CLIENT_NAME,
|
|
@@ -2295,6 +2406,21 @@ var MCPClient = class {
|
|
|
2295
2406
|
}
|
|
2296
2407
|
);
|
|
2297
2408
|
}
|
|
2409
|
+
/** Shared session-shaped data for ensureSession and saveSession */
|
|
2410
|
+
get session() {
|
|
2411
|
+
return {
|
|
2412
|
+
sessionId: this.config.sessionId,
|
|
2413
|
+
userId: this.config.userId,
|
|
2414
|
+
serverId: this.config.serverId,
|
|
2415
|
+
serverName: this.config.serverName,
|
|
2416
|
+
serverUrl: this.config.serverUrl,
|
|
2417
|
+
callbackUrl: this.config.callbackUrl,
|
|
2418
|
+
transportType: this.config.transportType || "streamable-http",
|
|
2419
|
+
headers: this.config.headers,
|
|
2420
|
+
createdAt: this.createdAt ?? Date.now(),
|
|
2421
|
+
updatedAt: Date.now()
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2298
2424
|
/**
|
|
2299
2425
|
* Emit a connection state change event
|
|
2300
2426
|
* @private
|
|
@@ -2302,13 +2428,13 @@ var MCPClient = class {
|
|
|
2302
2428
|
emitStateChange(newState) {
|
|
2303
2429
|
const previousState = this.currentState;
|
|
2304
2430
|
this.currentState = newState;
|
|
2305
|
-
if (!this.serverId) return;
|
|
2431
|
+
if (!this.config.serverId) return;
|
|
2306
2432
|
this._onConnectionEvent.fire({
|
|
2307
2433
|
type: "state_changed",
|
|
2308
|
-
sessionId: this.sessionId,
|
|
2309
|
-
serverId: this.serverId,
|
|
2310
|
-
serverName: this.serverName || this.serverId,
|
|
2311
|
-
serverUrl: this.serverUrl || "",
|
|
2434
|
+
sessionId: this.config.sessionId,
|
|
2435
|
+
serverId: this.config.serverId,
|
|
2436
|
+
serverName: this.config.serverName || this.config.serverId,
|
|
2437
|
+
serverUrl: this.config.serverUrl || "",
|
|
2312
2438
|
createdAt: this.createdAt,
|
|
2313
2439
|
state: newState,
|
|
2314
2440
|
previousState,
|
|
@@ -2319,8 +2445,8 @@ var MCPClient = class {
|
|
|
2319
2445
|
level: "info",
|
|
2320
2446
|
message: `Connection state: ${previousState} \u2192 ${newState}`,
|
|
2321
2447
|
displayMessage: `State changed to ${newState}`,
|
|
2322
|
-
sessionId: this.sessionId,
|
|
2323
|
-
serverId: this.serverId,
|
|
2448
|
+
sessionId: this.config.sessionId,
|
|
2449
|
+
serverId: this.config.serverId,
|
|
2324
2450
|
payload: { previousState, newState },
|
|
2325
2451
|
timestamp: Date.now(),
|
|
2326
2452
|
id: nanoid()
|
|
@@ -2331,11 +2457,11 @@ var MCPClient = class {
|
|
|
2331
2457
|
* @private
|
|
2332
2458
|
*/
|
|
2333
2459
|
emitError(error, errorType = "unknown") {
|
|
2334
|
-
if (!this.serverId) return;
|
|
2460
|
+
if (!this.config.serverId) return;
|
|
2335
2461
|
this._onConnectionEvent.fire({
|
|
2336
2462
|
type: "error",
|
|
2337
|
-
sessionId: this.sessionId,
|
|
2338
|
-
serverId: this.serverId,
|
|
2463
|
+
sessionId: this.config.sessionId,
|
|
2464
|
+
serverId: this.config.serverId,
|
|
2339
2465
|
error,
|
|
2340
2466
|
errorType,
|
|
2341
2467
|
timestamp: Date.now()
|
|
@@ -2345,8 +2471,8 @@ var MCPClient = class {
|
|
|
2345
2471
|
level: "error",
|
|
2346
2472
|
message: error,
|
|
2347
2473
|
displayMessage: error,
|
|
2348
|
-
sessionId: this.sessionId,
|
|
2349
|
-
serverId: this.serverId,
|
|
2474
|
+
sessionId: this.config.sessionId,
|
|
2475
|
+
serverId: this.config.serverId,
|
|
2350
2476
|
payload: { errorType, error },
|
|
2351
2477
|
timestamp: Date.now(),
|
|
2352
2478
|
id: nanoid()
|
|
@@ -2357,11 +2483,11 @@ var MCPClient = class {
|
|
|
2357
2483
|
* @private
|
|
2358
2484
|
*/
|
|
2359
2485
|
emitProgress(message) {
|
|
2360
|
-
if (!this.serverId) return;
|
|
2486
|
+
if (!this.config.serverId) return;
|
|
2361
2487
|
this._onConnectionEvent.fire({
|
|
2362
2488
|
type: "progress",
|
|
2363
|
-
sessionId: this.sessionId,
|
|
2364
|
-
serverId: this.serverId,
|
|
2489
|
+
sessionId: this.config.sessionId,
|
|
2490
|
+
serverId: this.config.serverId,
|
|
2365
2491
|
message,
|
|
2366
2492
|
timestamp: Date.now()
|
|
2367
2493
|
});
|
|
@@ -2379,14 +2505,14 @@ var MCPClient = class {
|
|
|
2379
2505
|
* @private
|
|
2380
2506
|
*/
|
|
2381
2507
|
getTransport(type) {
|
|
2382
|
-
if (!this.serverUrl) {
|
|
2508
|
+
if (!this.config.serverUrl) {
|
|
2383
2509
|
throw new Error("Server URL is required to create transport");
|
|
2384
2510
|
}
|
|
2385
|
-
const baseUrl = new URL(this.serverUrl);
|
|
2386
|
-
const hasAuthorizationHeader = Object.keys(this.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2511
|
+
const baseUrl = new URL(this.config.serverUrl);
|
|
2512
|
+
const hasAuthorizationHeader = Object.keys(this.config.headers || {}).some((key) => key.toLowerCase() === "authorization");
|
|
2387
2513
|
const transportOptions = {
|
|
2388
2514
|
...!hasAuthorizationHeader && { authProvider: this.oauthProvider },
|
|
2389
|
-
...this.headers && { requestInit: { headers: this.headers } },
|
|
2515
|
+
...this.config.headers && { requestInit: { headers: this.config.headers } },
|
|
2390
2516
|
/**
|
|
2391
2517
|
* Custom fetch implementation to handle connection timeouts.
|
|
2392
2518
|
* Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
|
|
@@ -2424,78 +2550,74 @@ var MCPClient = class {
|
|
|
2424
2550
|
* @private
|
|
2425
2551
|
*/
|
|
2426
2552
|
async ensureSession() {
|
|
2427
|
-
if (this.oauthProvider)
|
|
2428
|
-
return;
|
|
2429
|
-
}
|
|
2553
|
+
if (this.oauthProvider) return;
|
|
2430
2554
|
this.emitStateChange("INITIALIZING");
|
|
2431
2555
|
this.emitProgress("Loading session configuration...");
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
throw new Error(`Session not found: ${this.sessionId}`);
|
|
2556
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2557
|
+
const existingSession2 = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2558
|
+
if (!existingSession2) {
|
|
2559
|
+
throw new Error(`Session not found: ${this.config.sessionId}`);
|
|
2437
2560
|
}
|
|
2438
|
-
this.serverUrl = this.serverUrl ||
|
|
2439
|
-
this.callbackUrl = this.callbackUrl ||
|
|
2440
|
-
this.serverName = this.serverName ||
|
|
2441
|
-
this.serverId = this.serverId ||
|
|
2442
|
-
this.headers = this.headers ||
|
|
2443
|
-
this.createdAt =
|
|
2444
|
-
}
|
|
2445
|
-
if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
|
|
2561
|
+
this.config.serverUrl = this.config.serverUrl || existingSession2.serverUrl;
|
|
2562
|
+
this.config.callbackUrl = this.config.callbackUrl || existingSession2.callbackUrl;
|
|
2563
|
+
this.config.serverName = this.config.serverName || existingSession2.serverName;
|
|
2564
|
+
this.config.serverId = this.config.serverId || existingSession2.serverId || "unknown";
|
|
2565
|
+
this.config.headers = this.config.headers || existingSession2.headers;
|
|
2566
|
+
this.createdAt = existingSession2.createdAt;
|
|
2567
|
+
}
|
|
2568
|
+
if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
|
|
2446
2569
|
throw new Error("Missing required connection metadata");
|
|
2447
2570
|
}
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
this.
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
this.
|
|
2466
|
-
|
|
2571
|
+
this.oauthProvider = new StorageOAuthClientProvider({
|
|
2572
|
+
userId: this.config.userId,
|
|
2573
|
+
serverId: this.config.serverId,
|
|
2574
|
+
sessionId: this.config.sessionId,
|
|
2575
|
+
redirectUrl: this.config.callbackUrl,
|
|
2576
|
+
clientName: this.config.clientName,
|
|
2577
|
+
clientUri: this.config.clientUri,
|
|
2578
|
+
logoUri: this.config.logoUri,
|
|
2579
|
+
policyUri: this.config.policyUri,
|
|
2580
|
+
clientId: this.config.clientId,
|
|
2581
|
+
clientSecret: this.config.clientSecret,
|
|
2582
|
+
cachedTokens: this.config.cachedCredentials?.tokens,
|
|
2583
|
+
sessionStore: this._store,
|
|
2584
|
+
onRedirect: (redirectUrl) => {
|
|
2585
|
+
if (this.config.serverId) {
|
|
2586
|
+
this._onConnectionEvent.fire({
|
|
2587
|
+
type: "auth_required",
|
|
2588
|
+
sessionId: this.config.sessionId,
|
|
2589
|
+
serverId: this.config.serverId,
|
|
2590
|
+
authUrl: redirectUrl,
|
|
2591
|
+
timestamp: Date.now()
|
|
2592
|
+
});
|
|
2467
2593
|
}
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
}
|
|
2473
|
-
|
|
2594
|
+
if (this.config.onRedirect) {
|
|
2595
|
+
this.config.onRedirect(redirectUrl);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
});
|
|
2599
|
+
const existingSession = this.config.hasSession ? {} : await this._store.get(this.config.userId, this.config.sessionId);
|
|
2600
|
+
if (!existingSession) {
|
|
2474
2601
|
this.createdAt = Date.now();
|
|
2475
2602
|
const updatedAt = this.createdAt;
|
|
2476
2603
|
this._onObservabilityEvent.fire({
|
|
2477
2604
|
type: "mcp:client:session_created",
|
|
2478
2605
|
level: "info",
|
|
2479
|
-
message: `Creating pending session ${this.sessionId} for connection setup`,
|
|
2480
|
-
sessionId: this.sessionId,
|
|
2481
|
-
serverId: this.serverId,
|
|
2606
|
+
message: `Creating pending session ${this.config.sessionId} for connection setup`,
|
|
2607
|
+
sessionId: this.config.sessionId,
|
|
2608
|
+
serverId: this.config.serverId,
|
|
2482
2609
|
timestamp: Date.now(),
|
|
2483
2610
|
id: nanoid()
|
|
2484
2611
|
});
|
|
2485
|
-
await
|
|
2486
|
-
|
|
2487
|
-
userId: this.userId,
|
|
2488
|
-
serverId: this.serverId,
|
|
2489
|
-
serverName: this.serverName,
|
|
2490
|
-
serverUrl: this.serverUrl,
|
|
2491
|
-
callbackUrl: this.callbackUrl,
|
|
2492
|
-
transportType: this.transportType || "streamable-http",
|
|
2493
|
-
headers: this.headers,
|
|
2494
|
-
createdAt: this.createdAt,
|
|
2612
|
+
await this._store.create({
|
|
2613
|
+
...this.session,
|
|
2495
2614
|
updatedAt,
|
|
2496
2615
|
status: "pending"
|
|
2497
2616
|
});
|
|
2498
2617
|
}
|
|
2618
|
+
if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
|
|
2619
|
+
await this.oauthProvider.initializeCredentials();
|
|
2620
|
+
}
|
|
2499
2621
|
}
|
|
2500
2622
|
/**
|
|
2501
2623
|
* Saves current session state to the session store
|
|
@@ -2503,34 +2625,14 @@ var MCPClient = class {
|
|
|
2503
2625
|
* @param status - Session lifecycle status used by storage cleanup
|
|
2504
2626
|
* @private
|
|
2505
2627
|
*/
|
|
2506
|
-
async saveSession(status = "active"
|
|
2507
|
-
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
2628
|
+
async saveSession(status = "active") {
|
|
2629
|
+
if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
|
|
2508
2630
|
return;
|
|
2509
2631
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
userId: this.userId,
|
|
2513
|
-
serverId: this.serverId,
|
|
2514
|
-
serverName: this.serverName,
|
|
2515
|
-
serverUrl: this.serverUrl,
|
|
2516
|
-
callbackUrl: this.callbackUrl,
|
|
2517
|
-
transportType: this.transportType || "streamable-http",
|
|
2518
|
-
headers: this.headers,
|
|
2519
|
-
createdAt: this.createdAt || Date.now(),
|
|
2520
|
-
updatedAt: Date.now(),
|
|
2632
|
+
await this._store.update(this.config.userId, this.config.sessionId, {
|
|
2633
|
+
...this.session,
|
|
2521
2634
|
status
|
|
2522
|
-
};
|
|
2523
|
-
if (status === "active") {
|
|
2524
|
-
sessionData.authUrl = null;
|
|
2525
|
-
}
|
|
2526
|
-
if (existingSession === void 0) {
|
|
2527
|
-
existingSession = await sessions.get(this.userId, this.sessionId);
|
|
2528
|
-
}
|
|
2529
|
-
if (existingSession) {
|
|
2530
|
-
await sessions.update(this.userId, this.sessionId, sessionData);
|
|
2531
|
-
} else {
|
|
2532
|
-
await sessions.create(sessionData);
|
|
2533
|
-
}
|
|
2635
|
+
});
|
|
2534
2636
|
}
|
|
2535
2637
|
/**
|
|
2536
2638
|
* Removes transient setup/auth sessions without masking the original error.
|
|
@@ -2538,7 +2640,7 @@ var MCPClient = class {
|
|
|
2538
2640
|
*/
|
|
2539
2641
|
async deleteTransientSession() {
|
|
2540
2642
|
try {
|
|
2541
|
-
await
|
|
2643
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2542
2644
|
} catch {
|
|
2543
2645
|
}
|
|
2544
2646
|
}
|
|
@@ -2548,7 +2650,7 @@ var MCPClient = class {
|
|
|
2548
2650
|
* @private
|
|
2549
2651
|
*/
|
|
2550
2652
|
async tryConnect() {
|
|
2551
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2653
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2552
2654
|
let lastError;
|
|
2553
2655
|
for (const currentType of transportsToTry) {
|
|
2554
2656
|
const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
|
|
@@ -2556,6 +2658,7 @@ var MCPClient = class {
|
|
|
2556
2658
|
const transport = this.getTransport(currentType);
|
|
2557
2659
|
this.transport = transport;
|
|
2558
2660
|
await this.client.connect(transport);
|
|
2661
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2559
2662
|
return { transportType: currentType };
|
|
2560
2663
|
} catch (error) {
|
|
2561
2664
|
lastError = error;
|
|
@@ -2571,8 +2674,8 @@ var MCPClient = class {
|
|
|
2571
2674
|
this._onObservabilityEvent.fire({
|
|
2572
2675
|
level: "warn",
|
|
2573
2676
|
message: `Transport ${currentType} failed, falling back`,
|
|
2574
|
-
sessionId: this.sessionId,
|
|
2575
|
-
serverId: this.serverId,
|
|
2677
|
+
sessionId: this.config.sessionId,
|
|
2678
|
+
serverId: this.config.serverId,
|
|
2576
2679
|
metadata: {
|
|
2577
2680
|
failedTransport: currentType,
|
|
2578
2681
|
error: errorMessage
|
|
@@ -2615,15 +2718,15 @@ var MCPClient = class {
|
|
|
2615
2718
|
try {
|
|
2616
2719
|
this.emitStateChange("CONNECTING");
|
|
2617
2720
|
const { transportType } = await this.tryConnect();
|
|
2618
|
-
this.transportType = transportType;
|
|
2721
|
+
this.config.transportType = transportType;
|
|
2619
2722
|
this.emitStateChange("CONNECTED");
|
|
2620
2723
|
this.emitProgress("Connected successfully");
|
|
2621
2724
|
this._onObservabilityEvent.fire({
|
|
2622
2725
|
type: "mcp:client:session_saved",
|
|
2623
2726
|
level: "info",
|
|
2624
|
-
message: `Saving active session ${this.sessionId} (connect success)`,
|
|
2625
|
-
sessionId: this.sessionId,
|
|
2626
|
-
serverId: this.serverId,
|
|
2727
|
+
message: `Saving active session ${this.config.sessionId} (connect success)`,
|
|
2728
|
+
sessionId: this.config.sessionId,
|
|
2729
|
+
serverId: this.config.serverId,
|
|
2627
2730
|
timestamp: Date.now(),
|
|
2628
2731
|
id: nanoid()
|
|
2629
2732
|
});
|
|
@@ -2646,23 +2749,23 @@ var MCPClient = class {
|
|
|
2646
2749
|
this._onObservabilityEvent.fire({
|
|
2647
2750
|
type: "mcp:client:session_saved",
|
|
2648
2751
|
level: "info",
|
|
2649
|
-
message: `Saving pending OAuth session ${this.sessionId}`,
|
|
2650
|
-
sessionId: this.sessionId,
|
|
2651
|
-
serverId: this.serverId,
|
|
2752
|
+
message: `Saving pending OAuth session ${this.config.sessionId}`,
|
|
2753
|
+
sessionId: this.config.sessionId,
|
|
2754
|
+
serverId: this.config.serverId,
|
|
2652
2755
|
timestamp: Date.now(),
|
|
2653
2756
|
id: nanoid()
|
|
2654
2757
|
});
|
|
2655
2758
|
await this.saveSession("pending");
|
|
2656
|
-
if (this.serverId) {
|
|
2759
|
+
if (this.config.serverId) {
|
|
2657
2760
|
this._onConnectionEvent.fire({
|
|
2658
2761
|
type: "auth_required",
|
|
2659
|
-
sessionId: this.sessionId,
|
|
2660
|
-
serverId: this.serverId,
|
|
2762
|
+
sessionId: this.config.sessionId,
|
|
2763
|
+
serverId: this.config.serverId,
|
|
2661
2764
|
authUrl,
|
|
2662
2765
|
timestamp: Date.now()
|
|
2663
2766
|
});
|
|
2664
|
-
if (authUrl && this.onRedirect) {
|
|
2665
|
-
this.onRedirect(authUrl);
|
|
2767
|
+
if (authUrl && this.config.onRedirect) {
|
|
2768
|
+
this.config.onRedirect(authUrl);
|
|
2666
2769
|
}
|
|
2667
2770
|
}
|
|
2668
2771
|
throw new UnauthorizedError("OAuth authorization required");
|
|
@@ -2671,9 +2774,9 @@ var MCPClient = class {
|
|
|
2671
2774
|
this.emitError(errorMessage, "connection");
|
|
2672
2775
|
this.emitStateChange("FAILED");
|
|
2673
2776
|
try {
|
|
2674
|
-
const existingSession = await
|
|
2777
|
+
const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
|
|
2675
2778
|
if (!existingSession || existingSession.status !== "active") {
|
|
2676
|
-
await
|
|
2779
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
2677
2780
|
}
|
|
2678
2781
|
} catch {
|
|
2679
2782
|
}
|
|
@@ -2698,16 +2801,16 @@ var MCPClient = class {
|
|
|
2698
2801
|
throw new Error(error);
|
|
2699
2802
|
}
|
|
2700
2803
|
if (state) {
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2804
|
+
try {
|
|
2805
|
+
await this.oauthProvider.consumeState(state);
|
|
2806
|
+
} catch (error) {
|
|
2807
|
+
const msg = error instanceof Error ? error.message : "Invalid OAuth state";
|
|
2808
|
+
this.emitError(msg, "auth");
|
|
2705
2809
|
this.emitStateChange("FAILED");
|
|
2706
|
-
throw
|
|
2810
|
+
throw error;
|
|
2707
2811
|
}
|
|
2708
|
-
await this.oauthProvider.consumeState(state);
|
|
2709
2812
|
}
|
|
2710
|
-
const transportsToTry = this.transportType ? [this.transportType] : ["streamable-http", "sse"];
|
|
2813
|
+
const transportsToTry = this.config.transportType ? [this.config.transportType] : ["streamable-http", "sse"];
|
|
2711
2814
|
let lastError;
|
|
2712
2815
|
let tokensExchanged = false;
|
|
2713
2816
|
let authenticatedStateEmitted = false;
|
|
@@ -2734,14 +2837,15 @@ var MCPClient = class {
|
|
|
2734
2837
|
}
|
|
2735
2838
|
}
|
|
2736
2839
|
await this.client.connect(this.transport);
|
|
2737
|
-
this.
|
|
2840
|
+
this._serverInfo = this.client.getServerVersion();
|
|
2841
|
+
this.config.transportType = currentType;
|
|
2738
2842
|
this.emitStateChange("CONNECTED");
|
|
2739
2843
|
this._onObservabilityEvent.fire({
|
|
2740
2844
|
type: "mcp:client:session_saved",
|
|
2741
2845
|
level: "info",
|
|
2742
|
-
message: `Saving active session ${this.sessionId} (OAuth complete)`,
|
|
2743
|
-
sessionId: this.sessionId,
|
|
2744
|
-
serverId: this.serverId,
|
|
2846
|
+
message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
|
|
2847
|
+
sessionId: this.config.sessionId,
|
|
2848
|
+
serverId: this.config.serverId,
|
|
2745
2849
|
timestamp: Date.now(),
|
|
2746
2850
|
id: nanoid()
|
|
2747
2851
|
});
|
|
@@ -2814,11 +2918,11 @@ var MCPClient = class {
|
|
|
2814
2918
|
this.emitStateChange("DISCOVERING");
|
|
2815
2919
|
try {
|
|
2816
2920
|
const result = await this.fetchTools();
|
|
2817
|
-
if (this.serverId) {
|
|
2921
|
+
if (this.config.serverId) {
|
|
2818
2922
|
this._onConnectionEvent.fire({
|
|
2819
2923
|
type: "tools_discovered",
|
|
2820
|
-
sessionId: this.sessionId,
|
|
2821
|
-
serverId: this.serverId,
|
|
2924
|
+
sessionId: this.config.sessionId,
|
|
2925
|
+
serverId: this.config.serverId,
|
|
2822
2926
|
toolCount: result.tools.length,
|
|
2823
2927
|
tools: result.tools,
|
|
2824
2928
|
timestamp: Date.now()
|
|
@@ -2858,8 +2962,8 @@ var MCPClient = class {
|
|
|
2858
2962
|
level: "info",
|
|
2859
2963
|
message: `Tool ${toolName} called successfully`,
|
|
2860
2964
|
displayMessage: `Called tool ${toolName}`,
|
|
2861
|
-
sessionId: this.sessionId,
|
|
2862
|
-
serverId: this.serverId,
|
|
2965
|
+
sessionId: this.config.sessionId,
|
|
2966
|
+
serverId: this.config.serverId,
|
|
2863
2967
|
payload: {
|
|
2864
2968
|
toolName,
|
|
2865
2969
|
args: toolArgs
|
|
@@ -2875,8 +2979,8 @@ var MCPClient = class {
|
|
|
2875
2979
|
level: "error",
|
|
2876
2980
|
message: errorMessage,
|
|
2877
2981
|
displayMessage: `Failed to call tool ${toolName}`,
|
|
2878
|
-
sessionId: this.sessionId,
|
|
2879
|
-
serverId: this.serverId,
|
|
2982
|
+
sessionId: this.config.sessionId,
|
|
2983
|
+
serverId: this.config.serverId,
|
|
2880
2984
|
payload: {
|
|
2881
2985
|
errorType: "tool_execution",
|
|
2882
2986
|
error: errorMessage,
|
|
@@ -3024,8 +3128,8 @@ var MCPClient = class {
|
|
|
3024
3128
|
type: "mcp:client:error",
|
|
3025
3129
|
level: "warn",
|
|
3026
3130
|
message: "Initialization failed during clearSession",
|
|
3027
|
-
sessionId: this.sessionId,
|
|
3028
|
-
serverId: this.serverId,
|
|
3131
|
+
sessionId: this.config.sessionId,
|
|
3132
|
+
serverId: this.config.serverId,
|
|
3029
3133
|
payload: { error: String(error) },
|
|
3030
3134
|
timestamp: Date.now(),
|
|
3031
3135
|
id: nanoid()
|
|
@@ -3034,7 +3138,7 @@ var MCPClient = class {
|
|
|
3034
3138
|
if (this.oauthProvider) {
|
|
3035
3139
|
await this.oauthProvider.invalidateCredentials("all");
|
|
3036
3140
|
}
|
|
3037
|
-
await
|
|
3141
|
+
await this._store.delete(this.config.userId, this.config.sessionId);
|
|
3038
3142
|
await this.disconnect();
|
|
3039
3143
|
}
|
|
3040
3144
|
/**
|
|
@@ -3068,19 +3172,19 @@ var MCPClient = class {
|
|
|
3068
3172
|
}
|
|
3069
3173
|
this.oauthProvider = null;
|
|
3070
3174
|
this.transport = null;
|
|
3071
|
-
if (this.serverId) {
|
|
3175
|
+
if (this.config.serverId) {
|
|
3072
3176
|
this._onConnectionEvent.fire({
|
|
3073
3177
|
type: "disconnected",
|
|
3074
|
-
sessionId: this.sessionId,
|
|
3075
|
-
serverId: this.serverId,
|
|
3178
|
+
sessionId: this.config.sessionId,
|
|
3179
|
+
serverId: this.config.serverId,
|
|
3076
3180
|
timestamp: Date.now()
|
|
3077
3181
|
});
|
|
3078
3182
|
this._onObservabilityEvent.fire({
|
|
3079
3183
|
type: "mcp:client:disconnect",
|
|
3080
3184
|
level: "info",
|
|
3081
|
-
message: `Disconnected from ${this.serverId}`,
|
|
3082
|
-
sessionId: this.sessionId,
|
|
3083
|
-
serverId: this.serverId,
|
|
3185
|
+
message: `Disconnected from ${this.config.serverId}`,
|
|
3186
|
+
sessionId: this.config.sessionId,
|
|
3187
|
+
serverId: this.config.serverId,
|
|
3084
3188
|
payload: {},
|
|
3085
3189
|
timestamp: Date.now(),
|
|
3086
3190
|
id: nanoid()
|
|
@@ -3105,42 +3209,52 @@ var MCPClient = class {
|
|
|
3105
3209
|
* @returns Server URL or empty string if not set
|
|
3106
3210
|
*/
|
|
3107
3211
|
getServerUrl() {
|
|
3108
|
-
return this.serverUrl || "";
|
|
3212
|
+
return this.config.serverUrl || "";
|
|
3109
3213
|
}
|
|
3110
3214
|
/**
|
|
3111
3215
|
* Gets the OAuth callback URL
|
|
3112
3216
|
* @returns Callback URL or empty string if not set
|
|
3113
3217
|
*/
|
|
3114
3218
|
getCallbackUrl() {
|
|
3115
|
-
return this.callbackUrl || "";
|
|
3219
|
+
return this.config.callbackUrl || "";
|
|
3116
3220
|
}
|
|
3117
3221
|
/**
|
|
3118
3222
|
* Gets the transport type being used
|
|
3119
3223
|
* @returns Transport type (defaults to 'streamable-http')
|
|
3120
3224
|
*/
|
|
3121
3225
|
getTransportType() {
|
|
3122
|
-
return this.transportType || "streamable-http";
|
|
3226
|
+
return this.config.transportType || "streamable-http";
|
|
3227
|
+
}
|
|
3228
|
+
/**
|
|
3229
|
+
* Gets the full server metadata from the MCP initialize response.
|
|
3230
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3231
|
+
* Returns undefined if the client hasn't connected yet.
|
|
3232
|
+
*/
|
|
3233
|
+
getServerInfo() {
|
|
3234
|
+
return this._serverInfo;
|
|
3123
3235
|
}
|
|
3124
3236
|
/**
|
|
3125
|
-
* Gets the human-readable server name
|
|
3237
|
+
* Gets the human-readable server name.
|
|
3238
|
+
* Prefers the server's reported title/name from the initialize response,
|
|
3239
|
+
* falling back to the name provided at construction or session metadata.
|
|
3126
3240
|
* @returns Server name or undefined
|
|
3127
3241
|
*/
|
|
3128
3242
|
getServerName() {
|
|
3129
|
-
return this.serverName;
|
|
3243
|
+
return this.config.serverName;
|
|
3130
3244
|
}
|
|
3131
3245
|
/**
|
|
3132
3246
|
* Gets the server ID
|
|
3133
3247
|
* @returns Server ID or undefined
|
|
3134
3248
|
*/
|
|
3135
3249
|
getServerId() {
|
|
3136
|
-
return this.serverId;
|
|
3250
|
+
return this.config.serverId;
|
|
3137
3251
|
}
|
|
3138
3252
|
/**
|
|
3139
3253
|
* Gets the session ID
|
|
3140
3254
|
* @returns Session ID
|
|
3141
3255
|
*/
|
|
3142
3256
|
getSessionId() {
|
|
3143
|
-
return this.sessionId;
|
|
3257
|
+
return this.config.sessionId;
|
|
3144
3258
|
}
|
|
3145
3259
|
};
|
|
3146
3260
|
|
|
@@ -3163,6 +3277,14 @@ var ToolPolicyGateway = class {
|
|
|
3163
3277
|
getServerId() {
|
|
3164
3278
|
return this.client.getServerId?.();
|
|
3165
3279
|
}
|
|
3280
|
+
/**
|
|
3281
|
+
* Returns the full server metadata from the underlying client, if available.
|
|
3282
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
3283
|
+
* Available only after the client has connected and completed initialization.
|
|
3284
|
+
*/
|
|
3285
|
+
getServerInfo() {
|
|
3286
|
+
return this.client.getServerInfo?.();
|
|
3287
|
+
}
|
|
3166
3288
|
/**
|
|
3167
3289
|
* Returns the human-readable server name from the underlying client, if available.
|
|
3168
3290
|
*/
|
|
@@ -3291,6 +3413,7 @@ var MultiSessionClient = class {
|
|
|
3291
3413
|
__publicField(this, "connectionPromises", /* @__PURE__ */ new Map());
|
|
3292
3414
|
__publicField(this, "userId");
|
|
3293
3415
|
__publicField(this, "options");
|
|
3416
|
+
__publicField(this, "_store");
|
|
3294
3417
|
this.userId = userId;
|
|
3295
3418
|
this.options = {
|
|
3296
3419
|
timeout: DEFAULT_TIMEOUT_MS,
|
|
@@ -3298,6 +3421,7 @@ var MultiSessionClient = class {
|
|
|
3298
3421
|
retryDelay: DEFAULT_RETRY_DELAY_MS,
|
|
3299
3422
|
...options
|
|
3300
3423
|
};
|
|
3424
|
+
this._store = options.onObservabilityEvent ? withDbObservability(options.sessionStore ?? sessions, options.onObservabilityEvent) : options.sessionStore ?? sessions;
|
|
3301
3425
|
}
|
|
3302
3426
|
// -----------------------------------------------------------------------
|
|
3303
3427
|
// Public API
|
|
@@ -3381,9 +3505,9 @@ var MultiSessionClient = class {
|
|
|
3381
3505
|
* to querying the storage backend via `sessions.list(userId)`.
|
|
3382
3506
|
*/
|
|
3383
3507
|
async fetchActiveSessions() {
|
|
3384
|
-
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await
|
|
3508
|
+
const sessionList = this.options.sessionProvider ? await this.options.sessionProvider() : await this._store.list(this.userId);
|
|
3385
3509
|
return sessionList.filter(
|
|
3386
|
-
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active"
|
|
3510
|
+
(s) => s.serverId && s.serverUrl && s.callbackUrl && s.status === "active" && s.enabled !== false
|
|
3387
3511
|
);
|
|
3388
3512
|
}
|
|
3389
3513
|
/**
|
|
@@ -3456,8 +3580,13 @@ var MultiSessionClient = class {
|
|
|
3456
3580
|
callbackUrl: session.callbackUrl,
|
|
3457
3581
|
serverName: session.serverName,
|
|
3458
3582
|
transportType: session.transportType,
|
|
3459
|
-
headers: session.headers
|
|
3583
|
+
headers: session.headers,
|
|
3584
|
+
hasSession: true,
|
|
3585
|
+
sessionStore: this._store
|
|
3460
3586
|
});
|
|
3587
|
+
if (this.options.onObservabilityEvent) {
|
|
3588
|
+
client.onObservabilityEvent(this.options.onObservabilityEvent);
|
|
3589
|
+
}
|
|
3461
3590
|
const timeoutMs = this.options.timeout;
|
|
3462
3591
|
let timeoutTimer;
|
|
3463
3592
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -3512,130 +3641,188 @@ function isConnectionEvent(event) {
|
|
|
3512
3641
|
}
|
|
3513
3642
|
|
|
3514
3643
|
// src/server/handlers/sse-handler.ts
|
|
3515
|
-
var
|
|
3644
|
+
var DEFAULT_HEARTBEAT_MS = 3e4;
|
|
3516
3645
|
function normalizeHeaders(headers) {
|
|
3517
3646
|
if (!headers || typeof headers !== "object") return void 0;
|
|
3518
|
-
const entries = Object.entries(headers).map(([
|
|
3647
|
+
const entries = Object.entries(headers).map(([k, v]) => [k.trim(), String(v).trim()]).filter(([k, v]) => k.length > 0 && v.length > 0);
|
|
3519
3648
|
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
3520
3649
|
}
|
|
3650
|
+
function validatingStateEvent(sessionId, session) {
|
|
3651
|
+
return {
|
|
3652
|
+
type: "state_changed",
|
|
3653
|
+
sessionId,
|
|
3654
|
+
serverId: session.serverId ?? "unknown",
|
|
3655
|
+
serverName: session.serverName ?? "Unknown",
|
|
3656
|
+
serverUrl: session.serverUrl,
|
|
3657
|
+
state: "VALIDATING",
|
|
3658
|
+
previousState: "DISCONNECTED",
|
|
3659
|
+
timestamp: Date.now()
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
function connectionErrorEvent(sessionId, serverId, error, errorType) {
|
|
3663
|
+
return {
|
|
3664
|
+
type: "error",
|
|
3665
|
+
sessionId,
|
|
3666
|
+
serverId: serverId ?? "unknown",
|
|
3667
|
+
error: error instanceof Error ? error.message : "Connection failed",
|
|
3668
|
+
errorType,
|
|
3669
|
+
timestamp: Date.now()
|
|
3670
|
+
};
|
|
3671
|
+
}
|
|
3521
3672
|
var SSEConnectionManager = class {
|
|
3673
|
+
// -----------------------------------------------------------------------
|
|
3674
|
+
// Constructor
|
|
3675
|
+
// -----------------------------------------------------------------------
|
|
3676
|
+
/**
|
|
3677
|
+
* @param options - Handler configuration (userId, auth, metadata, heartbeat, observability).
|
|
3678
|
+
* @param sendEvent - Callback that writes a typed event onto the SSE stream.
|
|
3679
|
+
*/
|
|
3522
3680
|
constructor(options, sendEvent) {
|
|
3523
3681
|
this.options = options;
|
|
3524
3682
|
this.sendEvent = sendEvent;
|
|
3683
|
+
// -----------------------------------------------------------------------
|
|
3684
|
+
// State
|
|
3685
|
+
// -----------------------------------------------------------------------
|
|
3525
3686
|
__publicField(this, "userId");
|
|
3687
|
+
/** Active MCP transports keyed by sessionId. */
|
|
3526
3688
|
__publicField(this, "clients", /* @__PURE__ */ new Map());
|
|
3689
|
+
/** Instrumented session store — always wraps `sessions` with DB observability. */
|
|
3690
|
+
__publicField(this, "observedStore");
|
|
3527
3691
|
__publicField(this, "heartbeatTimer");
|
|
3528
|
-
__publicField(this, "
|
|
3692
|
+
__publicField(this, "active", true);
|
|
3529
3693
|
this.userId = options.userId;
|
|
3694
|
+
this.observedStore = withDbObservability(sessions, (event) => this.sendEvent(event));
|
|
3530
3695
|
this.startHeartbeat();
|
|
3531
3696
|
}
|
|
3697
|
+
// -----------------------------------------------------------------------
|
|
3698
|
+
// Public API
|
|
3699
|
+
// -----------------------------------------------------------------------
|
|
3532
3700
|
/**
|
|
3533
|
-
*
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
}
|
|
3544
|
-
return metadata;
|
|
3545
|
-
}
|
|
3546
|
-
/**
|
|
3547
|
-
* Start heartbeat to keep connection alive
|
|
3548
|
-
*/
|
|
3549
|
-
startHeartbeat() {
|
|
3550
|
-
const interval = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_INTERVAL;
|
|
3551
|
-
this.heartbeatTimer = setInterval(() => {
|
|
3552
|
-
if (this.isActive) {
|
|
3553
|
-
this.sendEvent({
|
|
3554
|
-
level: "debug",
|
|
3555
|
-
message: "heartbeat",
|
|
3556
|
-
timestamp: Date.now()
|
|
3557
|
-
});
|
|
3558
|
-
}
|
|
3559
|
-
}, interval);
|
|
3560
|
-
}
|
|
3561
|
-
/**
|
|
3562
|
-
* Handle incoming RPC requests
|
|
3563
|
-
* Returns the RPC response directly for immediate HTTP response (bypassing SSE latency)
|
|
3701
|
+
* Dispatches an incoming RPC request, emits timing observability,
|
|
3702
|
+
* and returns the response.
|
|
3703
|
+
*
|
|
3704
|
+
* Emits `rpc:start` before dispatch and `rpc:end` on completion
|
|
3705
|
+
* (success or error), each carrying the method name, sessionId, and
|
|
3706
|
+
* duration. All events flow through the unified `onObservability`
|
|
3707
|
+
* and the SSE stream.
|
|
3708
|
+
*
|
|
3709
|
+
* @param request - The deserialized RPC envelope.
|
|
3710
|
+
* @returns The RPC response (success or error).
|
|
3564
3711
|
*/
|
|
3565
3712
|
async handleRequest(request) {
|
|
3713
|
+
const method = request.method;
|
|
3714
|
+
const sessionId = request.params?.sessionId;
|
|
3715
|
+
const t0 = performance.now();
|
|
3716
|
+
this.sendEvent({
|
|
3717
|
+
type: "rpc:start",
|
|
3718
|
+
level: "debug",
|
|
3719
|
+
message: method,
|
|
3720
|
+
sessionId,
|
|
3721
|
+
timestamp: Date.now()
|
|
3722
|
+
});
|
|
3566
3723
|
try {
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
break;
|
|
3578
|
-
case "disconnect":
|
|
3579
|
-
result = await this.disconnect(request.params);
|
|
3580
|
-
break;
|
|
3581
|
-
case "listTools":
|
|
3582
|
-
result = await this.listTools(request.params);
|
|
3583
|
-
break;
|
|
3584
|
-
case "setToolPolicy":
|
|
3585
|
-
result = await this.setToolPolicy(request.params);
|
|
3586
|
-
break;
|
|
3587
|
-
case "getToolPolicy":
|
|
3588
|
-
result = await this.getToolPolicy(request.params);
|
|
3589
|
-
break;
|
|
3590
|
-
case "callTool":
|
|
3591
|
-
result = await this.callTool(request.params);
|
|
3592
|
-
break;
|
|
3593
|
-
case "getSession":
|
|
3594
|
-
result = await this.getSession(request.params);
|
|
3595
|
-
break;
|
|
3596
|
-
case "finishAuth":
|
|
3597
|
-
result = await this.finishAuth(request.params);
|
|
3598
|
-
break;
|
|
3599
|
-
case "listPrompts":
|
|
3600
|
-
result = await this.listPrompts(request.params);
|
|
3601
|
-
break;
|
|
3602
|
-
case "getPrompt":
|
|
3603
|
-
result = await this.getPrompt(request.params);
|
|
3604
|
-
break;
|
|
3605
|
-
case "listResources":
|
|
3606
|
-
result = await this.listResources(request.params);
|
|
3607
|
-
break;
|
|
3608
|
-
case "readResource":
|
|
3609
|
-
result = await this.readResource(request.params);
|
|
3610
|
-
break;
|
|
3611
|
-
default:
|
|
3612
|
-
throw new Error(`Unknown method: ${request.method}`);
|
|
3613
|
-
}
|
|
3614
|
-
const response = {
|
|
3615
|
-
id: request.id,
|
|
3616
|
-
result
|
|
3617
|
-
};
|
|
3724
|
+
const result = await this.dispatchImpl(request);
|
|
3725
|
+
this.sendEvent({
|
|
3726
|
+
type: "rpc:end",
|
|
3727
|
+
level: "debug",
|
|
3728
|
+
message: method,
|
|
3729
|
+
sessionId,
|
|
3730
|
+
payload: { durationMs: performance.now() - t0 },
|
|
3731
|
+
timestamp: Date.now()
|
|
3732
|
+
});
|
|
3733
|
+
const response = { id: request.id, result };
|
|
3618
3734
|
this.sendEvent(response);
|
|
3619
3735
|
return response;
|
|
3620
3736
|
} catch (error) {
|
|
3621
|
-
|
|
3737
|
+
this.sendEvent({
|
|
3738
|
+
type: "rpc:end",
|
|
3739
|
+
level: "error",
|
|
3740
|
+
message: method,
|
|
3741
|
+
sessionId,
|
|
3742
|
+
payload: { durationMs: performance.now() - t0, error: String(error) },
|
|
3743
|
+
timestamp: Date.now()
|
|
3744
|
+
});
|
|
3745
|
+
const response = {
|
|
3622
3746
|
id: request.id,
|
|
3623
3747
|
error: {
|
|
3624
3748
|
code: RpcErrorCodes.EXECUTION_ERROR,
|
|
3625
3749
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
3626
3750
|
}
|
|
3627
3751
|
};
|
|
3628
|
-
this.sendEvent(
|
|
3629
|
-
return
|
|
3752
|
+
this.sendEvent(response);
|
|
3753
|
+
return response;
|
|
3630
3754
|
}
|
|
3631
3755
|
}
|
|
3632
3756
|
/**
|
|
3633
|
-
*
|
|
3757
|
+
* Tears down all active MCP transports and stops the heartbeat timer.
|
|
3758
|
+
*
|
|
3759
|
+
* Disconnects are issued in parallel across all sessions. After calling
|
|
3760
|
+
* this method the manager instance should be discarded.
|
|
3761
|
+
*/
|
|
3762
|
+
async dispose() {
|
|
3763
|
+
this.active = false;
|
|
3764
|
+
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
|
|
3765
|
+
await Promise.all(
|
|
3766
|
+
Array.from(this.clients.values()).map((c) => c.disconnect())
|
|
3767
|
+
);
|
|
3768
|
+
this.clients.clear();
|
|
3769
|
+
}
|
|
3770
|
+
// -----------------------------------------------------------------------
|
|
3771
|
+
// RPC Dispatch (raw — called by handleRequest which adds timing)
|
|
3772
|
+
// -----------------------------------------------------------------------
|
|
3773
|
+
/**
|
|
3774
|
+
* Routes an RPC method name to the appropriate private handler.
|
|
3775
|
+
*
|
|
3776
|
+
* @throws {Error} When the method name is unrecognized.
|
|
3777
|
+
*/
|
|
3778
|
+
async dispatchImpl(request) {
|
|
3779
|
+
switch (request.method) {
|
|
3780
|
+
case "listSessions":
|
|
3781
|
+
return this.listSessions();
|
|
3782
|
+
case "connect":
|
|
3783
|
+
return this.connect(request.params);
|
|
3784
|
+
case "reconnect":
|
|
3785
|
+
return this.reconnect(request.params);
|
|
3786
|
+
case "disconnect":
|
|
3787
|
+
return this.disconnect(request.params);
|
|
3788
|
+
case "listTools":
|
|
3789
|
+
return this.listTools(request.params);
|
|
3790
|
+
case "setToolPolicy":
|
|
3791
|
+
return this.setToolPolicy(request.params);
|
|
3792
|
+
case "getToolPolicy":
|
|
3793
|
+
return this.getToolPolicy(request.params);
|
|
3794
|
+
case "updateSession":
|
|
3795
|
+
return this.updateSession(request.params);
|
|
3796
|
+
case "callTool":
|
|
3797
|
+
return this.callTool(request.params);
|
|
3798
|
+
case "getSession":
|
|
3799
|
+
return this.getSession(request.params);
|
|
3800
|
+
case "finishAuth":
|
|
3801
|
+
return this.finishAuth(request.params);
|
|
3802
|
+
case "listPrompts":
|
|
3803
|
+
return this.listPrompts(request.params);
|
|
3804
|
+
case "getPrompt":
|
|
3805
|
+
return this.getPrompt(request.params);
|
|
3806
|
+
case "listResources":
|
|
3807
|
+
return this.listResources(request.params);
|
|
3808
|
+
case "readResource":
|
|
3809
|
+
return this.readResource(request.params);
|
|
3810
|
+
default:
|
|
3811
|
+
throw new Error(`Unknown RPC method: ${request.method}`);
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
// -----------------------------------------------------------------------
|
|
3815
|
+
// Session Query
|
|
3816
|
+
// -----------------------------------------------------------------------
|
|
3817
|
+
/**
|
|
3818
|
+
* Lists all sessions owned by the current user.
|
|
3819
|
+
*
|
|
3820
|
+
* Returns a lightweight view — session metadata only, no credential fields.
|
|
3634
3821
|
*/
|
|
3635
3822
|
async listSessions() {
|
|
3636
|
-
const
|
|
3823
|
+
const all = await sessions.list(this.userId);
|
|
3637
3824
|
return {
|
|
3638
|
-
sessions:
|
|
3825
|
+
sessions: all.map((s) => ({
|
|
3639
3826
|
sessionId: s.sessionId,
|
|
3640
3827
|
serverId: s.serverId,
|
|
3641
3828
|
serverName: s.serverName,
|
|
@@ -3644,215 +3831,203 @@ var SSEConnectionManager = class {
|
|
|
3644
3831
|
createdAt: s.createdAt,
|
|
3645
3832
|
updatedAt: s.updatedAt ?? s.createdAt,
|
|
3646
3833
|
status: s.status ?? "pending",
|
|
3647
|
-
toolPolicy: s.toolPolicy
|
|
3834
|
+
toolPolicy: s.toolPolicy,
|
|
3835
|
+
enabled: s.enabled ?? true
|
|
3648
3836
|
}))
|
|
3649
3837
|
};
|
|
3650
3838
|
}
|
|
3651
3839
|
/**
|
|
3652
|
-
*
|
|
3840
|
+
* Restores and validates a previously persisted session.
|
|
3841
|
+
*
|
|
3842
|
+
* Loads the full session row (including credentials) from storage, creates
|
|
3843
|
+
* a new MCP transport, connects to the remote server, and emits the
|
|
3844
|
+
* discovered (policy-filtered) tool list via SSE.
|
|
3653
3845
|
*/
|
|
3654
|
-
async
|
|
3655
|
-
const
|
|
3656
|
-
|
|
3657
|
-
const serverId = params.serverId && params.serverId.length <= 12 ? params.serverId : generateServerId();
|
|
3658
|
-
const existingSessions = await sessions.list(this.userId);
|
|
3659
|
-
const duplicate = existingSessions.find(
|
|
3660
|
-
(s) => s.serverId === serverId || s.serverUrl === serverUrl
|
|
3661
|
-
);
|
|
3662
|
-
if (duplicate) {
|
|
3663
|
-
if (duplicate.status === "pending") {
|
|
3664
|
-
await this.getSession({ sessionId: duplicate.sessionId });
|
|
3665
|
-
return {
|
|
3666
|
-
sessionId: duplicate.sessionId,
|
|
3667
|
-
success: true
|
|
3668
|
-
};
|
|
3669
|
-
}
|
|
3670
|
-
throw new Error(`Connection already exists for server: ${duplicate.serverUrl || duplicate.serverId} (${duplicate.serverName})`);
|
|
3671
|
-
}
|
|
3672
|
-
const sessionId = await sessions.generateSessionId();
|
|
3846
|
+
async getSession(params) {
|
|
3847
|
+
const session = await this.requireSession(params.sessionId);
|
|
3848
|
+
this.sendEvent(validatingStateEvent(params.sessionId, session));
|
|
3673
3849
|
try {
|
|
3674
|
-
const
|
|
3675
|
-
|
|
3676
|
-
userId: this.userId,
|
|
3677
|
-
sessionId,
|
|
3678
|
-
serverId,
|
|
3679
|
-
serverName,
|
|
3680
|
-
serverUrl,
|
|
3681
|
-
callbackUrl,
|
|
3682
|
-
transportType,
|
|
3683
|
-
headers,
|
|
3684
|
-
clientId,
|
|
3685
|
-
clientSecret,
|
|
3686
|
-
...clientMetadata
|
|
3687
|
-
// Spread client metadata (clientName, clientUri, logoUri, policyUri)
|
|
3688
|
-
});
|
|
3689
|
-
this.clients.set(sessionId, client);
|
|
3690
|
-
client.onConnectionEvent((event) => {
|
|
3691
|
-
this.emitConnectionEvent(event);
|
|
3692
|
-
});
|
|
3693
|
-
client.onObservabilityEvent((event) => {
|
|
3694
|
-
this.sendEvent(event);
|
|
3695
|
-
});
|
|
3850
|
+
const client = this.restoreClient(session);
|
|
3851
|
+
this.attachClientEvents(client);
|
|
3696
3852
|
await client.connect();
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
success: true
|
|
3701
|
-
};
|
|
3853
|
+
this.clients.set(params.sessionId, client);
|
|
3854
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3855
|
+
return { success: true, toolCount: result.tools.length };
|
|
3702
3856
|
} catch (error) {
|
|
3703
|
-
|
|
3704
|
-
this.clients.delete(sessionId);
|
|
3705
|
-
return {
|
|
3706
|
-
sessionId,
|
|
3707
|
-
success: true
|
|
3708
|
-
};
|
|
3709
|
-
}
|
|
3710
|
-
this.emitConnectionEvent({
|
|
3711
|
-
type: "error",
|
|
3712
|
-
sessionId,
|
|
3713
|
-
serverId,
|
|
3714
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3715
|
-
errorType: "connection",
|
|
3716
|
-
timestamp: Date.now()
|
|
3717
|
-
});
|
|
3718
|
-
this.clients.delete(sessionId);
|
|
3857
|
+
this.sendEvent(connectionErrorEvent(params.sessionId, session.serverId, error, "validation"));
|
|
3719
3858
|
throw error;
|
|
3720
3859
|
}
|
|
3721
3860
|
}
|
|
3861
|
+
// -----------------------------------------------------------------------
|
|
3862
|
+
// Connection Lifecycle
|
|
3863
|
+
// -----------------------------------------------------------------------
|
|
3722
3864
|
/**
|
|
3723
|
-
*
|
|
3724
|
-
*
|
|
3865
|
+
* Initiates a connection to a new MCP server.
|
|
3866
|
+
*
|
|
3867
|
+
* If a session for the same `serverId` or `serverUrl` already exists and
|
|
3868
|
+
* is still in a `pending` (OAuth) state, the existing session is resumed
|
|
3869
|
+
* instead of creating a duplicate.
|
|
3870
|
+
*
|
|
3871
|
+
* `UnauthorizedError` is treated as a pending-auth state and returned as
|
|
3872
|
+
* a successful result (the client will then redirect to the auth URL).
|
|
3725
3873
|
*/
|
|
3726
|
-
async
|
|
3727
|
-
const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType, clientId, clientSecret } = params;
|
|
3874
|
+
async connect(params) {
|
|
3728
3875
|
const headers = normalizeHeaders(params.headers);
|
|
3729
|
-
const serverId =
|
|
3730
|
-
const
|
|
3731
|
-
|
|
3732
|
-
(
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
if (existingClient) {
|
|
3738
|
-
await existingClient.disconnect();
|
|
3739
|
-
this.clients.delete(duplicate.sessionId);
|
|
3876
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3877
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3878
|
+
if (existing) {
|
|
3879
|
+
if (existing.status === "pending") {
|
|
3880
|
+
return this.getSession({ sessionId: existing.sessionId }).then(() => ({
|
|
3881
|
+
sessionId: existing.sessionId,
|
|
3882
|
+
success: true
|
|
3883
|
+
}));
|
|
3740
3884
|
}
|
|
3885
|
+
throw new Error(
|
|
3886
|
+
`Connection already exists for server: ${existing.serverUrl ?? existing.serverId} (${existing.serverName})`
|
|
3887
|
+
);
|
|
3741
3888
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3889
|
+
const sessionId = await sessions.generateSessionId();
|
|
3890
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3891
|
+
const client = new MCPClient({
|
|
3892
|
+
userId: this.userId,
|
|
3893
|
+
sessionId,
|
|
3894
|
+
serverId,
|
|
3895
|
+
serverName: params.serverName,
|
|
3896
|
+
serverUrl: params.serverUrl,
|
|
3897
|
+
callbackUrl: params.callbackUrl,
|
|
3898
|
+
transportType: params.transportType,
|
|
3899
|
+
headers,
|
|
3900
|
+
sessionStore: this.observedStore,
|
|
3901
|
+
...metadata
|
|
3902
|
+
});
|
|
3903
|
+
this.cacheClient(sessionId, client);
|
|
3904
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* Reconnects to an MCP server by tearing down the active transport if one
|
|
3908
|
+
* exists and instantiating a fresh connection, reusing the existing session
|
|
3909
|
+
* (and its stored credentials / DCR client info) from the database.
|
|
3910
|
+
*/
|
|
3911
|
+
async reconnect(params) {
|
|
3912
|
+
const headers = normalizeHeaders(params.headers);
|
|
3913
|
+
const serverId = this.normalizeServerId(params.serverId);
|
|
3914
|
+
const existing = await this.findExistingSession(serverId, params.serverUrl);
|
|
3915
|
+
const sessionId = existing ? existing.sessionId : await sessions.generateSessionId();
|
|
3916
|
+
if (existing) {
|
|
3917
|
+
const staleClient = this.clients.get(existing.sessionId);
|
|
3918
|
+
if (staleClient) {
|
|
3919
|
+
await staleClient.disconnect();
|
|
3920
|
+
this.clients.delete(existing.sessionId);
|
|
3771
3921
|
}
|
|
3772
|
-
this.emitConnectionEvent({
|
|
3773
|
-
type: "error",
|
|
3774
|
-
sessionId,
|
|
3775
|
-
serverId,
|
|
3776
|
-
error: error instanceof Error ? error.message : "Connection failed",
|
|
3777
|
-
errorType: "connection",
|
|
3778
|
-
timestamp: Date.now()
|
|
3779
|
-
});
|
|
3780
|
-
this.clients.delete(sessionId);
|
|
3781
|
-
throw error;
|
|
3782
3922
|
}
|
|
3923
|
+
const metadata = await this.getResolvedClientMetadata();
|
|
3924
|
+
const client = new MCPClient({
|
|
3925
|
+
userId: this.userId,
|
|
3926
|
+
sessionId,
|
|
3927
|
+
serverId,
|
|
3928
|
+
serverName: params.serverName,
|
|
3929
|
+
serverUrl: params.serverUrl,
|
|
3930
|
+
callbackUrl: params.callbackUrl,
|
|
3931
|
+
transportType: params.transportType,
|
|
3932
|
+
headers,
|
|
3933
|
+
sessionStore: this.observedStore,
|
|
3934
|
+
...metadata
|
|
3935
|
+
});
|
|
3936
|
+
this.cacheClient(sessionId, client);
|
|
3937
|
+
return this.connectAndDiscover(client, sessionId, serverId);
|
|
3783
3938
|
}
|
|
3784
3939
|
/**
|
|
3785
|
-
*
|
|
3940
|
+
* Disconnects from an MCP server.
|
|
3941
|
+
*
|
|
3942
|
+
* If an active in-memory transport exists it delegates to `clearSession()`
|
|
3943
|
+
* (which sends the server an HTTP DELETE per the Streamable spec). If no
|
|
3944
|
+
* active transport is available (orphaned session from a failed OAuth flow),
|
|
3945
|
+
* the session row is removed directly from storage.
|
|
3786
3946
|
*/
|
|
3787
3947
|
async disconnect(params) {
|
|
3788
|
-
const
|
|
3789
|
-
const client = this.clients.get(sessionId);
|
|
3948
|
+
const client = this.clients.get(params.sessionId);
|
|
3790
3949
|
if (client) {
|
|
3791
3950
|
await client.clearSession();
|
|
3792
|
-
this.clients.delete(sessionId);
|
|
3951
|
+
this.clients.delete(params.sessionId);
|
|
3793
3952
|
} else {
|
|
3794
|
-
await sessions.delete(this.userId, sessionId);
|
|
3953
|
+
await sessions.delete(this.userId, params.sessionId);
|
|
3795
3954
|
}
|
|
3796
3955
|
return { success: true };
|
|
3797
3956
|
}
|
|
3957
|
+
// -----------------------------------------------------------------------
|
|
3958
|
+
// OAuth
|
|
3959
|
+
// -----------------------------------------------------------------------
|
|
3798
3960
|
/**
|
|
3799
|
-
*
|
|
3961
|
+
* Completes the OAuth 2.1 authorization code flow for a pending session.
|
|
3962
|
+
*
|
|
3963
|
+
* Loads the stored session (with credentials), creates a fresh `MCPClient`,
|
|
3964
|
+
* and calls `finishAuth` inside a {@link runWithCodeVerifierState} context
|
|
3965
|
+
* so the PKCE code verifier is available without a DB read.
|
|
3966
|
+
*
|
|
3967
|
+
* The session's stored `transportType` is intentionally **omitted** from the
|
|
3968
|
+
* client config — this lets `MCPClient` auto-negotiate (try Streamable HTTP
|
|
3969
|
+
* first, fall back to SSE), which is required for servers like Neon that
|
|
3970
|
+
* only support SSE transport.
|
|
3800
3971
|
*/
|
|
3801
|
-
async
|
|
3802
|
-
const
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3972
|
+
async finishAuth(params) {
|
|
3973
|
+
const parsed = parseOAuthState(params.state);
|
|
3974
|
+
const sessionId = parsed?.sessionId ?? params.state;
|
|
3975
|
+
const session = await this.requireSession(sessionId);
|
|
3976
|
+
try {
|
|
3977
|
+
const clientId = session.clientId ?? void 0;
|
|
3978
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
3979
|
+
const client = new MCPClient({
|
|
3980
|
+
userId: this.userId,
|
|
3981
|
+
sessionId,
|
|
3982
|
+
serverId: session.serverId,
|
|
3983
|
+
serverName: session.serverName,
|
|
3984
|
+
serverUrl: session.serverUrl,
|
|
3985
|
+
callbackUrl: session.callbackUrl,
|
|
3986
|
+
headers: session.headers,
|
|
3987
|
+
clientId,
|
|
3988
|
+
clientSecret,
|
|
3989
|
+
hasSession: true,
|
|
3990
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
3991
|
+
sessionStore: this.observedStore
|
|
3992
|
+
});
|
|
3993
|
+
this.attachClientEvents(client);
|
|
3994
|
+
await runWithCodeVerifierState(
|
|
3995
|
+
session.codeVerifier ?? "",
|
|
3996
|
+
"S256",
|
|
3997
|
+
() => client.finishAuth(params.code, params.state)
|
|
3998
|
+
);
|
|
3999
|
+
this.clients.set(sessionId, client);
|
|
4000
|
+
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4001
|
+
return { success: true, toolCount: result.tools.length };
|
|
4002
|
+
} catch (error) {
|
|
4003
|
+
this.sendEvent(connectionErrorEvent(sessionId, session.serverId, error, "auth"));
|
|
4004
|
+
throw error;
|
|
3809
4005
|
}
|
|
3810
|
-
const client = new MCPClient({
|
|
3811
|
-
userId: this.userId,
|
|
3812
|
-
sessionId,
|
|
3813
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3814
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3815
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3816
|
-
// reloading during initialize().
|
|
3817
|
-
serverId: session.serverId,
|
|
3818
|
-
serverName: session.serverName,
|
|
3819
|
-
serverUrl: session.serverUrl,
|
|
3820
|
-
callbackUrl: session.callbackUrl,
|
|
3821
|
-
transportType: session.transportType,
|
|
3822
|
-
headers: session.headers
|
|
3823
|
-
});
|
|
3824
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3825
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3826
|
-
await client.connect();
|
|
3827
|
-
this.clients.set(sessionId, client);
|
|
3828
|
-
return client;
|
|
3829
4006
|
}
|
|
4007
|
+
// -----------------------------------------------------------------------
|
|
4008
|
+
// Tool Discovery & Policy
|
|
4009
|
+
// -----------------------------------------------------------------------
|
|
3830
4010
|
/**
|
|
3831
|
-
* Fetches
|
|
4011
|
+
* Fetches the complete tool list from the remote MCP server and emits a
|
|
4012
|
+
* `tools_discovered` SSE event with two lists:
|
|
3832
4013
|
*
|
|
3833
|
-
*
|
|
3834
|
-
* - `
|
|
3835
|
-
* - `allTools` — the complete, unfiltered list used by the management UI so
|
|
3836
|
-
* that blocked tools still appear as checkboxes in the dialog.
|
|
4014
|
+
* - `tools` — policy-filtered (what agents are allowed to call)
|
|
4015
|
+
* - `allTools` — complete, unfiltered (used by the management UI)
|
|
3837
4016
|
*
|
|
3838
|
-
* `fetchTools()`
|
|
3839
|
-
* `gateway.listTools()`
|
|
3840
|
-
* network round-trip is made regardless of how many callers follow.
|
|
4017
|
+
* Only a single network round-trip is made: `fetchTools()` populates the
|
|
4018
|
+
* in-memory cache, then `gateway.listTools()` reuses that cache.
|
|
3841
4019
|
*
|
|
3842
|
-
* @param sessionId - The session
|
|
3843
|
-
* @returns The session record and the
|
|
3844
|
-
* @throws {Error}
|
|
4020
|
+
* @param sessionId - The session to discover tools for.
|
|
4021
|
+
* @returns The session record and the filtered tool result.
|
|
4022
|
+
* @throws {Error} If the session does not exist in storage.
|
|
3845
4023
|
*/
|
|
3846
4024
|
async listPolicyFilteredTools(sessionId) {
|
|
3847
|
-
const session = await
|
|
3848
|
-
if (!session) {
|
|
3849
|
-
throw new Error("Session not found");
|
|
3850
|
-
}
|
|
4025
|
+
const session = await this.requireSession(sessionId);
|
|
3851
4026
|
const client = await this.getOrCreateClient(sessionId);
|
|
3852
4027
|
const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
|
|
3853
4028
|
const gateway = createToolPolicyGateway(this.userId, sessionId, client);
|
|
3854
4029
|
const result = await gateway.listTools();
|
|
3855
|
-
this.
|
|
4030
|
+
this.sendEvent({
|
|
3856
4031
|
type: "tools_discovered",
|
|
3857
4032
|
sessionId,
|
|
3858
4033
|
serverId: session.serverId ?? "unknown",
|
|
@@ -3864,254 +4039,254 @@ var SSEConnectionManager = class {
|
|
|
3864
4039
|
return { session, result };
|
|
3865
4040
|
}
|
|
3866
4041
|
/**
|
|
3867
|
-
* Returns the policy-filtered tool list for a session
|
|
3868
|
-
*
|
|
3869
|
-
*
|
|
4042
|
+
* Returns the policy-filtered tool list for a session.
|
|
4043
|
+
*
|
|
4044
|
+
* Delegates to {@link listPolicyFilteredTools}, which also emits a
|
|
4045
|
+
* `tools_discovered` SSE event to keep the client state current.
|
|
3870
4046
|
*/
|
|
3871
4047
|
async listTools(params) {
|
|
3872
|
-
const {
|
|
3873
|
-
const { result } = await this.listPolicyFilteredTools(sessionId);
|
|
4048
|
+
const { result } = await this.listPolicyFilteredTools(params.sessionId);
|
|
3874
4049
|
return { tools: result.tools };
|
|
3875
4050
|
}
|
|
3876
4051
|
/**
|
|
3877
|
-
*
|
|
4052
|
+
* Returns all raw tools annotated with their effective policy state
|
|
4053
|
+
* for display in the management UI.
|
|
3878
4054
|
*/
|
|
3879
4055
|
async getToolPolicy(params) {
|
|
3880
|
-
const
|
|
3881
|
-
const
|
|
3882
|
-
if (!session) {
|
|
3883
|
-
throw new Error("Session not found");
|
|
3884
|
-
}
|
|
3885
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4056
|
+
const session = await this.requireSession(params.sessionId);
|
|
4057
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3886
4058
|
const allTools = await client.fetchTools();
|
|
3887
|
-
const
|
|
4059
|
+
const policy = session.toolPolicy ?? {
|
|
3888
4060
|
mode: "all",
|
|
3889
4061
|
toolIds: [],
|
|
3890
4062
|
updatedAt: session.updatedAt ?? session.createdAt
|
|
3891
4063
|
};
|
|
3892
4064
|
const serverId = session.serverId ?? "unknown";
|
|
3893
|
-
const tools = allTools.tools.map((
|
|
3894
|
-
...
|
|
3895
|
-
toolId: createToolId(serverId,
|
|
3896
|
-
allowed: isToolAllowed(
|
|
4065
|
+
const tools = allTools.tools.map((t) => ({
|
|
4066
|
+
...t,
|
|
4067
|
+
toolId: createToolId(serverId, t.name),
|
|
4068
|
+
allowed: isToolAllowed(policy, t.name, session.serverId)
|
|
3897
4069
|
}));
|
|
3898
4070
|
return {
|
|
3899
|
-
toolPolicy,
|
|
4071
|
+
toolPolicy: policy,
|
|
3900
4072
|
tools,
|
|
3901
4073
|
toolCount: tools.length,
|
|
3902
|
-
allowedToolCount: tools.filter((
|
|
4074
|
+
allowedToolCount: tools.filter((t) => t.allowed).length
|
|
3903
4075
|
};
|
|
3904
4076
|
}
|
|
3905
4077
|
/**
|
|
3906
|
-
* Persists a new tool access policy
|
|
3907
|
-
*
|
|
3908
|
-
*
|
|
3909
|
-
* Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
|
|
3910
|
-
* so the management UI can immediately reflect the new checkbox states without
|
|
3911
|
-
* an additional round-trip to the server.
|
|
4078
|
+
* Persists a new tool access policy and broadcasts the updated filtered
|
|
4079
|
+
* tool list via a `tools_discovered` SSE event.
|
|
3912
4080
|
*
|
|
3913
|
-
* @
|
|
3914
|
-
*
|
|
4081
|
+
* @throws {Error} If the session does not exist or the policy references
|
|
4082
|
+
* tool IDs that don't match any known tool.
|
|
3915
4083
|
*/
|
|
3916
4084
|
async setToolPolicy(params) {
|
|
3917
|
-
const
|
|
3918
|
-
const
|
|
3919
|
-
if (!session) {
|
|
3920
|
-
throw new Error("Session not found");
|
|
3921
|
-
}
|
|
3922
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4085
|
+
const session = await this.requireSession(params.sessionId);
|
|
4086
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
3923
4087
|
const allTools = await client.fetchTools();
|
|
3924
|
-
const
|
|
3925
|
-
validateToolPolicyAgainstTools(
|
|
3926
|
-
await sessions.update(this.userId, sessionId, { toolPolicy });
|
|
3927
|
-
const
|
|
3928
|
-
|
|
4088
|
+
const policy = normalizeToolPolicyForUpdate(params.toolPolicy);
|
|
4089
|
+
validateToolPolicyAgainstTools(policy, allTools.tools, session.serverId);
|
|
4090
|
+
await sessions.update(this.userId, params.sessionId, { toolPolicy: policy });
|
|
4091
|
+
const filtered = createToolPolicyGateway(
|
|
4092
|
+
this.userId,
|
|
4093
|
+
params.sessionId,
|
|
4094
|
+
client
|
|
4095
|
+
).filterTools({ ...session, toolPolicy: policy }, allTools.tools);
|
|
4096
|
+
this.sendEvent({
|
|
3929
4097
|
type: "tools_discovered",
|
|
3930
|
-
sessionId,
|
|
4098
|
+
sessionId: params.sessionId,
|
|
3931
4099
|
serverId: session.serverId ?? "unknown",
|
|
3932
|
-
toolCount:
|
|
3933
|
-
tools:
|
|
4100
|
+
toolCount: filtered.length,
|
|
4101
|
+
tools: filtered,
|
|
3934
4102
|
allTools: allTools.tools,
|
|
3935
4103
|
timestamp: Date.now()
|
|
3936
4104
|
});
|
|
3937
|
-
return {
|
|
3938
|
-
success: true,
|
|
3939
|
-
toolPolicy,
|
|
3940
|
-
tools: filteredTools,
|
|
3941
|
-
toolCount: filteredTools.length
|
|
3942
|
-
};
|
|
4105
|
+
return { success: true, toolPolicy: policy, tools: filtered, toolCount: filtered.length };
|
|
3943
4106
|
}
|
|
3944
4107
|
/**
|
|
3945
|
-
*
|
|
3946
|
-
*
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
...result,
|
|
3955
|
-
_meta: {
|
|
3956
|
-
...meta,
|
|
3957
|
-
sessionId
|
|
3958
|
-
}
|
|
3959
|
-
};
|
|
3960
|
-
}
|
|
3961
|
-
/**
|
|
3962
|
-
* Restore and validate an existing session
|
|
4108
|
+
* Enables or disables a session for agent tool discovery.
|
|
4109
|
+
*
|
|
4110
|
+
* Disabled sessions retain their OAuth tokens and connection metadata
|
|
4111
|
+
* but are hidden from `MultiSessionClient.connect()` and blocked from
|
|
4112
|
+
* RPC tool access. Re-enabling does not require re-authentication.
|
|
4113
|
+
*
|
|
4114
|
+
* @param params - `{ sessionId, enabled: boolean }`
|
|
4115
|
+
* @returns `{ success: true }`
|
|
4116
|
+
* @throws {Error} If the session does not exist.
|
|
3963
4117
|
*/
|
|
3964
|
-
async
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
throw new Error("Session not found");
|
|
3969
|
-
}
|
|
3970
|
-
this.emitConnectionEvent({
|
|
3971
|
-
type: "state_changed",
|
|
3972
|
-
sessionId,
|
|
3973
|
-
serverId: session.serverId ?? "unknown",
|
|
3974
|
-
serverName: session.serverName ?? "Unknown",
|
|
3975
|
-
serverUrl: session.serverUrl,
|
|
3976
|
-
state: "VALIDATING",
|
|
3977
|
-
previousState: "DISCONNECTED",
|
|
3978
|
-
timestamp: Date.now()
|
|
3979
|
-
});
|
|
3980
|
-
try {
|
|
3981
|
-
const clientMetadata = await this.getResolvedClientMetadata();
|
|
3982
|
-
const client = new MCPClient({
|
|
3983
|
-
userId: this.userId,
|
|
3984
|
-
sessionId,
|
|
3985
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
3986
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
3987
|
-
// original transport/connection metadata instead of relying on lazy
|
|
3988
|
-
// reloading during initialize().
|
|
3989
|
-
serverId: session.serverId,
|
|
3990
|
-
serverName: session.serverName,
|
|
3991
|
-
serverUrl: session.serverUrl,
|
|
3992
|
-
callbackUrl: session.callbackUrl,
|
|
3993
|
-
transportType: session.transportType,
|
|
3994
|
-
headers: session.headers,
|
|
3995
|
-
...clientMetadata
|
|
3996
|
-
});
|
|
3997
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
3998
|
-
client.onObservabilityEvent((event) => this.sendEvent(event));
|
|
3999
|
-
await client.connect();
|
|
4000
|
-
this.clients.set(sessionId, client);
|
|
4001
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4002
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4003
|
-
} catch (error) {
|
|
4004
|
-
this.emitConnectionEvent({
|
|
4005
|
-
type: "error",
|
|
4006
|
-
sessionId,
|
|
4007
|
-
serverId: session.serverId ?? "unknown",
|
|
4008
|
-
error: error instanceof Error ? error.message : "Validation failed",
|
|
4009
|
-
errorType: "validation",
|
|
4010
|
-
timestamp: Date.now()
|
|
4011
|
-
});
|
|
4012
|
-
throw error;
|
|
4013
|
-
}
|
|
4118
|
+
async updateSession(params) {
|
|
4119
|
+
await this.requireSession(params.sessionId);
|
|
4120
|
+
await sessions.update(this.userId, params.sessionId, { enabled: params.enabled });
|
|
4121
|
+
return { success: true };
|
|
4014
4122
|
}
|
|
4123
|
+
// -----------------------------------------------------------------------
|
|
4124
|
+
// Tool Execution
|
|
4125
|
+
// -----------------------------------------------------------------------
|
|
4015
4126
|
/**
|
|
4016
|
-
*
|
|
4127
|
+
* Proxies a tool invocation to the remote MCP server.
|
|
4128
|
+
*
|
|
4129
|
+
* Resolves (or creates) the transport for the given session, runs the call
|
|
4130
|
+
* through the tool-policy gateway, and injects `sessionId` into the result
|
|
4131
|
+
* metadata so the client can route the response without scanning all sessions.
|
|
4017
4132
|
*/
|
|
4018
|
-
async
|
|
4019
|
-
const
|
|
4020
|
-
const
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
}
|
|
4027
|
-
try {
|
|
4028
|
-
const client = new MCPClient({
|
|
4029
|
-
userId: this.userId,
|
|
4030
|
-
sessionId,
|
|
4031
|
-
// These fields are optional in MCPClient, but when rehydrating a known
|
|
4032
|
-
// stored session on the server we pass them explicitly to preserve the
|
|
4033
|
-
// original connection metadata instead of relying on lazy
|
|
4034
|
-
// reloading during initialize().
|
|
4035
|
-
serverId: session.serverId,
|
|
4036
|
-
serverName: session.serverName,
|
|
4037
|
-
serverUrl: session.serverUrl,
|
|
4038
|
-
callbackUrl: session.callbackUrl,
|
|
4039
|
-
// NOTE: transportType is intentionally omitted here.
|
|
4040
|
-
// The session's stored transportType is a placeholder ('streamable-http')
|
|
4041
|
-
// set before transport negotiation. Omitting it lets MCPClient auto-negotiate
|
|
4042
|
-
// (try streamable_http → SSE fallback), which is critical for servers like
|
|
4043
|
-
// Neon that only support SSE transport.
|
|
4044
|
-
headers: session.headers
|
|
4045
|
-
});
|
|
4046
|
-
client.onConnectionEvent((event) => this.emitConnectionEvent(event));
|
|
4047
|
-
await client.finishAuth(code, oauthState);
|
|
4048
|
-
this.clients.set(sessionId, client);
|
|
4049
|
-
const { result: tools } = await this.listPolicyFilteredTools(sessionId);
|
|
4050
|
-
return { success: true, toolCount: tools.tools.length };
|
|
4051
|
-
} catch (error) {
|
|
4052
|
-
this.emitConnectionEvent({
|
|
4053
|
-
type: "error",
|
|
4054
|
-
sessionId,
|
|
4055
|
-
serverId: session.serverId ?? "unknown",
|
|
4056
|
-
error: error instanceof Error ? error.message : "OAuth completion failed",
|
|
4057
|
-
errorType: "auth",
|
|
4058
|
-
timestamp: Date.now()
|
|
4059
|
-
});
|
|
4060
|
-
throw error;
|
|
4061
|
-
}
|
|
4133
|
+
async callTool(params) {
|
|
4134
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4135
|
+
const result = await createToolPolicyGateway(
|
|
4136
|
+
this.userId,
|
|
4137
|
+
params.sessionId,
|
|
4138
|
+
client
|
|
4139
|
+
).callTool(params.toolName, params.toolArgs);
|
|
4140
|
+
return { ...result, _meta: { ...result._meta ?? {}, sessionId: params.sessionId } };
|
|
4062
4141
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4142
|
+
// -----------------------------------------------------------------------
|
|
4143
|
+
// Prompts
|
|
4144
|
+
// -----------------------------------------------------------------------
|
|
4145
|
+
/** Lists all prompts exposed by the remote MCP server. */
|
|
4066
4146
|
async listPrompts(params) {
|
|
4067
|
-
const
|
|
4068
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4147
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4069
4148
|
const result = await client.listPrompts();
|
|
4070
4149
|
return { prompts: result.prompts };
|
|
4071
4150
|
}
|
|
4072
|
-
/**
|
|
4073
|
-
* Get a specific prompt
|
|
4074
|
-
*/
|
|
4151
|
+
/** Retrieves a specific prompt by name with optional arguments. */
|
|
4075
4152
|
async getPrompt(params) {
|
|
4076
|
-
const
|
|
4077
|
-
|
|
4078
|
-
return await client.getPrompt(name, args);
|
|
4153
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4154
|
+
return client.getPrompt(params.name, params.args);
|
|
4079
4155
|
}
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4156
|
+
// -----------------------------------------------------------------------
|
|
4157
|
+
// Resources
|
|
4158
|
+
// -----------------------------------------------------------------------
|
|
4159
|
+
/** Lists all resources exposed by the remote MCP server. */
|
|
4083
4160
|
async listResources(params) {
|
|
4084
|
-
const
|
|
4085
|
-
const client = await this.getOrCreateClient(sessionId);
|
|
4161
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4086
4162
|
const result = await client.listResources();
|
|
4087
4163
|
return { resources: result.resources };
|
|
4088
4164
|
}
|
|
4165
|
+
/** Reads a specific resource identified by URI. */
|
|
4166
|
+
async readResource(params) {
|
|
4167
|
+
const client = await this.getOrCreateClient(params.sessionId);
|
|
4168
|
+
return client.readResource(params.uri);
|
|
4169
|
+
}
|
|
4170
|
+
// -----------------------------------------------------------------------
|
|
4171
|
+
// Internal Helpers
|
|
4172
|
+
// -----------------------------------------------------------------------
|
|
4173
|
+
/** Resolves client metadata: `getClientMetadata()` → `clientDefaults` → `{}`. */
|
|
4174
|
+
async getResolvedClientMetadata(request) {
|
|
4175
|
+
let metadata = this.options.clientDefaults ? { ...this.options.clientDefaults } : {};
|
|
4176
|
+
if (this.options.getClientMetadata) {
|
|
4177
|
+
metadata = { ...metadata, ...await this.options.getClientMetadata(request) };
|
|
4178
|
+
}
|
|
4179
|
+
return metadata;
|
|
4180
|
+
}
|
|
4181
|
+
/** Ensures the given session exists in storage and throws otherwise. */
|
|
4182
|
+
async requireSession(sessionId) {
|
|
4183
|
+
const session = await sessions.get(this.userId, sessionId, { includeCredentials: true });
|
|
4184
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
4185
|
+
return session;
|
|
4186
|
+
}
|
|
4187
|
+
/** Finds an existing session matching the given serverId or serverUrl. */
|
|
4188
|
+
async findExistingSession(serverId, serverUrl) {
|
|
4189
|
+
const all = await sessions.list(this.userId);
|
|
4190
|
+
return all.find((s) => s.serverId === serverId || s.serverUrl === serverUrl);
|
|
4191
|
+
}
|
|
4192
|
+
/** Normalizes a serverId to max 12 chars (DeepSeek/OpenAI 64-char tool-name limit). */
|
|
4193
|
+
normalizeServerId(raw) {
|
|
4194
|
+
return raw && raw.length <= 12 ? raw : generateServerId();
|
|
4195
|
+
}
|
|
4089
4196
|
/**
|
|
4090
|
-
*
|
|
4197
|
+
* Returns the cached in-memory transport for `sessionId`, or creates one
|
|
4198
|
+
* from the persisted session row (with credentials) and connects it.
|
|
4091
4199
|
*/
|
|
4092
|
-
async
|
|
4093
|
-
const
|
|
4094
|
-
|
|
4095
|
-
|
|
4200
|
+
async getOrCreateClient(sessionId) {
|
|
4201
|
+
const existing = this.clients.get(sessionId);
|
|
4202
|
+
if (existing) return existing;
|
|
4203
|
+
const session = await this.requireSession(sessionId);
|
|
4204
|
+
if (session.enabled === false) {
|
|
4205
|
+
throw new Error("Session is disabled \u2014 re-enable it via updateSession to access tools");
|
|
4206
|
+
}
|
|
4207
|
+
const client = this.restoreClient(session);
|
|
4208
|
+
this.attachClientEvents(client);
|
|
4209
|
+
await client.connect();
|
|
4210
|
+
this.clients.set(sessionId, client);
|
|
4211
|
+
return client;
|
|
4096
4212
|
}
|
|
4097
4213
|
/**
|
|
4098
|
-
*
|
|
4214
|
+
* Builds an `MCPClient` from a stored session row.
|
|
4215
|
+
*
|
|
4216
|
+
* Extracts `clientId` and `clientSecret` from the session's credential
|
|
4217
|
+
* fields and passes `hasSession: true` + `cachedCredentials` so the client
|
|
4218
|
+
* can skip redundant existence checks and credential reads.
|
|
4099
4219
|
*/
|
|
4100
|
-
|
|
4101
|
-
|
|
4220
|
+
restoreClient(session) {
|
|
4221
|
+
const clientId = session.clientId ?? void 0;
|
|
4222
|
+
const clientSecret = session.clientInformation?.client_secret ?? void 0;
|
|
4223
|
+
return new MCPClient({
|
|
4224
|
+
userId: this.userId,
|
|
4225
|
+
sessionId: session.sessionId,
|
|
4226
|
+
serverId: session.serverId,
|
|
4227
|
+
serverName: session.serverName,
|
|
4228
|
+
serverUrl: session.serverUrl,
|
|
4229
|
+
callbackUrl: session.callbackUrl,
|
|
4230
|
+
transportType: session.transportType,
|
|
4231
|
+
headers: session.headers,
|
|
4232
|
+
clientId,
|
|
4233
|
+
clientSecret,
|
|
4234
|
+
hasSession: true,
|
|
4235
|
+
cachedCredentials: { tokens: session.tokens ?? void 0 },
|
|
4236
|
+
sessionStore: this.observedStore
|
|
4237
|
+
});
|
|
4102
4238
|
}
|
|
4103
4239
|
/**
|
|
4104
|
-
*
|
|
4240
|
+
* Wires connection and observability events from the client into the
|
|
4241
|
+
* unified observability channel. Connection events go to `sendEvent`
|
|
4242
|
+
* (SSE stream); observability events go to `emitObs` (user callback
|
|
4243
|
+
* + SSE stream).
|
|
4105
4244
|
*/
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4245
|
+
attachClientEvents(client) {
|
|
4246
|
+
client.onConnectionEvent((e) => this.sendEvent(e));
|
|
4247
|
+
client.onObservabilityEvent((e) => this.sendEvent(e));
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Registers the client in the in-memory cache and attaches its event
|
|
4251
|
+
* listeners to the unified observability channel.
|
|
4252
|
+
*/
|
|
4253
|
+
cacheClient(sessionId, client) {
|
|
4254
|
+
this.attachClientEvents(client);
|
|
4255
|
+
this.clients.set(sessionId, client);
|
|
4256
|
+
}
|
|
4257
|
+
/**
|
|
4258
|
+
* Attempts a `client.connect()` and, on success, discovers tools.
|
|
4259
|
+
*
|
|
4260
|
+
* If the server responds with `UnauthorizedError`, the session is
|
|
4261
|
+
* treated as pending OAuth — a successful result is returned with
|
|
4262
|
+
* the sessionId so the browser client can redirect to the auth URL.
|
|
4263
|
+
*
|
|
4264
|
+
* For any other error an `error` connection event is emitted and the
|
|
4265
|
+
* client is removed from the in-memory cache before re-throwing.
|
|
4266
|
+
*/
|
|
4267
|
+
async connectAndDiscover(client, sessionId, serverId) {
|
|
4268
|
+
try {
|
|
4269
|
+
await client.connect();
|
|
4270
|
+
await this.listPolicyFilteredTools(sessionId);
|
|
4271
|
+
return { sessionId, success: true };
|
|
4272
|
+
} catch (error) {
|
|
4273
|
+
if (error instanceof UnauthorizedError) {
|
|
4274
|
+
this.clients.delete(sessionId);
|
|
4275
|
+
return { sessionId, success: true };
|
|
4276
|
+
}
|
|
4277
|
+
this.sendEvent(connectionErrorEvent(sessionId, serverId, error, "connection"));
|
|
4278
|
+
this.clients.delete(sessionId);
|
|
4279
|
+
throw error;
|
|
4110
4280
|
}
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
this.
|
|
4281
|
+
}
|
|
4282
|
+
/** Starts the periodic heartbeat timer. */
|
|
4283
|
+
startHeartbeat() {
|
|
4284
|
+
const ms = this.options.heartbeatInterval ?? DEFAULT_HEARTBEAT_MS;
|
|
4285
|
+
this.heartbeatTimer = setInterval(() => {
|
|
4286
|
+
if (this.active) {
|
|
4287
|
+
this.sendEvent({ level: "debug", message: "heartbeat", timestamp: Date.now() });
|
|
4288
|
+
}
|
|
4289
|
+
}, ms);
|
|
4115
4290
|
}
|
|
4116
4291
|
};
|
|
4117
4292
|
function createSSEHandler(options) {
|
|
@@ -4140,8 +4315,7 @@ function createSSEHandler(options) {
|
|
|
4140
4315
|
});
|
|
4141
4316
|
req.on("end", async () => {
|
|
4142
4317
|
try {
|
|
4143
|
-
|
|
4144
|
-
await manager.handleRequest(request);
|
|
4318
|
+
await manager.handleRequest(JSON.parse(body));
|
|
4145
4319
|
} catch {
|
|
4146
4320
|
}
|
|
4147
4321
|
});
|
|
@@ -4347,6 +4521,9 @@ var SSEClient = class {
|
|
|
4347
4521
|
async getToolPolicy(sessionId) {
|
|
4348
4522
|
return this.sendRequest("getToolPolicy", { sessionId });
|
|
4349
4523
|
}
|
|
4524
|
+
async updateSession(sessionId, enabled) {
|
|
4525
|
+
return this.sendRequest("updateSession", { sessionId, enabled });
|
|
4526
|
+
}
|
|
4350
4527
|
async listTools(sessionId) {
|
|
4351
4528
|
return this.sendRequest("listTools", { sessionId });
|
|
4352
4529
|
}
|
|
@@ -6213,6 +6390,6 @@ function globToRegExp(pattern) {
|
|
|
6213
6390
|
return new RegExp(regexPattern);
|
|
6214
6391
|
}
|
|
6215
6392
|
|
|
6216
|
-
export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions };
|
|
6393
|
+
export { APP_HOST_DEFAULTS, AppHost, AuthenticationError, ConfigurationError, ConnectionError, DEFAULT_CLIENT_NAME, DEFAULT_CLIENT_URI, DEFAULT_HEARTBEAT_INTERVAL_MS, DEFAULT_LOGO_URI, DEFAULT_MCP_APP_CSP, DEFAULT_POLICY_URI, DORMANT_SESSION_EXPIRATION_MS, DORMANT_SESSION_EXPIRATION_SECONDS, DisposableStore, Emitter, InvalidStateError, MCPClient, MCP_CLIENT_NAME, MCP_CLIENT_VERSION, McpError, MultiSessionClient, NotConnectedError, PENDING_SESSION_EXPIRATION_SECONDS, REDIS_KEY_PREFIX, RpcErrorCodes, SANDBOX_PROXY_READY_METHOD, SANDBOX_RESOURCE_READY_METHOD, SOFTWARE_ID, SOFTWARE_VERSION, SSEClient, SSEConnectionManager, STATE_EXPIRATION_MS, SchemaCompressor, SessionNotFoundError, SessionValidationError, StorageOAuthClientProvider, ToolExecutionError, ToolIndex, ToolRouter, UnauthorizedError, createExecuteToolDefinition, createGetSchemaToolDefinition, createListServersToolDefinition, createNextMcpHandler, createRegexSearchToolDefinition, createSSEHandler, createSearchToolDefinition, decryptObject, encryptObject, executeMetaTool, findToolByName, getToolUiResourceUri, isCallToolSuccess, isConnectAuthRequired, isConnectError, isConnectSuccess, isListToolsSuccess, isMetaTool, onSessionMutation, resolveMetaToolProxy, sanitizeServerLabel, sessions, withDbObservability };
|
|
6217
6394
|
//# sourceMappingURL=index.mjs.map
|
|
6218
6395
|
//# sourceMappingURL=index.mjs.map
|