@mcp-ts/sdk 1.6.2 → 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 +15 -19
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -19
- 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/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
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
OAuthClientMetadata,
|
|
6
6
|
OAuthTokens
|
|
7
7
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
8
|
-
import {
|
|
8
|
+
import { sessions, type Session } from "../storage/index.js";
|
|
9
9
|
import {
|
|
10
10
|
DEFAULT_CLIENT_NAME,
|
|
11
11
|
DEFAULT_CLIENT_URI,
|
|
@@ -34,7 +34,7 @@ export interface AgentsOAuthProvider extends OAuthClientProvider {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export interface StorageOAuthClientProviderOptions {
|
|
37
|
-
|
|
37
|
+
userId: string;
|
|
38
38
|
serverId: string;
|
|
39
39
|
sessionId: string;
|
|
40
40
|
redirectUrl: string;
|
|
@@ -49,10 +49,10 @@ export interface StorageOAuthClientProviderOptions {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Storage-backed OAuth provider implementation for MCP
|
|
52
|
-
* Stores OAuth tokens, client information, and PKCE verifiers using the configured
|
|
52
|
+
* Stores OAuth tokens, client information, and PKCE verifiers using the configured SessionStore
|
|
53
53
|
*/
|
|
54
54
|
export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
55
|
-
public readonly
|
|
55
|
+
public readonly userId: string;
|
|
56
56
|
public readonly serverId: string;
|
|
57
57
|
public readonly sessionId: string;
|
|
58
58
|
public readonly redirectUrl: string;
|
|
@@ -69,11 +69,11 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
69
69
|
private tokenExpiresAt?: number;
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
|
-
* Creates a new
|
|
72
|
+
* Creates a new session-backed OAuth provider
|
|
73
73
|
* @param options - Provider configuration
|
|
74
74
|
*/
|
|
75
75
|
constructor(options: StorageOAuthClientProviderOptions) {
|
|
76
|
-
this.
|
|
76
|
+
this.userId = options.userId;
|
|
77
77
|
this.serverId = options.serverId;
|
|
78
78
|
this.sessionId = options.sessionId;
|
|
79
79
|
this.redirectUrl = options.redirectUrl;
|
|
@@ -110,25 +110,25 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
|
-
* Loads OAuth data from
|
|
113
|
+
* Loads OAuth data from the session store
|
|
114
114
|
* @private
|
|
115
115
|
*/
|
|
116
|
-
private async getSessionData(): Promise<
|
|
117
|
-
const data = await
|
|
116
|
+
private async getSessionData(): Promise<Session> {
|
|
117
|
+
const data = await sessions.get(this.userId, this.sessionId);
|
|
118
118
|
if (!data) {
|
|
119
|
-
return {} as
|
|
119
|
+
return {} as Session;
|
|
120
120
|
}
|
|
121
121
|
return data;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Saves OAuth data to
|
|
125
|
+
* Saves OAuth data to the session store
|
|
126
126
|
* @param data - Partial OAuth data to save
|
|
127
127
|
* @private
|
|
128
128
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
129
129
|
*/
|
|
130
|
-
private async saveSessionData(data: Partial<
|
|
131
|
-
await
|
|
130
|
+
private async saveSessionData(data: Partial<Session>): Promise<void> {
|
|
131
|
+
await sessions.update(this.userId, this.sessionId, data);
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
@@ -170,7 +170,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
170
170
|
* Stores OAuth tokens
|
|
171
171
|
*/
|
|
172
172
|
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
173
|
-
const data: Partial<
|
|
173
|
+
const data: Partial<Session> = { tokens };
|
|
174
174
|
|
|
175
175
|
if (tokens.expires_in) {
|
|
176
176
|
this.tokenExpiresAt = Date.now() + (tokens.expires_in * 1000) - TOKEN_EXPIRY_BUFFER_MS;
|
|
@@ -188,7 +188,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
188
188
|
}
|
|
189
189
|
|
|
190
190
|
async checkState(_state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
|
|
191
|
-
const data = await
|
|
191
|
+
const data = await sessions.get(this.userId, this.sessionId);
|
|
192
192
|
|
|
193
193
|
if (!data) {
|
|
194
194
|
return { valid: false, error: "Session not found" };
|
|
@@ -212,9 +212,9 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
212
212
|
scope: "all" | "client" | "tokens" | "verifier"
|
|
213
213
|
): Promise<void> {
|
|
214
214
|
if (scope === "all") {
|
|
215
|
-
await
|
|
215
|
+
await sessions.delete(this.userId, this.sessionId);
|
|
216
216
|
} else {
|
|
217
|
-
const updates: Partial<
|
|
217
|
+
const updates: Partial<Session> = {};
|
|
218
218
|
|
|
219
219
|
if (scope === "client") {
|
|
220
220
|
updates.clientInformation = undefined;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import type { SessionStore, Session, SetClientOptions } from './types.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* File system implementation of
|
|
7
|
+
* File system implementation of SessionStore
|
|
8
8
|
* Persists sessions to a JSON file
|
|
9
9
|
*/
|
|
10
|
-
export class FileStorageBackend implements
|
|
10
|
+
export class FileStorageBackend implements SessionStore {
|
|
11
11
|
private filePath: string;
|
|
12
|
-
private memoryCache: Map<string,
|
|
12
|
+
private memoryCache: Map<string, Session> | null = null;
|
|
13
13
|
private initialized = false;
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -36,8 +36,8 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
36
36
|
|
|
37
37
|
this.memoryCache = new Map();
|
|
38
38
|
if (Array.isArray(json)) {
|
|
39
|
-
json.forEach((s:
|
|
40
|
-
this.memoryCache!.set(this.getSessionKey(s.
|
|
39
|
+
json.forEach((s: Session) => {
|
|
40
|
+
this.memoryCache!.set(this.getSessionKey(s.userId || 'unknown', s.sessionId), s);
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
} catch (error: any) {
|
|
@@ -65,20 +65,20 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
65
65
|
await fs.writeFile(this.filePath, JSON.stringify(sessions, null, 2), 'utf-8');
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
private getSessionKey(
|
|
69
|
-
return `${
|
|
68
|
+
private getSessionKey(userId: string, sessionId: string): string {
|
|
69
|
+
return `${userId}:${sessionId}`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
generateSessionId(): string {
|
|
73
73
|
return generateSessionId();
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
async
|
|
76
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
77
77
|
await this.ensureInitialized();
|
|
78
|
-
const { sessionId,
|
|
79
|
-
if (!sessionId || !
|
|
78
|
+
const { sessionId, userId } = session;
|
|
79
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
80
80
|
|
|
81
|
-
const sessionKey = this.getSessionKey(
|
|
81
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
82
82
|
if (this.memoryCache!.has(sessionKey)) {
|
|
83
83
|
throw new Error(`Session ${sessionId} already exists`);
|
|
84
84
|
}
|
|
@@ -88,11 +88,11 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
88
88
|
// Note: TTL is ignored in file backend - sessions don't auto-expire
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
async
|
|
91
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
92
92
|
await this.ensureInitialized();
|
|
93
|
-
if (!
|
|
93
|
+
if (!userId || !sessionId) throw new Error('userId and sessionId required');
|
|
94
94
|
|
|
95
|
-
const sessionKey = this.getSessionKey(
|
|
95
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
96
96
|
const current = this.memoryCache!.get(sessionKey);
|
|
97
97
|
|
|
98
98
|
if (!current) {
|
|
@@ -109,33 +109,33 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
109
109
|
// Note: TTL is ignored in file backend - sessions don't auto-expire
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
async
|
|
112
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
113
113
|
await this.ensureInitialized();
|
|
114
|
-
const sessionKey = this.getSessionKey(
|
|
114
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
115
115
|
return this.memoryCache!.get(sessionKey) || null;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
async
|
|
118
|
+
async list(userId: string): Promise<Session[]> {
|
|
119
119
|
await this.ensureInitialized();
|
|
120
|
-
return Array.from(this.memoryCache!.values()).filter(s => s.
|
|
120
|
+
return Array.from(this.memoryCache!.values()).filter(s => s.userId === userId);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
async
|
|
123
|
+
async listIds(userId: string): Promise<string[]> {
|
|
124
124
|
await this.ensureInitialized();
|
|
125
125
|
return Array.from(this.memoryCache!.values())
|
|
126
|
-
.filter(s => s.
|
|
126
|
+
.filter(s => s.userId === userId)
|
|
127
127
|
.map(s => s.sessionId);
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
async
|
|
130
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
131
131
|
await this.ensureInitialized();
|
|
132
|
-
const sessionKey = this.getSessionKey(
|
|
132
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
133
133
|
if (this.memoryCache!.delete(sessionKey)) {
|
|
134
134
|
await this.flush();
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
async
|
|
138
|
+
async listAllIds(): Promise<string[]> {
|
|
139
139
|
await this.ensureInitialized();
|
|
140
140
|
return Array.from(this.memoryCache!.values()).map(s => s.sessionId);
|
|
141
141
|
}
|
|
@@ -146,7 +146,7 @@ export class FileStorageBackend implements StorageBackend {
|
|
|
146
146
|
await this.flush();
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
async
|
|
149
|
+
async cleanupExpired(): Promise<void> {
|
|
150
150
|
// Could implement TTL check here using createdAt
|
|
151
151
|
await this.ensureInitialized();
|
|
152
152
|
}
|
|
@@ -4,28 +4,53 @@ import { MemoryStorageBackend } from './memory-backend';
|
|
|
4
4
|
import { FileStorageBackend } from './file-backend';
|
|
5
5
|
import { SqliteStorage } from './sqlite-backend.js';
|
|
6
6
|
import { SupabaseStorageBackend } from './supabase-backend.js';
|
|
7
|
-
import type
|
|
7
|
+
import { NeonStorageBackend, type NeonStorageOptions } from './neon-backend.js';
|
|
8
|
+
import type { SessionStore } from './types.js';
|
|
8
9
|
|
|
9
10
|
// Re-export types
|
|
10
11
|
export * from './types.js';
|
|
11
12
|
export { generateSessionId } from '../../shared/utils.js';
|
|
12
|
-
export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend };
|
|
13
|
+
export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteStorage, SupabaseStorageBackend, NeonStorageBackend };
|
|
13
14
|
|
|
14
15
|
export function createSupabaseStorageBackend(client: any): SupabaseStorageBackend {
|
|
15
16
|
return new SupabaseStorageBackend(client);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
export function createNeonStorageBackend(sql: any, options?: NeonStorageOptions): NeonStorageBackend {
|
|
20
|
+
return new NeonStorageBackend(sql, options);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function warnIfNeonConnectionStringIsInsecure(connectionString: string): void {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(connectionString);
|
|
26
|
+
const sslMode = url.searchParams.get('sslmode');
|
|
27
|
+
const channelBinding = url.searchParams.get('channel_binding');
|
|
28
|
+
|
|
29
|
+
if (!sslMode) {
|
|
30
|
+
console.warn('[mcp-ts][Storage] Neon connection string does not include sslmode. Neon recommends sslmode=verify-full for the strongest certificate verification.');
|
|
31
|
+
} else if (!['verify-full', 'require'].includes(sslMode)) {
|
|
32
|
+
console.warn(`[mcp-ts][Storage] Neon connection string uses sslmode=${sslMode}. Use sslmode=verify-full or sslmode=require for secure connections.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!channelBinding) {
|
|
36
|
+
console.warn('[mcp-ts][Storage] Neon connection string does not include channel_binding=require. Add it when supported by your runtime and connection path.');
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
console.warn('[mcp-ts][Storage] Neon connection string could not be parsed for SSL checks.');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let storageInstance: SessionStore | null = null;
|
|
44
|
+
let storagePromise: Promise<SessionStore> | null = null;
|
|
20
45
|
|
|
21
|
-
async function initializeStorage<T extends
|
|
46
|
+
async function initializeStorage<T extends SessionStore>(store: T): Promise<T> {
|
|
22
47
|
if (typeof store.init === 'function') {
|
|
23
48
|
await store.init();
|
|
24
49
|
}
|
|
25
50
|
return store;
|
|
26
51
|
}
|
|
27
52
|
|
|
28
|
-
async function createStorage(): Promise<
|
|
53
|
+
async function createStorage(): Promise<SessionStore> {
|
|
29
54
|
const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
|
|
30
55
|
|
|
31
56
|
// Explicit selection
|
|
@@ -80,6 +105,26 @@ async function createStorage(): Promise<StorageBackend> {
|
|
|
80
105
|
}
|
|
81
106
|
}
|
|
82
107
|
|
|
108
|
+
if (type === 'neon') {
|
|
109
|
+
const connectionString = process.env.NEON_DATABASE_URL || process.env.DATABASE_URL;
|
|
110
|
+
|
|
111
|
+
if (!connectionString) {
|
|
112
|
+
console.warn('[mcp-ts][Storage] Explicit selection "neon" requires NEON_DATABASE_URL or DATABASE_URL.');
|
|
113
|
+
} else {
|
|
114
|
+
try {
|
|
115
|
+
const { neon } = await import('@neondatabase/serverless');
|
|
116
|
+
warnIfNeonConnectionStringIsInsecure(connectionString);
|
|
117
|
+
const sql = neon(connectionString);
|
|
118
|
+
console.log('[mcp-ts][Storage] Explicit selection: "neon"');
|
|
119
|
+
return await initializeStorage(new NeonStorageBackend(sql));
|
|
120
|
+
} catch (error: any) {
|
|
121
|
+
console.error('[mcp-ts][Storage] Failed to initialize Neon:', error.message);
|
|
122
|
+
console.log('[mcp-ts][Storage] Falling back to In-Memory storage');
|
|
123
|
+
return await initializeStorage(new MemoryStorageBackend());
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
83
128
|
if (type === 'memory') {
|
|
84
129
|
console.log('[mcp-ts][Storage] Explicit selection: "memory"');
|
|
85
130
|
return await initializeStorage(new MemoryStorageBackend());
|
|
@@ -126,11 +171,23 @@ async function createStorage(): Promise<StorageBackend> {
|
|
|
126
171
|
}
|
|
127
172
|
}
|
|
128
173
|
|
|
174
|
+
if (process.env.NEON_DATABASE_URL) {
|
|
175
|
+
try {
|
|
176
|
+
const { neon } = await import('@neondatabase/serverless');
|
|
177
|
+
warnIfNeonConnectionStringIsInsecure(process.env.NEON_DATABASE_URL);
|
|
178
|
+
const sql = neon(process.env.NEON_DATABASE_URL);
|
|
179
|
+
console.log('[mcp-ts][Storage] Auto-detection: "neon" (via NEON_DATABASE_URL)');
|
|
180
|
+
return await initializeStorage(new NeonStorageBackend(sql));
|
|
181
|
+
} catch (error: any) {
|
|
182
|
+
console.error('[mcp-ts][Storage] Neon auto-detection failed:', error.message);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
129
186
|
console.log('[mcp-ts][Storage] Defaulting to: "memory"');
|
|
130
187
|
return await initializeStorage(new MemoryStorageBackend());
|
|
131
188
|
}
|
|
132
189
|
|
|
133
|
-
async function getStorage(): Promise<
|
|
190
|
+
async function getStorage(): Promise<SessionStore> {
|
|
134
191
|
if (storageInstance) {
|
|
135
192
|
return storageInstance;
|
|
136
193
|
}
|
|
@@ -149,9 +206,9 @@ async function getStorage(): Promise<StorageBackend> {
|
|
|
149
206
|
/**
|
|
150
207
|
* Set the storage instance (for testing)
|
|
151
208
|
* @internal
|
|
152
|
-
* @param instance -
|
|
209
|
+
* @param instance - SessionStore instance or null to reset
|
|
153
210
|
*/
|
|
154
|
-
export function _setStorageInstanceForTesting(instance:
|
|
211
|
+
export function _setStorageInstanceForTesting(instance: SessionStore | null): void {
|
|
155
212
|
storageInstance = instance;
|
|
156
213
|
if (!instance) {
|
|
157
214
|
storagePromise = null;
|
|
@@ -162,7 +219,7 @@ export function _setStorageInstanceForTesting(instance: StorageBackend | null):
|
|
|
162
219
|
* Global session store instance
|
|
163
220
|
* Uses lazy initialization with a Proxy to handle async setup transparently
|
|
164
221
|
*/
|
|
165
|
-
export const
|
|
222
|
+
export const sessions: SessionStore = new Proxy({} as SessionStore, {
|
|
166
223
|
get(_target, prop) {
|
|
167
224
|
return async (...args: any[]) => {
|
|
168
225
|
const instance = await getStorage();
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { SessionStore, Session, SetClientOptions } from './types.js';
|
|
2
2
|
import { generateSessionId } from '../../shared/utils.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* In-memory implementation of
|
|
5
|
+
* In-memory implementation of SessionStore
|
|
6
6
|
* Useful for local development or testing
|
|
7
7
|
*/
|
|
8
|
-
export class MemoryStorageBackend implements
|
|
9
|
-
// Map<
|
|
10
|
-
private sessions = new Map<string,
|
|
8
|
+
export class MemoryStorageBackend implements SessionStore {
|
|
9
|
+
// Map<userId:sessionId, Session>
|
|
10
|
+
private sessions = new Map<string, Session>();
|
|
11
11
|
|
|
12
|
-
// Map<
|
|
13
|
-
private
|
|
12
|
+
// Map<userId, Set<sessionId>>
|
|
13
|
+
private userIdSessions = new Map<string, Set<string>>();
|
|
14
14
|
|
|
15
15
|
constructor() { }
|
|
16
16
|
|
|
@@ -18,19 +18,19 @@ export class MemoryStorageBackend implements StorageBackend {
|
|
|
18
18
|
console.log('[mcp-ts][Storage] Memory: ✓ internal memory store active.');
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
private getSessionKey(
|
|
22
|
-
return `${
|
|
21
|
+
private getSessionKey(userId: string, sessionId: string): string {
|
|
22
|
+
return `${userId}:${sessionId}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
generateSessionId(): string {
|
|
26
26
|
return generateSessionId();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
async
|
|
30
|
-
const { sessionId,
|
|
31
|
-
if (!sessionId || !
|
|
29
|
+
async create(session: Session, ttl?: number): Promise<void> {
|
|
30
|
+
const { sessionId, userId } = session;
|
|
31
|
+
if (!sessionId || !userId) throw new Error('userId and sessionId required');
|
|
32
32
|
|
|
33
|
-
const sessionKey = this.getSessionKey(
|
|
33
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
34
34
|
if (this.sessions.has(sessionKey)) {
|
|
35
35
|
throw new Error(`Session ${sessionId} already exists`);
|
|
36
36
|
}
|
|
@@ -38,17 +38,17 @@ export class MemoryStorageBackend implements StorageBackend {
|
|
|
38
38
|
this.sessions.set(sessionKey, session);
|
|
39
39
|
|
|
40
40
|
// Update index
|
|
41
|
-
if (!this.
|
|
42
|
-
this.
|
|
41
|
+
if (!this.userIdSessions.has(userId)) {
|
|
42
|
+
this.userIdSessions.set(userId, new Set());
|
|
43
43
|
}
|
|
44
|
-
this.
|
|
44
|
+
this.userIdSessions.get(userId)!.add(sessionId);
|
|
45
45
|
// Note: TTL is ignored in memory backend - sessions don't auto-expire
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
async
|
|
49
|
-
if (!
|
|
48
|
+
async update(userId: string, sessionId: string, data: Partial<Session>, ttl?: number): Promise<void> {
|
|
49
|
+
if (!userId || !sessionId) throw new Error('userId and sessionId required');
|
|
50
50
|
|
|
51
|
-
const sessionKey = this.getSessionKey(
|
|
51
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
52
52
|
const current = this.sessions.get(sessionKey);
|
|
53
53
|
|
|
54
54
|
if (!current) {
|
|
@@ -65,23 +65,23 @@ export class MemoryStorageBackend implements StorageBackend {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
async
|
|
69
|
-
const sessionKey = this.getSessionKey(
|
|
68
|
+
async get(userId: string, sessionId: string): Promise<Session | null> {
|
|
69
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
70
70
|
return this.sessions.get(sessionKey) || null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async
|
|
74
|
-
const set = this.
|
|
73
|
+
async listIds(userId: string): Promise<string[]> {
|
|
74
|
+
const set = this.userIdSessions.get(userId);
|
|
75
75
|
return set ? Array.from(set) : [];
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
async
|
|
79
|
-
const set = this.
|
|
78
|
+
async list(userId: string): Promise<Session[]> {
|
|
79
|
+
const set = this.userIdSessions.get(userId);
|
|
80
80
|
if (!set) return [];
|
|
81
81
|
|
|
82
|
-
const results:
|
|
82
|
+
const results: Session[] = [];
|
|
83
83
|
for (const sessionId of set) {
|
|
84
|
-
const session = this.sessions.get(this.getSessionKey(
|
|
84
|
+
const session = this.sessions.get(this.getSessionKey(userId, sessionId));
|
|
85
85
|
if (session) {
|
|
86
86
|
results.push(session);
|
|
87
87
|
}
|
|
@@ -89,29 +89,29 @@ export class MemoryStorageBackend implements StorageBackend {
|
|
|
89
89
|
return results;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
async
|
|
93
|
-
const sessionKey = this.getSessionKey(
|
|
92
|
+
async delete(userId: string, sessionId: string): Promise<void> {
|
|
93
|
+
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
94
94
|
this.sessions.delete(sessionKey);
|
|
95
95
|
|
|
96
|
-
const set = this.
|
|
96
|
+
const set = this.userIdSessions.get(userId);
|
|
97
97
|
if (set) {
|
|
98
98
|
set.delete(sessionId);
|
|
99
99
|
if (set.size === 0) {
|
|
100
|
-
this.
|
|
100
|
+
this.userIdSessions.delete(userId);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
-
async
|
|
105
|
+
async listAllIds(): Promise<string[]> {
|
|
106
106
|
return Array.from(this.sessions.values()).map(s => s.sessionId);
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
async clearAll(): Promise<void> {
|
|
110
110
|
this.sessions.clear();
|
|
111
|
-
this.
|
|
111
|
+
this.userIdSessions.clear();
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
async
|
|
114
|
+
async cleanupExpired(): Promise<void> {
|
|
115
115
|
// In-memory doesn't implement TTL automatically,
|
|
116
116
|
// but we could check createdAt + TTL here if needed.
|
|
117
117
|
// For now, no-op.
|