@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.
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/adapters/agui-adapter.d.mts +119 -0
- package/dist/adapters/agui-adapter.d.ts +119 -0
- package/dist/adapters/agui-adapter.js +109 -0
- package/dist/adapters/agui-adapter.js.map +1 -0
- package/dist/adapters/agui-adapter.mjs +107 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -0
- package/dist/adapters/agui-middleware.d.mts +171 -0
- package/dist/adapters/agui-middleware.d.ts +171 -0
- package/dist/adapters/agui-middleware.js +429 -0
- package/dist/adapters/agui-middleware.js.map +1 -0
- package/dist/adapters/agui-middleware.mjs +417 -0
- package/dist/adapters/agui-middleware.mjs.map +1 -0
- package/dist/adapters/ai-adapter.d.mts +38 -0
- package/dist/adapters/ai-adapter.d.ts +38 -0
- package/dist/adapters/ai-adapter.js +82 -0
- package/dist/adapters/ai-adapter.js.map +1 -0
- package/dist/adapters/ai-adapter.mjs +80 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -0
- package/dist/adapters/langchain-adapter.d.mts +46 -0
- package/dist/adapters/langchain-adapter.d.ts +46 -0
- package/dist/adapters/langchain-adapter.js +102 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/langchain-adapter.mjs +100 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -0
- package/dist/adapters/mastra-adapter.d.mts +49 -0
- package/dist/adapters/mastra-adapter.d.ts +49 -0
- package/dist/adapters/mastra-adapter.js +95 -0
- package/dist/adapters/mastra-adapter.js.map +1 -0
- package/dist/adapters/mastra-adapter.mjs +93 -0
- package/dist/adapters/mastra-adapter.mjs.map +1 -0
- package/dist/client/index.d.mts +119 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +225 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +223 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/react.d.mts +151 -0
- package/dist/client/react.d.ts +151 -0
- package/dist/client/react.js +492 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/react.mjs +489 -0
- package/dist/client/react.mjs.map +1 -0
- package/dist/client/vue.d.mts +157 -0
- package/dist/client/vue.d.ts +157 -0
- package/dist/client/vue.js +474 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client/vue.mjs +471 -0
- package/dist/client/vue.mjs.map +1 -0
- package/dist/events-BP6WyRNh.d.mts +110 -0
- package/dist/events-BP6WyRNh.d.ts +110 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2784 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2723 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-session-client-BOFgPypS.d.ts +389 -0
- package/dist/multi-session-client-DMF3ED2O.d.mts +389 -0
- package/dist/server/index.d.mts +269 -0
- package/dist/server/index.d.ts +269 -0
- package/dist/server/index.js +2444 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2414 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/shared/index.d.mts +24 -0
- package/dist/shared/index.d.ts +24 -0
- package/dist/shared/index.js +223 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +190 -0
- package/dist/shared/index.mjs.map +1 -0
- package/dist/types-SbDlA2VX.d.mts +153 -0
- package/dist/types-SbDlA2VX.d.ts +153 -0
- package/dist/utils-0qmYrqoa.d.mts +92 -0
- package/dist/utils-0qmYrqoa.d.ts +92 -0
- package/package.json +165 -0
- package/src/adapters/agui-adapter.ts +210 -0
- package/src/adapters/agui-middleware.ts +512 -0
- package/src/adapters/ai-adapter.ts +115 -0
- package/src/adapters/langchain-adapter.ts +127 -0
- package/src/adapters/mastra-adapter.ts +126 -0
- package/src/client/core/sse-client.ts +340 -0
- package/src/client/index.ts +26 -0
- package/src/client/react/index.ts +10 -0
- package/src/client/react/useMcp.ts +558 -0
- package/src/client/vue/index.ts +10 -0
- package/src/client/vue/useMcp.ts +542 -0
- package/src/index.ts +11 -0
- package/src/server/handlers/nextjs-handler.ts +216 -0
- package/src/server/handlers/sse-handler.ts +699 -0
- package/src/server/index.ts +57 -0
- package/src/server/mcp/multi-session-client.ts +132 -0
- package/src/server/mcp/oauth-client.ts +1168 -0
- package/src/server/mcp/storage-oauth-provider.ts +239 -0
- package/src/server/storage/file-backend.ts +169 -0
- package/src/server/storage/index.ts +115 -0
- package/src/server/storage/memory-backend.ts +132 -0
- package/src/server/storage/redis-backend.ts +210 -0
- package/src/server/storage/redis.ts +160 -0
- package/src/server/storage/types.ts +109 -0
- package/src/shared/constants.ts +29 -0
- package/src/shared/errors.ts +133 -0
- package/src/shared/events.ts +166 -0
- package/src/shared/index.ts +70 -0
- package/src/shared/types.ts +274 -0
- 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
|
+
}
|