@mcp-ts/sdk 1.0.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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -0
  3. package/dist/adapters/agui-adapter.d.mts +119 -0
  4. package/dist/adapters/agui-adapter.d.ts +119 -0
  5. package/dist/adapters/agui-adapter.js +109 -0
  6. package/dist/adapters/agui-adapter.js.map +1 -0
  7. package/dist/adapters/agui-adapter.mjs +107 -0
  8. package/dist/adapters/agui-adapter.mjs.map +1 -0
  9. package/dist/adapters/agui-middleware.d.mts +171 -0
  10. package/dist/adapters/agui-middleware.d.ts +171 -0
  11. package/dist/adapters/agui-middleware.js +429 -0
  12. package/dist/adapters/agui-middleware.js.map +1 -0
  13. package/dist/adapters/agui-middleware.mjs +417 -0
  14. package/dist/adapters/agui-middleware.mjs.map +1 -0
  15. package/dist/adapters/ai-adapter.d.mts +38 -0
  16. package/dist/adapters/ai-adapter.d.ts +38 -0
  17. package/dist/adapters/ai-adapter.js +82 -0
  18. package/dist/adapters/ai-adapter.js.map +1 -0
  19. package/dist/adapters/ai-adapter.mjs +80 -0
  20. package/dist/adapters/ai-adapter.mjs.map +1 -0
  21. package/dist/adapters/langchain-adapter.d.mts +46 -0
  22. package/dist/adapters/langchain-adapter.d.ts +46 -0
  23. package/dist/adapters/langchain-adapter.js +102 -0
  24. package/dist/adapters/langchain-adapter.js.map +1 -0
  25. package/dist/adapters/langchain-adapter.mjs +100 -0
  26. package/dist/adapters/langchain-adapter.mjs.map +1 -0
  27. package/dist/adapters/mastra-adapter.d.mts +49 -0
  28. package/dist/adapters/mastra-adapter.d.ts +49 -0
  29. package/dist/adapters/mastra-adapter.js +95 -0
  30. package/dist/adapters/mastra-adapter.js.map +1 -0
  31. package/dist/adapters/mastra-adapter.mjs +93 -0
  32. package/dist/adapters/mastra-adapter.mjs.map +1 -0
  33. package/dist/client/index.d.mts +119 -0
  34. package/dist/client/index.d.ts +119 -0
  35. package/dist/client/index.js +225 -0
  36. package/dist/client/index.js.map +1 -0
  37. package/dist/client/index.mjs +223 -0
  38. package/dist/client/index.mjs.map +1 -0
  39. package/dist/client/react.d.mts +151 -0
  40. package/dist/client/react.d.ts +151 -0
  41. package/dist/client/react.js +492 -0
  42. package/dist/client/react.js.map +1 -0
  43. package/dist/client/react.mjs +489 -0
  44. package/dist/client/react.mjs.map +1 -0
  45. package/dist/client/vue.d.mts +157 -0
  46. package/dist/client/vue.d.ts +157 -0
  47. package/dist/client/vue.js +474 -0
  48. package/dist/client/vue.js.map +1 -0
  49. package/dist/client/vue.mjs +471 -0
  50. package/dist/client/vue.mjs.map +1 -0
  51. package/dist/events-BP6WyRNh.d.mts +110 -0
  52. package/dist/events-BP6WyRNh.d.ts +110 -0
  53. package/dist/index.d.mts +10 -0
  54. package/dist/index.d.ts +10 -0
  55. package/dist/index.js +2784 -0
  56. package/dist/index.js.map +1 -0
  57. package/dist/index.mjs +2723 -0
  58. package/dist/index.mjs.map +1 -0
  59. package/dist/multi-session-client-BOFgPypS.d.ts +389 -0
  60. package/dist/multi-session-client-DMF3ED2O.d.mts +389 -0
  61. package/dist/server/index.d.mts +269 -0
  62. package/dist/server/index.d.ts +269 -0
  63. package/dist/server/index.js +2444 -0
  64. package/dist/server/index.js.map +1 -0
  65. package/dist/server/index.mjs +2414 -0
  66. package/dist/server/index.mjs.map +1 -0
  67. package/dist/shared/index.d.mts +24 -0
  68. package/dist/shared/index.d.ts +24 -0
  69. package/dist/shared/index.js +223 -0
  70. package/dist/shared/index.js.map +1 -0
  71. package/dist/shared/index.mjs +190 -0
  72. package/dist/shared/index.mjs.map +1 -0
  73. package/dist/types-SbDlA2VX.d.mts +153 -0
  74. package/dist/types-SbDlA2VX.d.ts +153 -0
  75. package/dist/utils-0qmYrqoa.d.mts +92 -0
  76. package/dist/utils-0qmYrqoa.d.ts +92 -0
  77. package/package.json +165 -0
  78. package/src/adapters/agui-adapter.ts +210 -0
  79. package/src/adapters/agui-middleware.ts +512 -0
  80. package/src/adapters/ai-adapter.ts +115 -0
  81. package/src/adapters/langchain-adapter.ts +127 -0
  82. package/src/adapters/mastra-adapter.ts +126 -0
  83. package/src/client/core/sse-client.ts +340 -0
  84. package/src/client/index.ts +26 -0
  85. package/src/client/react/index.ts +10 -0
  86. package/src/client/react/useMcp.ts +558 -0
  87. package/src/client/vue/index.ts +10 -0
  88. package/src/client/vue/useMcp.ts +542 -0
  89. package/src/index.ts +11 -0
  90. package/src/server/handlers/nextjs-handler.ts +216 -0
  91. package/src/server/handlers/sse-handler.ts +699 -0
  92. package/src/server/index.ts +57 -0
  93. package/src/server/mcp/multi-session-client.ts +132 -0
  94. package/src/server/mcp/oauth-client.ts +1168 -0
  95. package/src/server/mcp/storage-oauth-provider.ts +239 -0
  96. package/src/server/storage/file-backend.ts +169 -0
  97. package/src/server/storage/index.ts +115 -0
  98. package/src/server/storage/memory-backend.ts +132 -0
  99. package/src/server/storage/redis-backend.ts +210 -0
  100. package/src/server/storage/redis.ts +160 -0
  101. package/src/server/storage/types.ts +109 -0
  102. package/src/shared/constants.ts +29 -0
  103. package/src/shared/errors.ts +133 -0
  104. package/src/shared/events.ts +166 -0
  105. package/src/shared/index.ts +70 -0
  106. package/src/shared/types.ts +274 -0
  107. package/src/shared/utils.ts +16 -0
