@mcp-ts/sdk 1.6.1 → 2.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.
- package/README.md +12 -6
- package/dist/adapters/agui-adapter.d.mts +3 -3
- package/dist/adapters/agui-adapter.d.ts +3 -3
- package/dist/adapters/agui-adapter.js +4 -5
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +4 -5
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +3 -3
- package/dist/adapters/agui-middleware.d.ts +3 -3
- package/dist/adapters/ai-adapter.d.mts +9 -3
- package/dist/adapters/ai-adapter.d.ts +9 -3
- package/dist/adapters/ai-adapter.js +20 -6
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +20 -6
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +3 -3
- package/dist/adapters/langchain-adapter.d.ts +3 -3
- package/dist/adapters/langchain-adapter.js +9 -6
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +9 -6
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.js +5 -1
- package/dist/adapters/mastra-adapter.js.map +1 -1
- package/dist/adapters/mastra-adapter.mjs +5 -1
- package/dist/adapters/mastra-adapter.mjs.map +1 -1
- package/dist/bin/mcp-ts.js +7 -1
- package/dist/bin/mcp-ts.js.map +1 -1
- package/dist/bin/mcp-ts.mjs +7 -1
- package/dist/bin/mcp-ts.mjs.map +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +9 -13
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +9 -13
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +7 -7
- package/dist/client/react.d.ts +7 -7
- package/dist/client/react.js +111 -63
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +111 -63
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +7 -7
- package/dist/client/vue.d.ts +7 -7
- package/dist/client/vue.js +14 -18
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +14 -18
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-DhA-OEAe.d.ts → index-C9gvpxy5.d.ts} +5 -5
- package/dist/{index-bFL4ZF2N.d.mts → index-eaH14_5u.d.mts} +5 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +616 -370
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +615 -370
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CHE8QpVE.d.ts → multi-session-client-BYtguGJm.d.ts} +22 -22
- package/dist/{multi-session-client-CQsRbxYI.d.mts → multi-session-client-DYNe6az3.d.mts} +22 -22
- package/dist/server/index.d.mts +31 -34
- package/dist/server/index.d.ts +31 -34
- package/dist/server/index.js +531 -256
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +530 -256
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +5 -5
- package/dist/shared/index.d.ts +5 -5
- package/dist/shared/index.js +76 -101
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +76 -101
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-Dh2804tM.d.ts → tool-router-Ddtybmr0.d.ts} +71 -73
- package/dist/{tool-router-BVaV1udm.d.mts → tool-router-Dnd6IOKC.d.mts} +71 -73
- package/dist/{types-rIuN1CQi.d.mts → types-BCAG20P6.d.mts} +4 -4
- package/dist/{types-rIuN1CQi.d.ts → types-BCAG20P6.d.ts} +4 -4
- package/dist/{utils-0qmYrqoa.d.mts → utils-DELRKQPU.d.mts} +1 -1
- package/dist/{utils-0qmYrqoa.d.ts → utils-DELRKQPU.d.ts} +1 -1
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +69 -0
- package/migrations/neon/20260513020000_add_session_cleanup_cron.sql +35 -0
- package/{supabase/migrations → migrations/supabase}/20260330195700_install_mcp_sessions.sql +7 -9
- package/package.json +14 -5
- package/src/adapters/ai-adapter.ts +30 -1
- package/src/adapters/langchain-adapter.ts +6 -2
- package/src/adapters/mastra-adapter.ts +6 -2
- package/src/bin/mcp-ts.ts +8 -1
- package/src/client/core/app-host.ts +1 -1
- package/src/client/core/sse-client.ts +12 -14
- package/src/client/core/types.ts +1 -1
- package/src/client/react/oauth-popup.tsx +111 -51
- package/src/client/react/use-mcp-apps.tsx +1 -1
- package/src/client/react/use-mcp.ts +11 -11
- package/src/client/vue/use-mcp.ts +10 -10
- package/src/server/handlers/nextjs-handler.ts +18 -15
- package/src/server/handlers/sse-handler.ts +29 -29
- package/src/server/index.ts +1 -1
- package/src/server/mcp/multi-session-client.ts +17 -17
- package/src/server/mcp/oauth-client.ts +37 -37
- package/src/server/mcp/storage-oauth-provider.ts +17 -17
- package/src/server/storage/file-backend.ts +25 -25
- package/src/server/storage/index.ts +67 -10
- package/src/server/storage/memory-backend.ts +34 -34
- package/src/server/storage/neon-backend.ts +281 -0
- package/src/server/storage/redis-backend.ts +64 -64
- package/src/server/storage/sqlite-backend.ts +33 -33
- package/src/server/storage/supabase-backend.ts +23 -24
- package/src/server/storage/types.ts +18 -21
- package/src/shared/errors.ts +1 -1
- package/src/shared/index.ts +1 -2
- package/src/shared/meta-tools.ts +4 -6
- package/src/shared/schema-compressor.ts +2 -42
- package/src/shared/tool-index.ts +89 -84
- package/src/shared/tool-router.ts +0 -24
- package/src/shared/types.ts +4 -4
- /package/{supabase/migrations → migrations/supabase}/20260421010000_add_session_cleanup_cron.sql +0 -0
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Redis } from 'ioredis';
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionStore, Session } from './types.js';
|
|
3
3
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Redis implementation of
|
|
7
|
+
* Redis implementation of SessionStore
|
|
8
8
|
*/
|
|
9
|
-
export class RedisStorageBackend implements
|
|
9
|
+
export class RedisStorageBackend implements SessionStore {
|
|
10
10
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
11
11
|
private readonly KEY_PREFIX = 'mcp:session:';
|
|
12
|
-
private readonly
|
|
13
|
-
private readonly
|
|
12
|
+
private readonly USER_ID_KEY_PREFIX = 'mcp:userId:';
|
|
13
|
+
private readonly USER_ID_KEY_SUFFIX = ':sessions';
|
|
14
14
|
|
|
15
15
|
constructor(private redis: Redis) { }
|
|
16
16
|
|
|
@@ -19,7 +19,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
19
19
|
await this.redis.ping();
|
|
20
20
|
console.log('[mcp-ts][Storage] Redis: ✓ Connected to server.');
|
|
21
21
|
} catch (error: any) {
|
|
22
|
-
throw new Error(`[
|
|
22
|
+
throw new Error(`[RedisStorageBackend] Failed to connect to Redis: ${error.message}`);
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -27,22 +27,22 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
27
27
|
* Generates Redis key for a specific session
|
|
28
28
|
* @private
|
|
29
29
|
*/
|
|
30
|
-
private getSessionKey(
|
|
31
|
-
return `${this.KEY_PREFIX}${
|
|
30
|
+
private getSessionKey(userId: string, sessionId: string): string {
|
|
31
|
+
return `${this.KEY_PREFIX}${userId}:${sessionId}`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
-
* Generates Redis key for tracking all sessions for
|
|
35
|
+
* Generates Redis key for tracking all sessions for a user
|
|
36
36
|
* @private
|
|
37
37
|
*/
|
|
38
|
-
private
|
|
39
|
-
return `${this.
|
|
38
|
+
private getUserIdKey(userId: string): string {
|
|
39
|
+
return `${this.USER_ID_KEY_PREFIX}${userId}${this.USER_ID_KEY_SUFFIX}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
private
|
|
43
|
-
return
|
|
44
|
-
this.
|
|
45
|
-
|
|
42
|
+
private parseUserIdFromKey(userIdKey: string): string {
|
|
43
|
+
return userIdKey.slice(
|
|
44
|
+
this.USER_ID_KEY_PREFIX.length,
|
|
45
|
+
userIdKey.length - this.USER_ID_KEY_SUFFIX.length
|
|
46
46
|
);
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -67,7 +67,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
67
67
|
}
|
|
68
68
|
} while (cursor !== '0');
|
|
69
69
|
} catch (error) {
|
|
70
|
-
console.warn('[
|
|
70
|
+
console.warn('[RedisStorageBackend] SCAN failed, falling back to KEYS:', error);
|
|
71
71
|
return await this.redis.keys(pattern);
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -78,12 +78,12 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
78
78
|
return generateSessionId();
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async
|
|
82
|
-
const { sessionId,
|
|
83
|
-
if (!sessionId || !
|
|
81
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
82
|
+
const { sessionId, userId } = session;
|
|
83
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
84
84
|
|
|
85
|
-
const sessionKey = this.getSessionKey(
|
|
86
|
-
const
|
|
85
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
86
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
87
87
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
88
88
|
|
|
89
89
|
/** ioredis syntax: set(key, val, 'EX', ttl, 'NX') */
|
|
@@ -99,10 +99,10 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
99
99
|
throw new Error(`Session ${sessionId} already exists`);
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
await this.redis.sadd(
|
|
102
|
+
await this.redis.sadd(userIdKey, sessionId);
|
|
103
103
|
}
|
|
104
|
-
async
|
|
105
|
-
const sessionKey = this.getSessionKey(
|
|
104
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
105
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
106
106
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
107
107
|
|
|
108
108
|
/** Lua script for atomic parsing, merging, and saving */
|
|
@@ -132,70 +132,70 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
132
132
|
);
|
|
133
133
|
|
|
134
134
|
if (result === 0) {
|
|
135
|
-
throw new Error(`Session ${sessionId} not found for
|
|
135
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
async
|
|
139
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
140
140
|
try {
|
|
141
|
-
const sessionKey = this.getSessionKey(
|
|
141
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
142
142
|
const sessionDataStr = await this.redis.get(sessionKey);
|
|
143
143
|
|
|
144
144
|
if (!sessionDataStr) {
|
|
145
145
|
return null;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
const
|
|
149
|
-
return
|
|
148
|
+
const Session: Session = JSON.parse(sessionDataStr);
|
|
149
|
+
return Session;
|
|
150
150
|
} catch (error) {
|
|
151
|
-
console.error('[
|
|
151
|
+
console.error('[RedisStorageBackend] Failed to get session:', error);
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
async
|
|
157
|
-
const sessions = await this.
|
|
156
|
+
async listIds(userId: string): Promise<string[]> {
|
|
157
|
+
const sessions = await this.list(userId);
|
|
158
158
|
return sessions.map((session) => session.sessionId);
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
async
|
|
161
|
+
async list(userId: string): Promise<Session[]> {
|
|
162
162
|
try {
|
|
163
|
-
const
|
|
164
|
-
const sessionIds = await this.redis.smembers(
|
|
163
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
164
|
+
const sessionIds = await this.redis.smembers(userIdKey);
|
|
165
165
|
if (sessionIds.length === 0) return [];
|
|
166
166
|
|
|
167
167
|
const results = await Promise.all(
|
|
168
168
|
sessionIds.map(async (sessionId) => {
|
|
169
|
-
const data = await this.redis.get(this.getSessionKey(
|
|
170
|
-
return data ? (JSON.parse(data) as
|
|
169
|
+
const data = await this.redis.get(this.getSessionKey(userId, sessionId));
|
|
170
|
+
return data ? (JSON.parse(data) as Session) : null;
|
|
171
171
|
})
|
|
172
172
|
);
|
|
173
173
|
|
|
174
174
|
const staleSessionIds = sessionIds.filter((_, index) => results[index] === null);
|
|
175
175
|
if (staleSessionIds.length > 0) {
|
|
176
|
-
await this.redis.srem(
|
|
176
|
+
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
-
return results.filter((session): session is
|
|
179
|
+
return results.filter((session): session is Session => session !== null);
|
|
180
180
|
} catch (error) {
|
|
181
|
-
console.error(`[
|
|
181
|
+
console.error(`[RedisStorageBackend] Failed to get session data for ${userId}:`, error);
|
|
182
182
|
return [];
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
async
|
|
186
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
187
187
|
try {
|
|
188
|
-
const sessionKey = this.getSessionKey(
|
|
189
|
-
const
|
|
188
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
189
|
+
const userIdKey = this.getUserIdKey(userId);
|
|
190
190
|
|
|
191
|
-
await this.redis.srem(
|
|
191
|
+
await this.redis.srem(userIdKey, sessionId);
|
|
192
192
|
await this.redis.del(sessionKey);
|
|
193
193
|
} catch (error) {
|
|
194
|
-
console.error('[
|
|
194
|
+
console.error('[RedisStorageBackend] Failed to remove session:', error);
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
async
|
|
198
|
+
async listAllIds(): Promise<string[]> {
|
|
199
199
|
try {
|
|
200
200
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
201
201
|
const sessions = await Promise.all(
|
|
@@ -206,9 +206,9 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
try {
|
|
209
|
-
return (JSON.parse(data) as
|
|
209
|
+
return (JSON.parse(data) as Session).sessionId;
|
|
210
210
|
} catch (error) {
|
|
211
|
-
console.error('[
|
|
211
|
+
console.error('[RedisStorageBackend] Failed to parse session while listing all session IDs:', error);
|
|
212
212
|
return null;
|
|
213
213
|
}
|
|
214
214
|
})
|
|
@@ -216,7 +216,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
216
216
|
|
|
217
217
|
return sessions.filter((sessionId): sessionId is string => sessionId !== null);
|
|
218
218
|
} catch (error) {
|
|
219
|
-
console.error('[
|
|
219
|
+
console.error('[RedisStorageBackend] Failed to get all sessions:', error);
|
|
220
220
|
return [];
|
|
221
221
|
}
|
|
222
222
|
}
|
|
@@ -224,45 +224,45 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
224
224
|
async clearAll(): Promise<void> {
|
|
225
225
|
try {
|
|
226
226
|
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
227
|
-
const
|
|
228
|
-
const allKeys = [...keys, ...
|
|
227
|
+
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
228
|
+
const allKeys = [...keys, ...userIdKeys];
|
|
229
229
|
if (allKeys.length > 0) {
|
|
230
230
|
await this.redis.del(...allKeys);
|
|
231
231
|
}
|
|
232
232
|
} catch (error) {
|
|
233
|
-
console.error('[
|
|
233
|
+
console.error('[RedisStorageBackend] Failed to clear sessions:', error);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
-
async
|
|
237
|
+
async cleanupExpired(): Promise<void> {
|
|
238
238
|
try {
|
|
239
|
-
const
|
|
239
|
+
const userIdKeys = await this.scanKeys(`${this.USER_ID_KEY_PREFIX}*${this.USER_ID_KEY_SUFFIX}`);
|
|
240
240
|
|
|
241
|
-
for (const
|
|
242
|
-
const
|
|
243
|
-
const sessionIds = await this.redis.smembers(
|
|
241
|
+
for (const userIdKey of userIdKeys) {
|
|
242
|
+
const userId = this.parseUserIdFromKey(userIdKey);
|
|
243
|
+
const sessionIds = await this.redis.smembers(userIdKey);
|
|
244
244
|
|
|
245
245
|
if (sessionIds.length === 0) {
|
|
246
|
-
await this.redis.del(
|
|
246
|
+
await this.redis.del(userIdKey);
|
|
247
247
|
continue;
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
const existenceChecks = await Promise.all(
|
|
251
|
-
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(
|
|
251
|
+
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(userId, sessionId)))
|
|
252
252
|
);
|
|
253
253
|
|
|
254
254
|
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
255
255
|
if (staleSessionIds.length > 0) {
|
|
256
|
-
await this.redis.srem(
|
|
256
|
+
await this.redis.srem(userIdKey, ...staleSessionIds);
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
-
const remainingCount = await this.redis.scard(
|
|
259
|
+
const remainingCount = await this.redis.scard(userIdKey);
|
|
260
260
|
if (remainingCount === 0) {
|
|
261
|
-
await this.redis.del(
|
|
261
|
+
await this.redis.del(userIdKey);
|
|
262
262
|
}
|
|
263
263
|
}
|
|
264
264
|
} catch (error) {
|
|
265
|
-
console.error('[
|
|
265
|
+
console.error('[RedisStorageBackend] Failed to cleanup expired sessions:', error);
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
@@ -270,7 +270,7 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
270
270
|
try {
|
|
271
271
|
await this.redis.quit();
|
|
272
272
|
} catch (error) {
|
|
273
|
-
console.error('[
|
|
273
|
+
console.error('[RedisStorageBackend] Failed to disconnect:', error);
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Database } from 'better-sqlite3';
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionStore, Session } from './types.js';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { generateSessionId } from '../../shared/utils.js';
|
|
@@ -9,7 +9,7 @@ export interface SqliteStorageOptions {
|
|
|
9
9
|
table?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export class SqliteStorage implements
|
|
12
|
+
export class SqliteStorage implements SessionStore {
|
|
13
13
|
private db: Database | null = null;
|
|
14
14
|
private table: string;
|
|
15
15
|
private initialized = false;
|
|
@@ -37,11 +37,11 @@ export class SqliteStorage implements StorageBackend {
|
|
|
37
37
|
this.db.exec(`
|
|
38
38
|
CREATE TABLE IF NOT EXISTS ${this.table} (
|
|
39
39
|
sessionId TEXT PRIMARY KEY,
|
|
40
|
-
|
|
40
|
+
userId TEXT NOT NULL,
|
|
41
41
|
data TEXT NOT NULL,
|
|
42
42
|
expiresAt INTEGER
|
|
43
43
|
);
|
|
44
|
-
CREATE INDEX IF NOT EXISTS idx_${this.table}
|
|
44
|
+
CREATE INDEX IF NOT EXISTS idx_${this.table}_userId ON ${this.table}(userId);
|
|
45
45
|
`);
|
|
46
46
|
|
|
47
47
|
this.initialized = true;
|
|
@@ -66,21 +66,21 @@ export class SqliteStorage implements StorageBackend {
|
|
|
66
66
|
return generateSessionId();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
async
|
|
69
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
70
70
|
this.ensureInitialized();
|
|
71
|
-
const { sessionId,
|
|
71
|
+
const { sessionId, userId } = session;
|
|
72
72
|
|
|
73
|
-
if (!sessionId || !
|
|
74
|
-
throw new Error('
|
|
73
|
+
if (!sessionId || !userId) {
|
|
74
|
+
throw new Error('userId and sessionId required');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const expiresAt = ttl ? Date.now() + ttl * 1000 : null;
|
|
78
78
|
|
|
79
79
|
try {
|
|
80
80
|
const stmt = this.db!.prepare(
|
|
81
|
-
`INSERT INTO ${this.table} (sessionId,
|
|
81
|
+
`INSERT INTO ${this.table} (sessionId, userId, data, expiresAt) VALUES (?, ?, ?, ?)`
|
|
82
82
|
);
|
|
83
|
-
stmt.run(sessionId,
|
|
83
|
+
stmt.run(sessionId, userId, JSON.stringify(session), expiresAt);
|
|
84
84
|
} catch (error: any) {
|
|
85
85
|
if (error.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
|
|
86
86
|
throw new Error(`Session ${sessionId} already exists`);
|
|
@@ -89,70 +89,70 @@ export class SqliteStorage implements StorageBackend {
|
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
async
|
|
92
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
93
93
|
this.ensureInitialized();
|
|
94
|
-
if (!sessionId || !
|
|
95
|
-
throw new Error('
|
|
94
|
+
if (!sessionId || !userId) {
|
|
95
|
+
throw new Error('userId and sessionId required');
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
const currentSession = await this.
|
|
98
|
+
const currentSession = await this.get(userId, sessionId);
|
|
99
99
|
if (!currentSession) {
|
|
100
|
-
throw new Error(`Session ${sessionId} not found for
|
|
100
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
const updatedSession = { ...currentSession, ...data };
|
|
104
104
|
const expiresAt = ttl ? Date.now() + ttl * 1000 : null;
|
|
105
105
|
|
|
106
106
|
const stmt = this.db!.prepare(
|
|
107
|
-
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND
|
|
107
|
+
`UPDATE ${this.table} SET data = ?, expiresAt = ? WHERE sessionId = ? AND userId = ?`
|
|
108
108
|
);
|
|
109
109
|
|
|
110
|
-
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId,
|
|
110
|
+
stmt.run(JSON.stringify(updatedSession), expiresAt, sessionId, userId);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
async
|
|
113
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
114
114
|
this.ensureInitialized();
|
|
115
115
|
|
|
116
116
|
const stmt = this.db!.prepare(
|
|
117
|
-
`SELECT data FROM ${this.table} WHERE sessionId = ? AND
|
|
117
|
+
`SELECT data FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
118
118
|
);
|
|
119
|
-
const row = stmt.get(sessionId,
|
|
119
|
+
const row = stmt.get(sessionId, userId) as { data: string } | undefined;
|
|
120
120
|
|
|
121
121
|
if (!row) return null;
|
|
122
|
-
return JSON.parse(row.data) as
|
|
122
|
+
return JSON.parse(row.data) as Session;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
async
|
|
125
|
+
async list(userId: string): Promise<Session[]> {
|
|
126
126
|
this.ensureInitialized();
|
|
127
127
|
|
|
128
128
|
const stmt = this.db!.prepare(
|
|
129
|
-
`SELECT data FROM ${this.table} WHERE
|
|
129
|
+
`SELECT data FROM ${this.table} WHERE userId = ?`
|
|
130
130
|
);
|
|
131
|
-
const rows = stmt.all(
|
|
131
|
+
const rows = stmt.all(userId) as { data: string }[];
|
|
132
132
|
|
|
133
|
-
return rows.map(row => JSON.parse(row.data) as
|
|
133
|
+
return rows.map(row => JSON.parse(row.data) as Session);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
async
|
|
136
|
+
async listIds(userId: string): Promise<string[]> {
|
|
137
137
|
this.ensureInitialized();
|
|
138
138
|
|
|
139
139
|
const stmt = this.db!.prepare(
|
|
140
|
-
`SELECT sessionId FROM ${this.table} WHERE
|
|
140
|
+
`SELECT sessionId FROM ${this.table} WHERE userId = ?`
|
|
141
141
|
);
|
|
142
|
-
const rows = stmt.all(
|
|
142
|
+
const rows = stmt.all(userId) as { sessionId: string }[];
|
|
143
143
|
|
|
144
144
|
return rows.map(row => row.sessionId);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
async
|
|
147
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
148
148
|
this.ensureInitialized();
|
|
149
149
|
const stmt = this.db!.prepare(
|
|
150
|
-
`DELETE FROM ${this.table} WHERE sessionId = ? AND
|
|
150
|
+
`DELETE FROM ${this.table} WHERE sessionId = ? AND userId = ?`
|
|
151
151
|
);
|
|
152
|
-
stmt.run(sessionId,
|
|
152
|
+
stmt.run(sessionId, userId);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
async
|
|
155
|
+
async listAllIds(): Promise<string[]> {
|
|
156
156
|
this.ensureInitialized();
|
|
157
157
|
const stmt = this.db!.prepare(`SELECT sessionId FROM ${this.table}`);
|
|
158
158
|
const rows = stmt.all() as { sessionId: string }[];
|
|
@@ -165,7 +165,7 @@ export class SqliteStorage implements StorageBackend {
|
|
|
165
165
|
stmt.run();
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
async
|
|
168
|
+
async cleanupExpired(): Promise<void> {
|
|
169
169
|
this.ensureInitialized();
|
|
170
170
|
const now = Date.now();
|
|
171
171
|
const stmt = this.db!.prepare(
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
-
import {
|
|
2
|
+
import type { SessionStore, Session } from './types.js';
|
|
3
3
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
5
5
|
import { encryptObject, decryptObject } from './crypto.js';
|
|
6
6
|
|
|
7
|
-
export class SupabaseStorageBackend implements
|
|
7
|
+
export class SupabaseStorageBackend implements SessionStore {
|
|
8
8
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
9
9
|
|
|
10
10
|
constructor(private supabase: SupabaseClient) {}
|
|
@@ -34,7 +34,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
34
34
|
return generateSessionId();
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
private mapRowToSessionData(row: any):
|
|
37
|
+
private mapRowToSessionData(row: any): Session {
|
|
38
38
|
return {
|
|
39
39
|
sessionId: row.session_id,
|
|
40
40
|
serverId: row.server_id,
|
|
@@ -43,7 +43,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
43
43
|
transportType: row.transport_type,
|
|
44
44
|
callbackUrl: row.callback_url,
|
|
45
45
|
createdAt: new Date(row.created_at).getTime(),
|
|
46
|
-
|
|
46
|
+
userId: row.user_id,
|
|
47
47
|
headers: decryptObject(row.headers),
|
|
48
48
|
active: row.active,
|
|
49
49
|
clientInformation: row.client_information,
|
|
@@ -53,9 +53,9 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async
|
|
57
|
-
const { sessionId,
|
|
58
|
-
if (!sessionId || !
|
|
56
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
57
|
+
const { sessionId, userId } = session;
|
|
58
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
59
59
|
|
|
60
60
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
61
61
|
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
@@ -64,14 +64,13 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
64
64
|
.from('mcp_sessions')
|
|
65
65
|
.insert({
|
|
66
66
|
session_id: sessionId,
|
|
67
|
-
user_id:
|
|
67
|
+
user_id: userId, // Maps user_id to userId to support RLS using auth.uid()
|
|
68
68
|
server_id: session.serverId,
|
|
69
69
|
server_name: session.serverName,
|
|
70
70
|
server_url: session.serverUrl,
|
|
71
71
|
transport_type: session.transportType,
|
|
72
72
|
callback_url: session.callbackUrl,
|
|
73
73
|
created_at: new Date(session.createdAt || Date.now()).toISOString(),
|
|
74
|
-
identity: identity,
|
|
75
74
|
headers: encryptObject(session.headers),
|
|
76
75
|
active: session.active ?? false,
|
|
77
76
|
client_information: session.clientInformation,
|
|
@@ -90,7 +89,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
90
89
|
}
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
async
|
|
92
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
94
93
|
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
95
94
|
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
96
95
|
|
|
@@ -115,7 +114,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
115
114
|
const { data: updatedRows, error } = await this.supabase
|
|
116
115
|
.from('mcp_sessions')
|
|
117
116
|
.update(updateData)
|
|
118
|
-
.eq('
|
|
117
|
+
.eq('user_id', userId)
|
|
119
118
|
.eq('session_id', sessionId)
|
|
120
119
|
.select('id');
|
|
121
120
|
|
|
@@ -124,15 +123,15 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
124
123
|
}
|
|
125
124
|
|
|
126
125
|
if (!updatedRows || updatedRows.length === 0) {
|
|
127
|
-
throw new Error(`Session ${sessionId} not found for
|
|
126
|
+
throw new Error(`Session ${sessionId} not found for userId ${userId}`);
|
|
128
127
|
}
|
|
129
128
|
}
|
|
130
129
|
|
|
131
|
-
async
|
|
130
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
132
131
|
const { data, error } = await this.supabase
|
|
133
132
|
.from('mcp_sessions')
|
|
134
133
|
.select('*')
|
|
135
|
-
.eq('
|
|
134
|
+
.eq('user_id', userId)
|
|
136
135
|
.eq('session_id', sessionId)
|
|
137
136
|
.maybeSingle();
|
|
138
137
|
|
|
@@ -146,25 +145,25 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
146
145
|
return this.mapRowToSessionData(data);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
|
-
async
|
|
148
|
+
async list(userId: string): Promise<Session[]> {
|
|
150
149
|
const { data, error } = await this.supabase
|
|
151
150
|
.from('mcp_sessions')
|
|
152
151
|
.select('*')
|
|
153
|
-
.eq('
|
|
152
|
+
.eq('user_id', userId);
|
|
154
153
|
|
|
155
154
|
if (error) {
|
|
156
|
-
console.error(`[SupabaseStorage] Failed to get session data for ${
|
|
155
|
+
console.error(`[SupabaseStorage] Failed to get session data for ${userId}:`, error);
|
|
157
156
|
return [];
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
return data.map(row => this.mapRowToSessionData(row));
|
|
161
160
|
}
|
|
162
161
|
|
|
163
|
-
async
|
|
162
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
164
163
|
const { error } = await this.supabase
|
|
165
164
|
.from('mcp_sessions')
|
|
166
165
|
.delete()
|
|
167
|
-
.eq('
|
|
166
|
+
.eq('user_id', userId)
|
|
168
167
|
.eq('session_id', sessionId);
|
|
169
168
|
|
|
170
169
|
if (error) {
|
|
@@ -172,21 +171,21 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
172
171
|
}
|
|
173
172
|
}
|
|
174
173
|
|
|
175
|
-
async
|
|
174
|
+
async listIds(userId: string): Promise<string[]> {
|
|
176
175
|
const { data, error } = await this.supabase
|
|
177
176
|
.from('mcp_sessions')
|
|
178
177
|
.select('session_id')
|
|
179
|
-
.eq('
|
|
178
|
+
.eq('user_id', userId);
|
|
180
179
|
|
|
181
180
|
if (error) {
|
|
182
|
-
console.error(`[SupabaseStorage] Failed to get sessions for ${
|
|
181
|
+
console.error(`[SupabaseStorage] Failed to get sessions for ${userId}:`, error);
|
|
183
182
|
return [];
|
|
184
183
|
}
|
|
185
184
|
|
|
186
185
|
return data.map(row => row.session_id);
|
|
187
186
|
}
|
|
188
187
|
|
|
189
|
-
async
|
|
188
|
+
async listAllIds(): Promise<string[]> {
|
|
190
189
|
const { data, error } = await this.supabase
|
|
191
190
|
.from('mcp_sessions')
|
|
192
191
|
.select('session_id');
|
|
@@ -211,7 +210,7 @@ export class SupabaseStorageBackend implements StorageBackend {
|
|
|
211
210
|
}
|
|
212
211
|
}
|
|
213
212
|
|
|
214
|
-
async
|
|
213
|
+
async cleanupExpired(): Promise<void> {
|
|
215
214
|
const { error } = await this.supabase
|
|
216
215
|
.from('mcp_sessions')
|
|
217
216
|
.delete()
|