@mcp-ts/sdk 1.3.4 → 1.3.6
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 +404 -400
- package/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/bin/mcp-ts.d.mts +1 -0
- package/dist/bin/mcp-ts.d.ts +1 -0
- package/dist/bin/mcp-ts.js +105 -0
- package/dist/bin/mcp-ts.js.map +1 -0
- package/dist/bin/mcp-ts.mjs +82 -0
- package/dist/bin/mcp-ts.mjs.map +1 -0
- package/dist/client/index.d.mts +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +14 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +14 -5
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.js +15 -6
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -6
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.js +15 -6
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +15 -6
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +480 -179
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +418 -179
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-FAFpUzZ4.d.ts → multi-session-client-BYLarghq.d.ts} +29 -19
- package/dist/{multi-session-client-DzjmT7FX.d.mts → multi-session-client-CzhMkE0k.d.mts} +29 -19
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +455 -172
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +410 -172
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +19 -6
- package/src/bin/mcp-ts.ts +102 -0
- package/src/client/core/sse-client.ts +371 -354
- package/src/client/react/use-mcp.ts +31 -31
- package/src/client/vue/use-mcp.ts +77 -77
- package/src/server/handlers/nextjs-handler.ts +204 -207
- package/src/server/handlers/sse-handler.ts +14 -63
- package/src/server/mcp/oauth-client.ts +67 -79
- package/src/server/mcp/storage-oauth-provider.ts +71 -38
- package/src/server/storage/file-backend.ts +1 -0
- package/src/server/storage/index.ts +82 -38
- package/src/server/storage/memory-backend.ts +4 -0
- package/src/server/storage/redis-backend.ts +102 -23
- package/src/server/storage/sqlite-backend.ts +1 -0
- package/src/server/storage/supabase-backend.ts +227 -0
- package/src/server/storage/types.ts +12 -12
- package/src/shared/constants.ts +2 -2
- package/src/shared/event-routing.ts +28 -0
- package/supabase/migrations/20260330195700_install_mcp_sessions.sql +84 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import type { Redis } from 'ioredis';
|
|
3
3
|
import { customAlphabet } from 'nanoid';
|
|
4
|
-
import { StorageBackend, SessionData
|
|
4
|
+
import { StorageBackend, SessionData } from './types';
|
|
5
5
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
6
6
|
|
|
7
7
|
/** first char: letters only (required by OpenAI) */
|
|
@@ -22,8 +22,19 @@ const rest = customAlphabet(
|
|
|
22
22
|
export class RedisStorageBackend implements StorageBackend {
|
|
23
23
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
24
24
|
private readonly KEY_PREFIX = 'mcp:session:';
|
|
25
|
+
private readonly IDENTITY_KEY_PREFIX = 'mcp:identity:';
|
|
26
|
+
private readonly IDENTITY_KEY_SUFFIX = ':sessions';
|
|
25
27
|
|
|
26
28
|
constructor(private redis: Redis) { }
|
|
29
|
+
|
|
30
|
+
async init(): Promise<void> {
|
|
31
|
+
try {
|
|
32
|
+
await this.redis.ping();
|
|
33
|
+
console.log('[mcp-ts][Storage] Redis: ✓ Connected to server.');
|
|
34
|
+
} catch (error: any) {
|
|
35
|
+
throw new Error(`[RedisStorage] Failed to connect to Redis: ${error.message}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
27
38
|
|
|
28
39
|
/**
|
|
29
40
|
* Generates Redis key for a specific session
|
|
@@ -38,7 +49,42 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
38
49
|
* @private
|
|
39
50
|
*/
|
|
40
51
|
private getIdentityKey(identity: string): string {
|
|
41
|
-
return
|
|
52
|
+
return `${this.IDENTITY_KEY_PREFIX}${identity}${this.IDENTITY_KEY_SUFFIX}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private parseIdentityFromKey(identityKey: string): string {
|
|
56
|
+
return identityKey.slice(
|
|
57
|
+
this.IDENTITY_KEY_PREFIX.length,
|
|
58
|
+
identityKey.length - this.IDENTITY_KEY_SUFFIX.length
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async scanKeys(pattern: string): Promise<string[]> {
|
|
63
|
+
const redis = this.redis as Redis & {
|
|
64
|
+
scan?: (cursor: string, ...args: Array<string | number>) => Promise<[string, string[]]>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (typeof redis.scan !== 'function') {
|
|
68
|
+
return await this.redis.keys(pattern);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const keys = new Set<string>();
|
|
72
|
+
let cursor = '0';
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
do {
|
|
76
|
+
const [nextCursor, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
77
|
+
cursor = nextCursor;
|
|
78
|
+
for (const key of batch) {
|
|
79
|
+
keys.add(key);
|
|
80
|
+
}
|
|
81
|
+
} while (cursor !== '0');
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.warn('[RedisStorage] SCAN failed, falling back to KEYS:', error);
|
|
84
|
+
return await this.redis.keys(pattern);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Array.from(keys);
|
|
42
88
|
}
|
|
43
89
|
|
|
44
90
|
generateSessionId(): string {
|
|
@@ -121,18 +167,14 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
121
167
|
}
|
|
122
168
|
|
|
123
169
|
async getIdentityMcpSessions(identity: string): Promise<string[]> {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
return await this.redis.smembers(identityKey);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error(`[RedisStorage] Failed to get sessions for ${identity}:`, error);
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
170
|
+
const sessions = await this.getIdentitySessionsData(identity);
|
|
171
|
+
return sessions.map((session) => session.sessionId);
|
|
131
172
|
}
|
|
132
173
|
|
|
133
174
|
async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
|
|
134
175
|
try {
|
|
135
|
-
const
|
|
176
|
+
const identityKey = this.getIdentityKey(identity);
|
|
177
|
+
const sessionIds = await this.redis.smembers(identityKey);
|
|
136
178
|
if (sessionIds.length === 0) return [];
|
|
137
179
|
|
|
138
180
|
const results = await Promise.all(
|
|
@@ -142,6 +184,11 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
142
184
|
})
|
|
143
185
|
);
|
|
144
186
|
|
|
187
|
+
const staleSessionIds = sessionIds.filter((_, index) => results[index] === null);
|
|
188
|
+
if (staleSessionIds.length > 0) {
|
|
189
|
+
await this.redis.srem(identityKey, ...staleSessionIds);
|
|
190
|
+
}
|
|
191
|
+
|
|
145
192
|
return results.filter((session): session is SessionData => session !== null);
|
|
146
193
|
} catch (error) {
|
|
147
194
|
console.error(`[RedisStorage] Failed to get session data for ${identity}:`, error);
|
|
@@ -163,9 +210,24 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
163
210
|
|
|
164
211
|
async getAllSessionIds(): Promise<string[]> {
|
|
165
212
|
try {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
213
|
+
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
214
|
+
const sessions = await Promise.all(
|
|
215
|
+
keys.map(async (key) => {
|
|
216
|
+
const data = await this.redis.get(key);
|
|
217
|
+
if (!data) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
return (JSON.parse(data) as SessionData).sessionId;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error('[RedisStorage] Failed to parse session while listing all session IDs:', error);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
})
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
return sessions.filter((sessionId): sessionId is string => sessionId !== null);
|
|
169
231
|
} catch (error) {
|
|
170
232
|
console.error('[RedisStorage] Failed to get all sessions:', error);
|
|
171
233
|
return [];
|
|
@@ -174,10 +236,11 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
174
236
|
|
|
175
237
|
async clearAll(): Promise<void> {
|
|
176
238
|
try {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
239
|
+
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
240
|
+
const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
|
|
241
|
+
const allKeys = [...keys, ...identityKeys];
|
|
242
|
+
if (allKeys.length > 0) {
|
|
243
|
+
await this.redis.del(...allKeys);
|
|
181
244
|
}
|
|
182
245
|
} catch (error) {
|
|
183
246
|
console.error('[RedisStorage] Failed to clear sessions:', error);
|
|
@@ -186,13 +249,29 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
186
249
|
|
|
187
250
|
async cleanupExpiredSessions(): Promise<void> {
|
|
188
251
|
try {
|
|
189
|
-
const
|
|
190
|
-
|
|
252
|
+
const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
|
|
253
|
+
|
|
254
|
+
for (const identityKey of identityKeys) {
|
|
255
|
+
const identity = this.parseIdentityFromKey(identityKey);
|
|
256
|
+
const sessionIds = await this.redis.smembers(identityKey);
|
|
257
|
+
|
|
258
|
+
if (sessionIds.length === 0) {
|
|
259
|
+
await this.redis.del(identityKey);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const existenceChecks = await Promise.all(
|
|
264
|
+
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(identity, sessionId)))
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
268
|
+
if (staleSessionIds.length > 0) {
|
|
269
|
+
await this.redis.srem(identityKey, ...staleSessionIds);
|
|
270
|
+
}
|
|
191
271
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await this.redis.del(key);
|
|
272
|
+
const remainingCount = await this.redis.scard(identityKey);
|
|
273
|
+
if (remainingCount === 0) {
|
|
274
|
+
await this.redis.del(identityKey);
|
|
196
275
|
}
|
|
197
276
|
}
|
|
198
277
|
} catch (error) {
|
|
@@ -44,6 +44,7 @@ export class SqliteStorage implements StorageBackend {
|
|
|
44
44
|
`);
|
|
45
45
|
|
|
46
46
|
this.initialized = true;
|
|
47
|
+
console.log(`[mcp-ts][Storage] SQLite: ✓ database at ${this.dbPath} verified.`);
|
|
47
48
|
} catch (error: any) {
|
|
48
49
|
if (error.code === 'MODULE_NOT_FOUND' || error.message?.includes('better-sqlite3')) {
|
|
49
50
|
throw new Error(
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import { StorageBackend, SessionData } from './types.js';
|
|
3
|
+
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
4
|
+
|
|
5
|
+
export class SupabaseStorageBackend implements StorageBackend {
|
|
6
|
+
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
7
|
+
|
|
8
|
+
constructor(private supabase: SupabaseClient) {}
|
|
9
|
+
|
|
10
|
+
async init(): Promise<void> {
|
|
11
|
+
// Validate that the table exists
|
|
12
|
+
const { error } = await this.supabase
|
|
13
|
+
.from('mcp_sessions')
|
|
14
|
+
.select('session_id')
|
|
15
|
+
.limit(0);
|
|
16
|
+
|
|
17
|
+
if (error) {
|
|
18
|
+
// Postgres error code 42P01 is "relation does not exist"
|
|
19
|
+
if (error.code === '42P01') {
|
|
20
|
+
throw new Error(
|
|
21
|
+
'[SupabaseStorage] Table "mcp_sessions" not found in your database. ' +
|
|
22
|
+
'Please run "npx mcp-ts supabase-init" in your project to set up the required table and RLS policies.'
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`[SupabaseStorage] Initialization check failed: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log('[mcp-ts][Storage] Supabase: ✓ "mcp_sessions" table verified.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
generateSessionId(): string {
|
|
32
|
+
return crypto.randomUUID();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private mapRowToSessionData(row: any): SessionData {
|
|
36
|
+
return {
|
|
37
|
+
sessionId: row.session_id,
|
|
38
|
+
serverId: row.server_id,
|
|
39
|
+
serverName: row.server_name,
|
|
40
|
+
serverUrl: row.server_url,
|
|
41
|
+
transportType: row.transport_type,
|
|
42
|
+
callbackUrl: row.callback_url,
|
|
43
|
+
createdAt: new Date(row.created_at).getTime(),
|
|
44
|
+
identity: row.identity,
|
|
45
|
+
headers: row.headers,
|
|
46
|
+
active: row.active,
|
|
47
|
+
clientInformation: row.client_information,
|
|
48
|
+
tokens: row.tokens,
|
|
49
|
+
codeVerifier: row.code_verifier,
|
|
50
|
+
clientId: row.client_id,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async createSession(session: SessionData, ttl?: number): Promise<void> {
|
|
55
|
+
const { sessionId, identity } = session;
|
|
56
|
+
if (!sessionId || !identity) throw new Error('identity and sessionId required');
|
|
57
|
+
|
|
58
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
59
|
+
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
60
|
+
|
|
61
|
+
const { error } = await this.supabase
|
|
62
|
+
.from('mcp_sessions')
|
|
63
|
+
.insert({
|
|
64
|
+
session_id: sessionId,
|
|
65
|
+
user_id: identity, // Maps user_id to identity to support RLS using auth.uid()
|
|
66
|
+
server_id: session.serverId,
|
|
67
|
+
server_name: session.serverName,
|
|
68
|
+
server_url: session.serverUrl,
|
|
69
|
+
transport_type: session.transportType,
|
|
70
|
+
callback_url: session.callbackUrl,
|
|
71
|
+
created_at: new Date(session.createdAt || Date.now()).toISOString(),
|
|
72
|
+
identity: identity,
|
|
73
|
+
headers: session.headers,
|
|
74
|
+
active: session.active ?? false,
|
|
75
|
+
client_information: session.clientInformation,
|
|
76
|
+
tokens: session.tokens,
|
|
77
|
+
code_verifier: session.codeVerifier,
|
|
78
|
+
client_id: session.clientId,
|
|
79
|
+
expires_at: expiresAt
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (error) {
|
|
83
|
+
// Postgres error code 23505 is unique violation
|
|
84
|
+
if (error.code === '23505') {
|
|
85
|
+
throw new Error(`Session ${sessionId} already exists`);
|
|
86
|
+
}
|
|
87
|
+
throw new Error(`Failed to create session in Supabase: ${error.message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async updateSession(identity: string, sessionId: string, data: Partial<SessionData>, ttl?: number): Promise<void> {
|
|
92
|
+
const effectiveTtl = ttl ?? this.DEFAULT_TTL;
|
|
93
|
+
const expiresAt = new Date(Date.now() + effectiveTtl * 1000).toISOString();
|
|
94
|
+
|
|
95
|
+
// Convert the camelCase keys to snake_case for Supabase
|
|
96
|
+
const updateData: any = {
|
|
97
|
+
expires_at: expiresAt,
|
|
98
|
+
updated_at: new Date().toISOString()
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if ('serverId' in data) updateData.server_id = data.serverId;
|
|
102
|
+
if ('serverName' in data) updateData.server_name = data.serverName;
|
|
103
|
+
if ('serverUrl' in data) updateData.server_url = data.serverUrl;
|
|
104
|
+
if ('transportType' in data) updateData.transport_type = data.transportType;
|
|
105
|
+
if ('callbackUrl' in data) updateData.callback_url = data.callbackUrl;
|
|
106
|
+
if ('active' in data) updateData.active = data.active;
|
|
107
|
+
if ('headers' in data) updateData.headers = data.headers;
|
|
108
|
+
if ('clientInformation' in data) updateData.client_information = data.clientInformation;
|
|
109
|
+
if ('tokens' in data) updateData.tokens = data.tokens;
|
|
110
|
+
if ('codeVerifier' in data) updateData.code_verifier = data.codeVerifier;
|
|
111
|
+
if ('clientId' in data) updateData.client_id = data.clientId;
|
|
112
|
+
|
|
113
|
+
const { data: updatedRows, error } = await this.supabase
|
|
114
|
+
.from('mcp_sessions')
|
|
115
|
+
.update(updateData)
|
|
116
|
+
.eq('identity', identity)
|
|
117
|
+
.eq('session_id', sessionId)
|
|
118
|
+
.select('id');
|
|
119
|
+
|
|
120
|
+
if (error) {
|
|
121
|
+
throw new Error(`Failed to update session: ${error.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!updatedRows || updatedRows.length === 0) {
|
|
125
|
+
throw new Error(`Session ${sessionId} not found for identity ${identity}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getSession(identity: string, sessionId: string): Promise<SessionData | null> {
|
|
130
|
+
const { data, error } = await this.supabase
|
|
131
|
+
.from('mcp_sessions')
|
|
132
|
+
.select('*')
|
|
133
|
+
.eq('identity', identity)
|
|
134
|
+
.eq('session_id', sessionId)
|
|
135
|
+
.maybeSingle();
|
|
136
|
+
|
|
137
|
+
if (error) {
|
|
138
|
+
console.error('[SupabaseStorage] Failed to get session:', error);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (!data) return null;
|
|
143
|
+
|
|
144
|
+
return this.mapRowToSessionData(data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
|
|
148
|
+
const { data, error } = await this.supabase
|
|
149
|
+
.from('mcp_sessions')
|
|
150
|
+
.select('*')
|
|
151
|
+
.eq('identity', identity);
|
|
152
|
+
|
|
153
|
+
if (error) {
|
|
154
|
+
console.error(`[SupabaseStorage] Failed to get session data for ${identity}:`, error);
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return data.map(row => this.mapRowToSessionData(row));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async removeSession(identity: string, sessionId: string): Promise<void> {
|
|
162
|
+
const { error } = await this.supabase
|
|
163
|
+
.from('mcp_sessions')
|
|
164
|
+
.delete()
|
|
165
|
+
.eq('identity', identity)
|
|
166
|
+
.eq('session_id', sessionId);
|
|
167
|
+
|
|
168
|
+
if (error) {
|
|
169
|
+
console.error('[SupabaseStorage] Failed to remove session:', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async getIdentityMcpSessions(identity: string): Promise<string[]> {
|
|
174
|
+
const { data, error } = await this.supabase
|
|
175
|
+
.from('mcp_sessions')
|
|
176
|
+
.select('session_id')
|
|
177
|
+
.eq('identity', identity);
|
|
178
|
+
|
|
179
|
+
if (error) {
|
|
180
|
+
console.error(`[SupabaseStorage] Failed to get sessions for ${identity}:`, error);
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return data.map(row => row.session_id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async getAllSessionIds(): Promise<string[]> {
|
|
188
|
+
const { data, error } = await this.supabase
|
|
189
|
+
.from('mcp_sessions')
|
|
190
|
+
.select('session_id');
|
|
191
|
+
|
|
192
|
+
if (error) {
|
|
193
|
+
console.error('[SupabaseStorage] Failed to get all sessions:', error);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return data.map(row => row.session_id);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async clearAll(): Promise<void> {
|
|
201
|
+
// Warning: This deletes everything. Typically only used in testing.
|
|
202
|
+
const { error } = await this.supabase
|
|
203
|
+
.from('mcp_sessions')
|
|
204
|
+
.delete()
|
|
205
|
+
.neq('session_id', ''); // Delete all rows trick
|
|
206
|
+
|
|
207
|
+
if (error) {
|
|
208
|
+
console.error('[SupabaseStorage] Failed to clear sessions:', error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async cleanupExpiredSessions(): Promise<void> {
|
|
213
|
+
const { error } = await this.supabase
|
|
214
|
+
.from('mcp_sessions')
|
|
215
|
+
.delete()
|
|
216
|
+
.lt('expires_at', new Date().toISOString());
|
|
217
|
+
|
|
218
|
+
if (error) {
|
|
219
|
+
console.error('[SupabaseStorage] Failed to cleanup expired sessions:', error);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async disconnect(): Promise<void> {
|
|
224
|
+
// Supabase client handles its own connection pooling over HTTP,
|
|
225
|
+
// there is no explicit disconnect method.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -5,8 +5,8 @@ import type {
|
|
|
5
5
|
OAuthClientInformationMixed,
|
|
6
6
|
} from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
7
7
|
|
|
8
|
-
export interface SessionData {
|
|
9
|
-
sessionId: string;
|
|
8
|
+
export interface SessionData {
|
|
9
|
+
sessionId: string;
|
|
10
10
|
serverId?: string; // Database server ID for mapping
|
|
11
11
|
serverName?: string;
|
|
12
12
|
serverUrl: string;
|
|
@@ -14,16 +14,16 @@ export interface SessionData {
|
|
|
14
14
|
callbackUrl: string;
|
|
15
15
|
createdAt: number;
|
|
16
16
|
identity: string;
|
|
17
|
-
headers?: Record<string, string>;
|
|
18
|
-
/**
|
|
19
|
-
* Session status marker used for TTL transitions:
|
|
20
|
-
* - false: short-lived intermediate/error/auth-pending session state
|
|
21
|
-
* (keep this value when connection/auth is incomplete or failed)
|
|
22
|
-
* - true: active long-lived session state after successful connection/auth completion
|
|
23
|
-
*/
|
|
24
|
-
active?: boolean;
|
|
25
|
-
// OAuth data (consolidated)
|
|
26
|
-
clientInformation?: OAuthClientInformationMixed;
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Session status marker used for TTL transitions:
|
|
20
|
+
* - false: short-lived intermediate/error/auth-pending session state
|
|
21
|
+
* (keep this value when connection/auth is incomplete or failed)
|
|
22
|
+
* - true: active long-lived session state after successful connection/auth completion
|
|
23
|
+
*/
|
|
24
|
+
active?: boolean;
|
|
25
|
+
// OAuth data (consolidated)
|
|
26
|
+
clientInformation?: OAuthClientInformationMixed;
|
|
27
27
|
tokens?: OAuthTokens;
|
|
28
28
|
codeVerifier?: string;
|
|
29
29
|
clientId?: string;
|
package/src/shared/constants.ts
CHANGED
|
@@ -19,10 +19,10 @@ export const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000; // 5 minute buffer before e
|
|
|
19
19
|
// Client Information
|
|
20
20
|
export const DEFAULT_CLIENT_NAME = 'MCP Assistant';
|
|
21
21
|
export const DEFAULT_CLIENT_URI = 'https://mcp-assistant.in';
|
|
22
|
-
export const DEFAULT_LOGO_URI = 'https://mcp-assistant.in/logo.
|
|
22
|
+
export const DEFAULT_LOGO_URI = 'https://mcp-assistant.in/logo.svg';
|
|
23
23
|
export const DEFAULT_POLICY_URI = 'https://mcp-assistant.in/privacy';
|
|
24
24
|
export const SOFTWARE_ID = '@mcp-ts';
|
|
25
|
-
export const SOFTWARE_VERSION = '1.
|
|
25
|
+
export const SOFTWARE_VERSION = '1.3.4';
|
|
26
26
|
|
|
27
27
|
// MCP Client Configuration
|
|
28
28
|
export const MCP_CLIENT_NAME = 'mcp-ts-oauth-client';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { McpConnectionEvent, McpObservabilityEvent } from './events.js';
|
|
2
|
+
import type { McpRpcResponse } from './types.js';
|
|
3
|
+
|
|
4
|
+
export function isRpcResponseEvent(
|
|
5
|
+
event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse
|
|
6
|
+
): event is McpRpcResponse {
|
|
7
|
+
return 'id' in event && ('result' in event || 'error' in event);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isConnectionEvent(
|
|
11
|
+
event: McpConnectionEvent | McpObservabilityEvent | McpRpcResponse
|
|
12
|
+
): event is McpConnectionEvent {
|
|
13
|
+
if (!('type' in event)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
switch (event.type) {
|
|
18
|
+
case 'state_changed':
|
|
19
|
+
case 'tools_discovered':
|
|
20
|
+
case 'auth_required':
|
|
21
|
+
case 'error':
|
|
22
|
+
case 'disconnected':
|
|
23
|
+
case 'progress':
|
|
24
|
+
return true;
|
|
25
|
+
default:
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
-- Create the mcp_sessions table
|
|
2
|
+
CREATE TABLE IF NOT EXISTS public.mcp_sessions (
|
|
3
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
session_id TEXT NOT NULL UNIQUE,
|
|
5
|
+
user_id TEXT NOT NULL, -- Will store the Next.js user's ID or identity
|
|
6
|
+
server_id TEXT,
|
|
7
|
+
server_name TEXT,
|
|
8
|
+
server_url TEXT NOT NULL,
|
|
9
|
+
transport_type TEXT NOT NULL,
|
|
10
|
+
callback_url TEXT NOT NULL,
|
|
11
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
12
|
+
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
13
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
14
|
+
active BOOLEAN DEFAULT false,
|
|
15
|
+
identity TEXT NOT NULL,
|
|
16
|
+
headers JSONB,
|
|
17
|
+
client_information JSONB,
|
|
18
|
+
tokens JSONB,
|
|
19
|
+
code_verifier TEXT,
|
|
20
|
+
client_id TEXT
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
-- Add an index on identity and user_id for faster lookups
|
|
24
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_sessions_identity ON public.mcp_sessions(identity);
|
|
25
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_sessions_user_id ON public.mcp_sessions(user_id);
|
|
26
|
+
-- Add an index on expires_at to speed up the cleanup job
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_mcp_sessions_expires_at ON public.mcp_sessions(expires_at);
|
|
28
|
+
|
|
29
|
+
-- Trigger to automatically update the 'updated_at' column
|
|
30
|
+
CREATE OR REPLACE FUNCTION public.set_current_timestamp_updated_at()
|
|
31
|
+
RETURNS TRIGGER AS $$
|
|
32
|
+
BEGIN
|
|
33
|
+
NEW.updated_at = now();
|
|
34
|
+
RETURN NEW;
|
|
35
|
+
END;
|
|
36
|
+
$$ LANGUAGE plpgsql;
|
|
37
|
+
|
|
38
|
+
DROP TRIGGER IF EXISTS trg_mcp_sessions_updated_at ON public.mcp_sessions;
|
|
39
|
+
CREATE TRIGGER trg_mcp_sessions_updated_at
|
|
40
|
+
BEFORE UPDATE ON public.mcp_sessions
|
|
41
|
+
FOR EACH ROW
|
|
42
|
+
EXECUTE FUNCTION public.set_current_timestamp_updated_at();
|
|
43
|
+
|
|
44
|
+
-- Enable Row Level Security (RLS)
|
|
45
|
+
ALTER TABLE public.mcp_sessions ENABLE ROW LEVEL SECURITY;
|
|
46
|
+
|
|
47
|
+
-- Policy 1: Users can read their own sessions
|
|
48
|
+
CREATE POLICY "Users can view their own sessions"
|
|
49
|
+
ON public.mcp_sessions
|
|
50
|
+
FOR SELECT
|
|
51
|
+
TO authenticated
|
|
52
|
+
USING (
|
|
53
|
+
auth.uid()::text = user_id OR auth.uid()::text = identity
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
-- Policy 2: Users can insert their own sessions
|
|
57
|
+
CREATE POLICY "Users can insert their own sessions"
|
|
58
|
+
ON public.mcp_sessions
|
|
59
|
+
FOR INSERT
|
|
60
|
+
TO authenticated
|
|
61
|
+
WITH CHECK (
|
|
62
|
+
auth.uid()::text = user_id OR auth.uid()::text = identity
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
-- Policy 3: Users can update their own sessions
|
|
66
|
+
CREATE POLICY "Users can update their own sessions"
|
|
67
|
+
ON public.mcp_sessions
|
|
68
|
+
FOR UPDATE
|
|
69
|
+
TO authenticated
|
|
70
|
+
USING (
|
|
71
|
+
auth.uid()::text = user_id OR auth.uid()::text = identity
|
|
72
|
+
)
|
|
73
|
+
WITH CHECK (
|
|
74
|
+
auth.uid()::text = user_id OR auth.uid()::text = identity
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
-- Policy 4: Users can delete their own sessions
|
|
78
|
+
CREATE POLICY "Users can delete their own sessions"
|
|
79
|
+
ON public.mcp_sessions
|
|
80
|
+
FOR DELETE
|
|
81
|
+
TO authenticated
|
|
82
|
+
USING (
|
|
83
|
+
auth.uid()::text = user_id OR auth.uid()::text = identity
|
|
84
|
+
);
|