@@ -0,0 +1,239 @@
1
+
2
+ import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
3
+ import type {
4
+ OAuthClientInformation,
5
+ OAuthClientInformationFull,
6
+ OAuthClientMetadata,
7
+ OAuthTokens
8
+ } from "@modelcontextprotocol/sdk/shared/auth.js";
9
+ import { storage, SessionData } from "../storage/index.js";
10
+ import { TOKEN_EXPIRY_BUFFER_MS } from '../../shared/constants.js';
11
+
12
+ /**
13
+ * Extension of OAuthClientProvider interface with additional methods
14
+ * Enables server-specific tracking and state management
15
+ */
16
+ export interface AgentsOAuthProvider extends OAuthClientProvider {
17
+ authUrl: string | undefined;
18
+ clientId: string | undefined;
19
+ serverId: string | undefined;
20
+ checkState(
21
+ state: string
22
+ ): Promise<{ valid: boolean; serverId?: string; error?: string }>;
23
+ consumeState(state: string): Promise<void>;
24
+ deleteCodeVerifier(): Promise<void>;
25
+ isTokenExpired(): boolean;
26
+ setTokenExpiresAt(expiresAt: number): void;
27
+ }
28
+
29
+ /**
30
+ * Storage-backed OAuth provider implementation for MCP
31
+ * Stores OAuth tokens, client information, and PKCE verifiers using the configured StorageBackend
32
+ */
33
+ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
34
+ private _authUrl: string | undefined;
35
+ private _clientId: string | undefined;
36
+ private onRedirectCallback?: (url: string) => void;
37
+ private tokenExpiresAt?: number;
38
+
39
+ /**
40
+ * Creates a new Storage-backed OAuth provider
41
+ * @param identity - User/Client identifier
42
+ * @param serverId - Server identifier (for tracking which server this OAuth session belongs to)
43
+ * @param sessionId - Session identifier (used as OAuth state)
44
+ * @param clientName - OAuth client name
45
+ * @param baseRedirectUrl - OAuth callback URL
46
+ * @param onRedirect - Optional callback when redirect to authorization is needed
47
+ */
48
+ constructor(
49
+ public identity: string,
50
+ public serverId: string,
51
+ public sessionId: string,
52
+ public clientName: string,
53
+ public baseRedirectUrl: string,
54
+ onRedirect?: (url: string) => void
55
+ ) {
56
+ this.onRedirectCallback = onRedirect;
57
+ }
58
+
59
+ get clientMetadata(): OAuthClientMetadata {
60
+ return {
61
+ client_name: this.clientName,
62
+ client_uri: this.clientUri,
63
+ grant_types: ["authorization_code", "refresh_token"],
64
+ redirect_uris: [this.redirectUrl],
65
+ response_types: ["code"],
66
+ token_endpoint_auth_method: "none",
67
+ ...(this._clientId ? { client_id: this._clientId } : {})
68
+ };
69
+ }
70
+
71
+ get clientUri() {
72
+ return new URL(this.redirectUrl).origin;
73
+ }
74
+
75
+ get redirectUrl() {
76
+ return this.baseRedirectUrl;
77
+ }
78
+
79
+ get clientId() {
80
+ return this._clientId;
81
+ }
82
+
83
+ set clientId(clientId_: string | undefined) {
84
+ this._clientId = clientId_;
85
+ }
86
+
87
+ /**
88
+ * Loads OAuth data from storage session
89
+ * @private
90
+ */
91
+ private async getSessionData(): Promise<SessionData> {
92
+ const data = await storage.getSession(this.identity, this.sessionId);
93
+ if (!data) {
94
+ // Return empty/partial object if not found
95
+ return {} as SessionData;
96
+ }
97
+ return data;
98
+ }
99
+
100
+ /**
101
+ * Saves OAuth data to storage
102
+ * @param data - Partial OAuth data to save
103
+ * @private
104
+ * @throws Error if session doesn't exist (session must be created by controller layer)
105
+ */
106
+ private async saveSessionData(data: Partial<SessionData>): Promise<void> {
107
+ await storage.updateSession(this.identity, this.sessionId, data);
108
+ }
109
+
110
+ /**
111
+ * Retrieves stored OAuth client information
112
+ */
113
+ async clientInformation(): Promise<OAuthClientInformation | undefined> {
114
+ const data = await this.getSessionData();
115
+
116
+ if (data.clientId && !this._clientId) {
117
+ this._clientId = data.clientId;
118
+ }
119
+
120
+ return data.clientInformation;
121
+ }
122
+
123
+ /**
124
+ * Stores OAuth client information
125
+ */
126
+ async saveClientInformation(clientInformation: OAuthClientInformationFull): Promise<void> {
127
+ await this.saveSessionData({
128
+ clientInformation,
129
+ clientId: clientInformation.client_id
130
+ });
131
+ this.clientId = clientInformation.client_id;
132
+ }
133
+
134
+ /**
135
+ * Stores OAuth tokens
136
+ */
137
+ async saveTokens(tokens: OAuthTokens): Promise<void> {
138
+ const data: Partial<SessionData> = { tokens };
139
+
140
+ if (tokens.expires_in) {
141
+ this.tokenExpiresAt = Date.now() + (tokens.expires_in * 1000) - TOKEN_EXPIRY_BUFFER_MS;
142
+ }
143
+
144
+ await this.saveSessionData(data);
145
+ }
146
+
147
+ get authUrl() {
148
+ return this._authUrl;
149
+ }
150
+
151
+ async state(): Promise<string> {
152
+ return this.sessionId;
153
+ }
154
+
155
+ async checkState(state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
156
+ const data = await storage.getSession(this.identity, this.sessionId);
157
+
158
+ if (!data) {
159
+ return { valid: false, error: "Session not found" };
160
+ }
161
+
162
+ return { valid: true, serverId: this.serverId };
163
+ }
164
+
165
+ async consumeState(state: string): Promise<void> {
166
+ // No-op
167
+ }
168
+
169
+ async redirectToAuthorization(authUrl: URL): Promise<void> {
170
+ this._authUrl = authUrl.toString();
171
+ if (this.onRedirectCallback) {
172
+ this.onRedirectCallback(authUrl.toString());
173
+ }
174
+ }
175
+
176
+ async invalidateCredentials(
177
+ scope: "all" | "client" | "tokens" | "verifier"
178
+ ): Promise<void> {
179
+ if (scope === "all") {
180
+ await storage.removeSession(this.identity, this.sessionId);
181
+ } else {
182
+ const data = await this.getSessionData();
183
+ // Create a copy to modify
184
+ const updates: Partial<SessionData> = {};
185
+
186
+ if (scope === "client") {
187
+ updates.clientInformation = undefined;
188
+ updates.clientId = undefined;
189
+ } else if (scope === "tokens") {
190
+ updates.tokens = undefined;
191
+ } else if (scope === "verifier") {
192
+ updates.codeVerifier = undefined;
193
+ }
194
+ await this.saveSessionData(updates);
195
+ }
196
+ }
197
+
198
+ async saveCodeVerifier(verifier: string): Promise<void> {
199
+ await this.saveSessionData({ codeVerifier: verifier });
200
+ }
201
+
202
+ async codeVerifier(): Promise<string> {
203
+ const data = await this.getSessionData();
204
+
205
+ if (data.clientId && !this._clientId) {
206
+ this._clientId = data.clientId;
207
+ }
208
+
209
+ if (!data.codeVerifier) {
210
+ throw new Error("No code verifier found");
211
+ }
212
+ return data.codeVerifier;
213
+ }
214
+
215
+ async deleteCodeVerifier(): Promise<void> {
216
+ await this.saveSessionData({ codeVerifier: undefined });
217
+ }
218
+
219
+ async tokens(): Promise<OAuthTokens | undefined> {
220
+ const data = await this.getSessionData();
221
+
222
+ if (data.clientId && !this._clientId) {
223
+ this._clientId = data.clientId;
224
+ }
225
+
226
+ return data.tokens;
227
+ }
228
+
229
+ isTokenExpired(): boolean {
230
+ if (!this.tokenExpiresAt) {
231
+ return false;
232
+ }
233
+ return Date.now() >= this.tokenExpiresAt;
234
+ }
235
+
236
+ setTokenExpiresAt(expiresAt: number): void {
237
+ this.tokenExpiresAt = expiresAt;
238
+ }
239
+ }
@@ -0,0 +1,169 @@
1
+
2
+ import { promises as fs } from 'fs';
3
+ import * as path from 'path';
4
+ import { customAlphabet } from 'nanoid';
5
+ import { StorageBackend, SessionData, SetClientOptions } from './types';
6
+
7
+ // first char: letters only (required by OpenAI)
8
+ const firstChar = customAlphabet(
9
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
10
+ 1
11
+ );
12
+
13
+ // remaining chars: alphanumeric
14
+ const rest = customAlphabet(
15
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
16
+ 11
17
+ );
18
+
19
+ /**
20
+ * File system implementation of StorageBackend
21
+ * Persists sessions to a JSON file
22
+ */
23
+ export class FileStorageBackend implements StorageBackend {
24
+ private filePath: string;
25
+ private memoryCache: Map<string, SessionData> | null = null;
26
+ private initialized = false;
27
+
28
+ /**
29
+ * @param options.path Path to the JSON file storage (default: ./sessions.json)
30
+ */
31
+ constructor(options: { path?: string } = {}) {
32
+ this.filePath = options.path || './sessions.json';
33
+ }
34
+
35
+ /**
36
+ * Initialize storage: ensure file exists and load into memory cache
37
+ */
38
+ async init(): Promise<void> {
39
+ if (this.initialized) return;
40
+
41
+ try {
42
+ // Ensure directory exists
43
+ const dir = path.dirname(this.filePath);
44
+ await fs.mkdir(dir, { recursive: true });
45
+
46
+ // Try to read file
47
+ const data = await fs.readFile(this.filePath, 'utf-8');
48
+ const json = JSON.parse(data);
49
+
50
+ this.memoryCache = new Map();
51
+ if (Array.isArray(json)) {
52
+ json.forEach((s: SessionData) => {
53
+ this.memoryCache!.set(this.getSessionKey(s.identity || 'unknown', s.sessionId), s);
54
+ });
55
+ }
56
+ } catch (error: any) {
57
+ if (error.code === 'ENOENT') {
58
+ // File does not exist, initialize empty
59
+ this.memoryCache = new Map();
60
+ await this.flush();
61
+ } else {
62
+ console.error('[FileStorage] Failed to load sessions:', error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ this.initialized = true;
68
+ }
69
+
70
+ private async ensureInitialized() {
71
+ if (!this.initialized) await this.init();
72
+ }
73
+
74
+ private async flush(): Promise<void> {
75
+ if (!this.memoryCache) return;
76
+ const sessions = Array.from(this.memoryCache.values());
77
+ await fs.writeFile(this.filePath, JSON.stringify(sessions, null, 2), 'utf-8');
78
+ }
79
+
80
+ private getSessionKey(identity: string, sessionId: string): string {
81
+ return `${identity}:${sessionId}`;
82
+ }
83
+
84
+ generateSessionId(): string {
85
+ return firstChar() + rest();
86
+ }
87
+
88
+ async createSession(session: SessionData, ttl?: number): Promise<void> {
89
+ await this.ensureInitialized();
90
+ const { sessionId, identity } = session;
91
+ if (!sessionId || !identity) throw new Error('identity and sessionId required');
92
+
93
+ const sessionKey = this.getSessionKey(identity, sessionId);
94
+ if (this.memoryCache!.has(sessionKey)) {
95
+ throw new Error(`Session ${sessionId} already exists`);
96
+ }
97
+
98
+ this.memoryCache!.set(sessionKey, session);
99
+ await this.flush();
100
+ // Note: TTL is ignored in file backend - sessions don't auto-expire
101
+ }
102
+
103
+ async updateSession(identity: string, sessionId: string, data: Partial<SessionData>, ttl?: number): Promise<void> {
104
+ await this.ensureInitialized();
105
+ if (!identity || !sessionId) throw new Error('identity and sessionId required');
106
+
107
+ const sessionKey = this.getSessionKey(identity, sessionId);
108
+ const current = this.memoryCache!.get(sessionKey);
109
+
110
+ if (!current) {
111
+ throw new Error(`Session ${sessionId} not found`);
112
+ }
113
+
114
+ const updated = {
115
+ ...current,
116
+ ...data
117
+ };
118
+
119
+ this.memoryCache!.set(sessionKey, updated);
120
+ await this.flush();
121
+ // Note: TTL is ignored in file backend - sessions don't auto-expire
122
+ }
123
+
124
+ async getSession(identity: string, sessionId: string): Promise<SessionData | null> {
125
+ await this.ensureInitialized();
126
+ const sessionKey = this.getSessionKey(identity, sessionId);
127
+ return this.memoryCache!.get(sessionKey) || null;
128
+ }
129
+
130
+ async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
131
+ await this.ensureInitialized();
132
+ return Array.from(this.memoryCache!.values()).filter(s => s.identity === identity);
133
+ }
134
+
135
+ async getIdentityMcpSessions(identity: string): Promise<string[]> {
136
+ await this.ensureInitialized();
137
+ return Array.from(this.memoryCache!.values())
138
+ .filter(s => s.identity === identity)
139
+ .map(s => s.sessionId);
140
+ }
141
+
142
+ async removeSession(identity: string, sessionId: string): Promise<void> {
143
+ await this.ensureInitialized();
144
+ const sessionKey = this.getSessionKey(identity, sessionId);
145
+ if (this.memoryCache!.delete(sessionKey)) {
146
+ await this.flush();
147
+ }
148
+ }
149
+
150
+ async getAllSessionIds(): Promise<string[]> {
151
+ await this.ensureInitialized();
152
+ return Array.from(this.memoryCache!.values()).map(s => s.sessionId);
153
+ }
154
+
155
+ async clearAll(): Promise<void> {
156
+ await this.ensureInitialized();
157
+ this.memoryCache!.clear();
158
+ await this.flush();
159
+ }
160
+
161
+ async cleanupExpiredSessions(): Promise<void> {
162
+ // Could implement TTL check here using createdAt
163
+ await this.ensureInitialized();
164
+ }
165
+
166
+ async disconnect(): Promise<void> {
167
+ // No explicit disconnect needed for file
168
+ }
169
+ }
@@ -0,0 +1,115 @@
1
+
2
+ import { RedisStorageBackend } from './redis-backend';
3
+ import { MemoryStorageBackend } from './memory-backend';
4
+ import { FileStorageBackend } from './file-backend';
5
+ import type { StorageBackend } from './types';
6
+
7
+ // Re-export types
8
+ export * from './types';
9
+ export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend };
10
+
11
+ let storageInstance: StorageBackend | null = null;
12
+ let storagePromise: Promise<StorageBackend> | null = null;
13
+
14
+ async function createStorage(): Promise<StorageBackend> {
15
+ const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
16
+
17
+ // Explicit selection
18
+ if (type === 'redis') {
19
+ if (!process.env.REDIS_URL) {
20
+ console.warn('[Storage] MCP_TS_STORAGE_TYPE is "redis" but REDIS_URL is missing');
21
+ }
22
+ try {
23
+ const { getRedis } = await import('./redis.js');
24
+ const redis = await getRedis();
25
+ console.log('[Storage] Using Redis storage (Explicit)');
26
+ return new RedisStorageBackend(redis);
27
+ } catch (error: any) {
28
+ console.error('[Storage] Failed to initialize Redis:', error.message);
29
+ console.log('[Storage] Falling back to In-Memory storage');
30
+ return new MemoryStorageBackend();
31
+ }
32
+ }
33
+
34
+ if (type === 'file') {
35
+ const filePath = process.env.MCP_TS_STORAGE_FILE;
36
+ if (!filePath) {
37
+ console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
38
+ }
39
+ console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
40
+ const store = new FileStorageBackend({ path: filePath });
41
+ store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
42
+ return store;
43
+ }
44
+
45
+ if (type === 'memory') {
46
+ console.log('[Storage] Using In-Memory storage (Explicit)');
47
+ return new MemoryStorageBackend();
48
+ }
49
+
50
+ // Automatic inference (Fallback)
51
+ if (process.env.REDIS_URL) {
52
+ try {
53
+ const { getRedis } = await import('./redis.js');
54
+ const redis = await getRedis();
55
+ console.log('[Storage] Auto-detected REDIS_URL. Using Redis storage.');
56
+ return new RedisStorageBackend(redis);
57
+ } catch (error: any) {
58
+ console.error('[Storage] Redis auto-detection failed:', error.message);
59
+ console.log('[Storage] Falling back to In-Memory storage');
60
+ return new MemoryStorageBackend();
61
+ }
62
+ }
63
+
64
+ if (process.env.MCP_TS_STORAGE_FILE) {
65
+ console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
66
+ const store = new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE });
67
+ store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
68
+ return store;
69
+ }
70
+
71
+ console.log('[Storage] No storage configured. Using In-Memory storage (Default).');
72
+ return new MemoryStorageBackend();
73
+ }
74
+
75
+ async function getStorage(): Promise<StorageBackend> {
76
+ if (storageInstance) {
77
+ return storageInstance;
78
+ }
79
+
80
+ if (!storagePromise) {
81
+ storagePromise = createStorage();
82
+ }
83
+
84
+ storageInstance = await storagePromise;
85
+ return storageInstance;
86
+ }
87
+
88
+ /**
89
+ * Set the storage instance (for testing)
90
+ * @internal
91
+ * @param instance - StorageBackend instance or null to reset
92
+ */
93
+ export function _setStorageInstanceForTesting(instance: StorageBackend | null): void {
94
+ storageInstance = instance;
95
+ if (!instance) {
96
+ storagePromise = null;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Global session store instance
102
+ * Uses lazy initialization with a Proxy to handle async setup transparently
103
+ */
104
+ export const storage: StorageBackend = new Proxy({} as StorageBackend, {
105
+ get(_target, prop) {
106
+ return async (...args: any[]) => {
107
+ const instance = await getStorage();
108
+ const value = (instance as any)[prop];
109
+ if (typeof value === 'function') {
110
+ return value.apply(instance, args);
111
+ }
112
+ return value;
113
+ };
114
+ },
115
+ });
@@ -0,0 +1,132 @@
1
+
2
+ import { customAlphabet } from 'nanoid';
3
+ import { StorageBackend, SessionData, SetClientOptions } from './types';
4
+
5
+ // first char: letters only (required by OpenAI)
6
+ const firstChar = customAlphabet(
7
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz',
8
+ 1
9
+ );
10
+
11
+ // remaining chars: alphanumeric
12
+ const rest = customAlphabet(
13
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
14
+ 11
15
+ );
16
+
17
+ /**
18
+ * In-memory implementation of StorageBackend
19
+ * Useful for local development or testing
20
+ */
21
+ export class MemoryStorageBackend implements StorageBackend {
22
+ // Map<identity:sessionId, SessionData>
23
+ private sessions = new Map<string, SessionData>();
24
+
25
+ // Map<identity, Set<sessionId>>
26
+ private identitySessions = new Map<string, Set<string>>();
27
+
28
+ constructor() { }
29
+
30
+ private getSessionKey(identity: string, sessionId: string): string {
31
+ return `${identity}:${sessionId}`;
32
+ }
33
+
34
+ generateSessionId(): string {
35
+ return firstChar() + rest();
36
+ }
37
+
38
+ async createSession(session: SessionData, ttl?: number): Promise<void> {
39
+ const { sessionId, identity } = session;
40
+ if (!sessionId || !identity) throw new Error('identity and sessionId required');
41
+
42
+ const sessionKey = this.getSessionKey(identity, sessionId);
43
+ if (this.sessions.has(sessionKey)) {
44
+ throw new Error(`Session ${sessionId} already exists`);
45
+ }
46
+
47
+ this.sessions.set(sessionKey, session);
48
+
49
+ // Update index
50
+ if (!this.identitySessions.has(identity)) {
51
+ this.identitySessions.set(identity, new Set());
52
+ }
53
+ this.identitySessions.get(identity)!.add(sessionId);
54
+ // Note: TTL is ignored in memory backend - sessions don't auto-expire
55
+ }
56
+
57
+ async updateSession(identity: string, sessionId: string, data: Partial<SessionData>, ttl?: number): Promise<void> {
58
+ if (!identity || !sessionId) throw new Error('identity and sessionId required');
59
+
60
+ const sessionKey = this.getSessionKey(identity, sessionId);
61
+ const current = this.sessions.get(sessionKey);
62
+
63
+ if (!current) {
64
+ throw new Error(`Session ${sessionId} not found`);
65
+ }
66
+
67
+ const updated = {
68
+ ...current,
69
+ ...data
70
+ };
71
+
72
+ this.sessions.set(sessionKey, updated);
73
+ // Note: TTL is ignored in memory backend - sessions don't auto-expire
74
+ }
75
+
76
+
77
+ async getSession(identity: string, sessionId: string): Promise<SessionData | null> {
78
+ const sessionKey = this.getSessionKey(identity, sessionId);
79
+ return this.sessions.get(sessionKey) || null;
80
+ }
81
+
82
+ async getIdentityMcpSessions(identity: string): Promise<string[]> {
83
+ const set = this.identitySessions.get(identity);
84
+ return set ? Array.from(set) : [];
85
+ }
86
+
87
+ async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
88
+ const set = this.identitySessions.get(identity);
89
+ if (!set) return [];
90
+
91
+ const results: SessionData[] = [];
92
+ for (const sessionId of set) {
93
+ const session = this.sessions.get(this.getSessionKey(identity, sessionId));
94
+ if (session) {
95
+ results.push(session);
96
+ }
97
+ }
98
+ return results;
99
+ }
100
+
101
+ async removeSession(identity: string, sessionId: string): Promise<void> {
102
+ const sessionKey = this.getSessionKey(identity, sessionId);
103
+ this.sessions.delete(sessionKey);
104
+
105
+ const set = this.identitySessions.get(identity);
106
+ if (set) {
107
+ set.delete(sessionId);
108
+ if (set.size === 0) {
109
+ this.identitySessions.delete(identity);
110
+ }
111
+ }
112
+ }
113
+
114
+ async getAllSessionIds(): Promise<string[]> {
115
+ return Array.from(this.sessions.values()).map(s => s.sessionId);
116
+ }
117
+
118
+ async clearAll(): Promise<void> {
119
+ this.sessions.clear();
120
+ this.identitySessions.clear();
121
+ }
122
+
123
+ async cleanupExpiredSessions(): Promise<void> {
124
+ // In-memory doesn't implement TTL automatically,
125
+ // but we could check createdAt + TTL here if needed.
126
+ // For now, no-op.
127
+ }
128
+
129
+ async disconnect(): Promise<void> {
130
+ // No-op for memory
131
+ }
132
+ }