@mcp-ts/sdk 2.5.3 → 2.6.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 +3 -574
- package/dist/adapters/agui-adapter.d.mts +4 -4
- package/dist/adapters/agui-adapter.d.ts +4 -4
- package/dist/adapters/agui-middleware.d.mts +4 -4
- package/dist/adapters/agui-middleware.d.ts +4 -4
- package/dist/adapters/ai-adapter.d.mts +4 -4
- package/dist/adapters/ai-adapter.d.ts +4 -4
- package/dist/adapters/langchain-adapter.d.mts +4 -4
- package/dist/adapters/langchain-adapter.d.ts +4 -4
- package/dist/adapters/mastra-adapter.d.mts +3 -3
- package/dist/adapters/mastra-adapter.d.ts +3 -3
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +3 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +3 -0
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +10 -18
- package/dist/client/react.d.ts +10 -18
- package/dist/client/react.js +17 -0
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +17 -0
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +4 -4
- package/dist/client/vue.d.ts +4 -4
- package/dist/client/vue.js +3 -0
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +3 -0
- package/dist/client/vue.mjs.map +1 -1
- package/dist/{index-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-C3YGagi2.d.ts → index-DHQvlx4K.d.mts} +2 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +1056 -876
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -871
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-DqzT8Oo7.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
- package/dist/server/index.d.mts +338 -88
- package/dist/server/index.d.ts +338 -88
- package/dist/server/index.js +1053 -876
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1045 -871
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/{tool-router-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
- package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
- package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
- package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
- package/package.json +2 -1
- package/src/client/core/sse-client.ts +5 -0
- package/src/client/react/use-mcp.ts +27 -28
- package/src/server/handlers/sse-handler.ts +671 -633
- package/src/server/index.ts +2 -1
- package/src/server/mcp/multi-session-client.ts +34 -10
- package/src/server/mcp/oauth-client.ts +197 -205
- package/src/server/mcp/storage-oauth-provider.ts +148 -41
- package/src/server/mcp/tool-policy-gateway.ts +11 -1
- package/src/server/storage/file-backend.ts +25 -21
- package/src/server/storage/index.ts +71 -0
- package/src/server/storage/memory-backend.ts +24 -12
- package/src/server/storage/neon-backend.ts +70 -72
- package/src/server/storage/redis-backend.ts +26 -36
- package/src/server/storage/sqlite-backend.ts +25 -39
- package/src/server/storage/supabase-backend.ts +45 -54
- package/src/server/storage/types.ts +22 -2
- package/src/shared/types.ts +13 -3
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
1
2
|
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
2
3
|
import type {
|
|
3
4
|
OAuthClientInformationFull,
|
|
@@ -6,7 +7,7 @@ import type {
|
|
|
6
7
|
OAuthTokens
|
|
7
8
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
8
9
|
import { nanoid } from "nanoid";
|
|
9
|
-
import { sessions, type SessionCredentials } from "../storage/index.js";
|
|
10
|
+
import { sessions, type SessionCredentials, type SessionStore } from "../storage/index.js";
|
|
10
11
|
import {
|
|
11
12
|
DEFAULT_CLIENT_NAME,
|
|
12
13
|
DEFAULT_CLIENT_URI,
|
|
@@ -18,6 +19,50 @@ import {
|
|
|
18
19
|
} from '../../shared/constants.js';
|
|
19
20
|
import { formatOAuthState, parseOAuthState } from '../../shared/utils.js';
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Context stored in AsyncLocalStorage for callback-time code verifier resolution.
|
|
24
|
+
* Stores the raw verifier and method directly, matching Cloudflare's DurableObjectOAuthClientProvider.
|
|
25
|
+
* This avoids a DB read in codeVerifier() — the verifier is loaded once by the caller
|
|
26
|
+
* (from session.credentials after get({includeCredentials: true})) and propagated through ALS.
|
|
27
|
+
*/
|
|
28
|
+
interface CodeVerifierContext {
|
|
29
|
+
verifier: string;
|
|
30
|
+
method: 'S256';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const codeVerifierContext = new AsyncLocalStorage<CodeVerifierContext>();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Run a function inside a code verifier context, providing the raw verifier and method
|
|
37
|
+
* so that codeVerifier() can return them without a DB read.
|
|
38
|
+
*/
|
|
39
|
+
export function runWithCodeVerifierState<T>(
|
|
40
|
+
verifier: string,
|
|
41
|
+
method: 'S256',
|
|
42
|
+
fn: () => Promise<T>
|
|
43
|
+
): Promise<T> {
|
|
44
|
+
return codeVerifierContext.run({ verifier, method }, fn);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function base64UrlEncode(bytes: Uint8Array): string {
|
|
48
|
+
let binary = "";
|
|
49
|
+
for (const byte of bytes) {
|
|
50
|
+
binary += String.fromCharCode(byte);
|
|
51
|
+
}
|
|
52
|
+
return btoa(binary)
|
|
53
|
+
.replace(/\+/g, "-")
|
|
54
|
+
.replace(/\//g, "_")
|
|
55
|
+
.replace(/=+$/, "");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function createCodeChallenge(verifier: string): Promise<string> {
|
|
59
|
+
const digest = await crypto.subtle.digest(
|
|
60
|
+
"SHA-256",
|
|
61
|
+
new TextEncoder().encode(verifier)
|
|
62
|
+
);
|
|
63
|
+
return base64UrlEncode(new Uint8Array(digest));
|
|
64
|
+
}
|
|
65
|
+
|
|
21
66
|
/**
|
|
22
67
|
* Extension of OAuthClientProvider interface with additional methods
|
|
23
68
|
* Enables server-specific tracking and state management
|
|
@@ -44,6 +89,8 @@ export interface StorageOAuthClientProviderOptions {
|
|
|
44
89
|
policyUri?: string;
|
|
45
90
|
clientId?: string;
|
|
46
91
|
clientSecret?: string;
|
|
92
|
+
cachedTokens?: OAuthTokens;
|
|
93
|
+
sessionStore?: SessionStore;
|
|
47
94
|
onRedirect?: (url: string) => void;
|
|
48
95
|
}
|
|
49
96
|
|
|
@@ -63,8 +110,12 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
63
110
|
private readonly policyUri?: string;
|
|
64
111
|
private readonly clientSecret?: string;
|
|
65
112
|
|
|
113
|
+
private _store: SessionStore;
|
|
66
114
|
private _authUrl: string | undefined;
|
|
67
115
|
private _clientId: string | undefined;
|
|
116
|
+
private _cachedCodeVerifier: string | undefined;
|
|
117
|
+
private _hasCodeVerifier = false;
|
|
118
|
+
private _cachedTokens: OAuthTokens | null | undefined;
|
|
68
119
|
private onRedirectCallback?: (url: string) => void;
|
|
69
120
|
|
|
70
121
|
/**
|
|
@@ -82,6 +133,8 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
82
133
|
this.policyUri = options.policyUri;
|
|
83
134
|
this._clientId = options.clientId;
|
|
84
135
|
this.clientSecret = options.clientSecret;
|
|
136
|
+
this._cachedTokens = options.cachedTokens;
|
|
137
|
+
this._store = options.sessionStore ?? sessions;
|
|
85
138
|
this.onRedirectCallback = options.onRedirect;
|
|
86
139
|
}
|
|
87
140
|
|
|
@@ -113,7 +166,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
113
166
|
* @private
|
|
114
167
|
*/
|
|
115
168
|
private async getCredentials(): Promise<SessionCredentials> {
|
|
116
|
-
const data = await
|
|
169
|
+
const data = await this._store.getCredentials(this.userId, this.sessionId);
|
|
117
170
|
if (!data) {
|
|
118
171
|
return { userId: this.userId, sessionId: this.sessionId };
|
|
119
172
|
}
|
|
@@ -127,44 +180,39 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
127
180
|
* @throws Error if session doesn't exist (session must be created by controller layer)
|
|
128
181
|
*/
|
|
129
182
|
private async patchCredentials(data: Partial<SessionCredentials>): Promise<void> {
|
|
130
|
-
await
|
|
183
|
+
await this._store.patchCredentials(this.userId, this.sessionId, data);
|
|
131
184
|
}
|
|
132
185
|
|
|
133
186
|
/**
|
|
134
|
-
* Retrieves stored OAuth client information
|
|
187
|
+
* Retrieves stored OAuth client information.
|
|
135
188
|
*/
|
|
136
189
|
async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
if (this._clientId) {
|
|
191
|
+
return {
|
|
192
|
+
client_id: this._clientId,
|
|
193
|
+
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
194
|
+
};
|
|
141
195
|
}
|
|
142
196
|
|
|
143
|
-
|
|
144
|
-
// in database credentials yet, persist them now so they are available
|
|
145
|
-
// on subsequent connection recovery / session rehydration.
|
|
146
|
-
if (this._clientId && !data.clientId) {
|
|
147
|
-
await this.patchCredentials({
|
|
148
|
-
clientId: this._clientId,
|
|
149
|
-
clientInformation: {
|
|
150
|
-
client_id: this._clientId,
|
|
151
|
-
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
152
|
-
}
|
|
153
|
-
});
|
|
154
|
-
}
|
|
197
|
+
const data = await this.getCredentials();
|
|
155
198
|
|
|
156
|
-
|
|
157
|
-
|
|
199
|
+
// Pre-cache tokens (or absent sentinel) so subsequent tokens() call is 0 DB reads
|
|
200
|
+
if (this._cachedTokens === undefined) {
|
|
201
|
+
this._cachedTokens = data.tokens ?? null;
|
|
158
202
|
}
|
|
159
203
|
|
|
160
|
-
if (
|
|
161
|
-
|
|
204
|
+
if (data.clientId) {
|
|
205
|
+
this._clientId = data.clientId;
|
|
206
|
+
if (data.clientInformation) {
|
|
207
|
+
return data.clientInformation;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
client_id: data.clientId,
|
|
211
|
+
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
212
|
+
};
|
|
162
213
|
}
|
|
163
214
|
|
|
164
|
-
return
|
|
165
|
-
client_id: this._clientId,
|
|
166
|
-
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
167
|
-
};
|
|
215
|
+
return undefined;
|
|
168
216
|
}
|
|
169
217
|
|
|
170
218
|
/**
|
|
@@ -183,6 +231,24 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
183
231
|
*/
|
|
184
232
|
async saveTokens(tokens: OAuthTokens): Promise<void> {
|
|
185
233
|
await this.patchCredentials({ tokens });
|
|
234
|
+
this._cachedTokens = tokens;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Persists static client credentials to DB immediately on creation.
|
|
239
|
+
* Ensures getOrCreateClient() finds them on rehydration, even when the
|
|
240
|
+
* server allows anonymous connect and never triggers DCR.
|
|
241
|
+
*/
|
|
242
|
+
async initializeCredentials(): Promise<void> {
|
|
243
|
+
if (this._clientId) {
|
|
244
|
+
await this.patchCredentials({
|
|
245
|
+
clientId: this._clientId,
|
|
246
|
+
clientInformation: {
|
|
247
|
+
client_id: this._clientId,
|
|
248
|
+
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
}
|
|
186
252
|
}
|
|
187
253
|
|
|
188
254
|
get authUrl() {
|
|
@@ -190,6 +256,8 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
190
256
|
}
|
|
191
257
|
|
|
192
258
|
async state(): Promise<string> {
|
|
259
|
+
this._cachedCodeVerifier = undefined;
|
|
260
|
+
this._hasCodeVerifier = false;
|
|
193
261
|
const nonce = nanoid(32);
|
|
194
262
|
await this.patchCredentials({
|
|
195
263
|
oauthState: {
|
|
@@ -213,7 +281,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
213
281
|
return { valid: false, error: "OAuth state mismatch" };
|
|
214
282
|
}
|
|
215
283
|
|
|
216
|
-
const data = await
|
|
284
|
+
const data = await this._store.getCredentials(this.userId, parsed.sessionId);
|
|
217
285
|
|
|
218
286
|
if (!data) {
|
|
219
287
|
return { valid: false, error: "Session not found" };
|
|
@@ -250,7 +318,24 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
250
318
|
|
|
251
319
|
async redirectToAuthorization(authUrl: URL): Promise<void> {
|
|
252
320
|
this._authUrl = authUrl.toString();
|
|
253
|
-
|
|
321
|
+
|
|
322
|
+
// Extract PKCE parameters from auth URL and persist verifier
|
|
323
|
+
// keyed by state nonce for callback-time retrieval.
|
|
324
|
+
const codeChallenge = authUrl.searchParams.get("code_challenge");
|
|
325
|
+
const state = authUrl.searchParams.get("state");
|
|
326
|
+
|
|
327
|
+
if (this._cachedCodeVerifier && codeChallenge && state) {
|
|
328
|
+
const expectedChallenge = await createCodeChallenge(this._cachedCodeVerifier);
|
|
329
|
+
if (expectedChallenge === codeChallenge) {
|
|
330
|
+
const parsed = parseOAuthState(state);
|
|
331
|
+
if (parsed) {
|
|
332
|
+
await this.patchCredentials({
|
|
333
|
+
codeVerifier: this._cachedCodeVerifier,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
254
339
|
if (this.onRedirectCallback) {
|
|
255
340
|
this.onRedirectCallback(authUrl.toString());
|
|
256
341
|
}
|
|
@@ -260,7 +345,8 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
260
345
|
scope: "all" | "client" | "tokens" | "verifier"
|
|
261
346
|
): Promise<void> {
|
|
262
347
|
if (scope === "all") {
|
|
263
|
-
|
|
348
|
+
this._cachedTokens = undefined;
|
|
349
|
+
await this._store.delete(this.userId, this.sessionId);
|
|
264
350
|
} else {
|
|
265
351
|
const updates: Partial<SessionCredentials> = {};
|
|
266
352
|
|
|
@@ -268,8 +354,11 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
268
354
|
updates.clientInformation = null;
|
|
269
355
|
updates.clientId = null;
|
|
270
356
|
} else if (scope === "tokens") {
|
|
357
|
+
this._cachedTokens = undefined;
|
|
271
358
|
updates.tokens = null;
|
|
272
359
|
} else if (scope === "verifier") {
|
|
360
|
+
this._cachedCodeVerifier = undefined;
|
|
361
|
+
this._hasCodeVerifier = false;
|
|
273
362
|
updates.codeVerifier = null;
|
|
274
363
|
}
|
|
275
364
|
await this.patchCredentials(updates);
|
|
@@ -277,34 +366,52 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
277
366
|
}
|
|
278
367
|
|
|
279
368
|
async saveCodeVerifier(verifier: string): Promise<void> {
|
|
280
|
-
|
|
281
|
-
if (data.codeVerifier) {
|
|
369
|
+
if (this._hasCodeVerifier) {
|
|
282
370
|
return;
|
|
283
371
|
}
|
|
284
372
|
|
|
285
|
-
|
|
373
|
+
this._cachedCodeVerifier = verifier;
|
|
374
|
+
this._hasCodeVerifier = true;
|
|
286
375
|
}
|
|
287
376
|
|
|
288
377
|
async codeVerifier(): Promise<string> {
|
|
289
|
-
|
|
378
|
+
if (this._cachedCodeVerifier) {
|
|
379
|
+
return this._cachedCodeVerifier;
|
|
380
|
+
}
|
|
290
381
|
|
|
291
|
-
|
|
292
|
-
|
|
382
|
+
// ALS context carries the raw verifier directly (set by the caller via
|
|
383
|
+
// runWithCodeVerifierState), avoiding a DB read at callback time.
|
|
384
|
+
const ctx = codeVerifierContext.getStore();
|
|
385
|
+
if (ctx?.verifier) {
|
|
386
|
+
return ctx.verifier;
|
|
293
387
|
}
|
|
294
388
|
|
|
295
|
-
|
|
296
|
-
|
|
389
|
+
// Cross-instance fallback: read verifier from DB (no ALS context).
|
|
390
|
+
const data = await this.getCredentials();
|
|
391
|
+
if (data.codeVerifier) {
|
|
392
|
+
return data.codeVerifier;
|
|
297
393
|
}
|
|
298
|
-
|
|
394
|
+
|
|
395
|
+
throw new Error("No code verifier found");
|
|
299
396
|
}
|
|
300
397
|
|
|
301
398
|
async deleteCodeVerifier(): Promise<void> {
|
|
302
|
-
|
|
399
|
+
this._cachedCodeVerifier = undefined;
|
|
400
|
+
this._hasCodeVerifier = false;
|
|
401
|
+
await this.patchCredentials({
|
|
402
|
+
codeVerifier: null,
|
|
403
|
+
});
|
|
303
404
|
}
|
|
304
405
|
|
|
305
406
|
async tokens(): Promise<OAuthTokens | undefined> {
|
|
407
|
+
if (this._cachedTokens !== undefined) {
|
|
408
|
+
return this._cachedTokens ?? undefined;
|
|
409
|
+
}
|
|
410
|
+
|
|
306
411
|
const data = await this.getCredentials();
|
|
307
412
|
|
|
413
|
+
this._cachedTokens = data.tokens ?? null;
|
|
414
|
+
|
|
308
415
|
if (data.clientId && !this._clientId) {
|
|
309
416
|
this._clientId = data.clientId;
|
|
310
417
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Tool, ListToolsResult, CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
1
|
+
import type { Tool, ListToolsResult, CallToolResult, Implementation } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import type { ToolClient } from '../../shared/types.js';
|
|
3
3
|
import { sessions } from '../storage/index.js';
|
|
4
4
|
import type { Session } from '../storage/types.js';
|
|
@@ -13,6 +13,7 @@ type RawToolClient = ToolClient & {
|
|
|
13
13
|
fetchTools(): Promise<{ tools: Tool[] }>;
|
|
14
14
|
listTools(): Promise<{ tools: Tool[] }>;
|
|
15
15
|
callTool(name: string, args: Record<string, unknown>): Promise<CallToolResult>;
|
|
16
|
+
getServerInfo?(): Implementation | undefined;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -46,6 +47,15 @@ export class ToolPolicyGateway implements ToolClient {
|
|
|
46
47
|
return this.client.getServerId?.();
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Returns the full server metadata from the underlying client, if available.
|
|
52
|
+
* Includes name, version, icons, title, description, and website URL.
|
|
53
|
+
* Available only after the client has connected and completed initialization.
|
|
54
|
+
*/
|
|
55
|
+
getServerInfo(): Implementation | undefined {
|
|
56
|
+
return this.client.getServerInfo?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* Returns the human-readable server name from the underlying client, if available.
|
|
51
61
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import type { SessionStore, Session, SessionCredentials } from './types.js';
|
|
3
|
+
import type { SessionStore, Session, SessionCredentials, GetOptions, SessionResult } from './types.js';
|
|
4
4
|
import { generateSessionId } from '../../shared/utils.js';
|
|
5
5
|
import {
|
|
6
6
|
mergeSessionUpdate,
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
export class FileStorageBackend implements SessionStore {
|
|
17
17
|
private filePath: string;
|
|
18
18
|
private memoryCache: Map<string, Session> | null = null;
|
|
19
|
-
private credentialsCache: Map<string, SessionCredentials> | null = null;
|
|
20
19
|
private initialized = false;
|
|
21
20
|
|
|
22
21
|
/**
|
|
@@ -42,23 +41,16 @@ export class FileStorageBackend implements SessionStore {
|
|
|
42
41
|
const json = JSON.parse(data);
|
|
43
42
|
|
|
44
43
|
this.memoryCache = new Map();
|
|
45
|
-
this.credentialsCache = new Map();
|
|
46
44
|
if (Array.isArray(json.sessions)) {
|
|
47
45
|
json.sessions.forEach((s: Session) => {
|
|
48
46
|
const session = normalizeStoredSession(s);
|
|
49
47
|
this.memoryCache!.set(this.getSessionKey(session.userId || 'unknown', session.sessionId), session);
|
|
50
48
|
});
|
|
51
49
|
}
|
|
52
|
-
if (Array.isArray(json.credentials)) {
|
|
53
|
-
json.credentials.forEach((c: SessionCredentials) => {
|
|
54
|
-
this.credentialsCache!.set(this.getSessionKey(c.userId, c.sessionId), c);
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
50
|
} catch (error: any) {
|
|
58
51
|
if (error.code === 'ENOENT') {
|
|
59
52
|
// File does not exist, initialize empty
|
|
60
53
|
this.memoryCache = new Map();
|
|
61
|
-
this.credentialsCache = new Map();
|
|
62
54
|
await this.flush();
|
|
63
55
|
} else {
|
|
64
56
|
console.error('[FileStorage] Failed to load sessions:', error);
|
|
@@ -75,10 +67,9 @@ export class FileStorageBackend implements SessionStore {
|
|
|
75
67
|
}
|
|
76
68
|
|
|
77
69
|
private async flush(): Promise<void> {
|
|
78
|
-
if (!this.memoryCache
|
|
70
|
+
if (!this.memoryCache) return;
|
|
79
71
|
await fs.writeFile(this.filePath, JSON.stringify({
|
|
80
72
|
sessions: Array.from(this.memoryCache.values()),
|
|
81
|
-
credentials: Array.from(this.credentialsCache.values()),
|
|
82
73
|
}, null, 2), 'utf-8');
|
|
83
74
|
}
|
|
84
75
|
|
|
@@ -124,26 +115,40 @@ export class FileStorageBackend implements SessionStore {
|
|
|
124
115
|
async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
|
|
125
116
|
await this.ensureInitialized();
|
|
126
117
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
127
|
-
|
|
118
|
+
const current = this.memoryCache!.get(sessionKey);
|
|
119
|
+
if (!current) {
|
|
128
120
|
throw new Error(`Session ${sessionId} not found`);
|
|
129
121
|
}
|
|
130
122
|
|
|
131
|
-
|
|
132
|
-
this.credentialsCache!.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
123
|
+
this.memoryCache!.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
133
124
|
await this.flush();
|
|
134
125
|
}
|
|
135
126
|
|
|
136
|
-
async get(userId: string, sessionId: string): Promise<
|
|
127
|
+
async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
|
|
137
128
|
await this.ensureInitialized();
|
|
138
129
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
139
|
-
|
|
130
|
+
const session = this.memoryCache!.get(sessionKey) || null;
|
|
131
|
+
if (!session) return null;
|
|
132
|
+
if (!options?.includeCredentials) {
|
|
133
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
134
|
+
return sessionOnly as Session;
|
|
135
|
+
}
|
|
136
|
+
return session;
|
|
140
137
|
}
|
|
141
138
|
|
|
142
139
|
async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
|
|
143
140
|
await this.ensureInitialized();
|
|
144
141
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
145
|
-
|
|
146
|
-
|
|
142
|
+
const session = this.memoryCache!.get(sessionKey);
|
|
143
|
+
if (!session) return null;
|
|
144
|
+
|
|
145
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
146
|
+
return {
|
|
147
|
+
sessionId, userId,
|
|
148
|
+
clientInformation, tokens, codeVerifier,
|
|
149
|
+
codeVerifierChallenge, codeVerifierNonce,
|
|
150
|
+
clientId, oauthState,
|
|
151
|
+
};
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
async clearCredentials(userId: string, sessionId: string): Promise<void> {
|
|
@@ -151,6 +156,8 @@ export class FileStorageBackend implements SessionStore {
|
|
|
151
156
|
clientInformation: null,
|
|
152
157
|
tokens: null,
|
|
153
158
|
codeVerifier: null,
|
|
159
|
+
codeVerifierChallenge: null,
|
|
160
|
+
codeVerifierNonce: null,
|
|
154
161
|
clientId: null,
|
|
155
162
|
oauthState: null,
|
|
156
163
|
});
|
|
@@ -172,7 +179,6 @@ export class FileStorageBackend implements SessionStore {
|
|
|
172
179
|
await this.ensureInitialized();
|
|
173
180
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
174
181
|
const deleted = this.memoryCache!.delete(sessionKey);
|
|
175
|
-
this.credentialsCache!.delete(sessionKey);
|
|
176
182
|
if (deleted) {
|
|
177
183
|
await this.flush();
|
|
178
184
|
}
|
|
@@ -186,7 +192,6 @@ export class FileStorageBackend implements SessionStore {
|
|
|
186
192
|
async clearAll(): Promise<void> {
|
|
187
193
|
await this.ensureInitialized();
|
|
188
194
|
this.memoryCache!.clear();
|
|
189
|
-
this.credentialsCache!.clear();
|
|
190
195
|
await this.flush();
|
|
191
196
|
}
|
|
192
197
|
|
|
@@ -198,7 +203,6 @@ export class FileStorageBackend implements SessionStore {
|
|
|
198
203
|
if (!isSessionExpired(session)) continue;
|
|
199
204
|
|
|
200
205
|
this.memoryCache!.delete(key);
|
|
201
|
-
this.credentialsCache!.delete(key);
|
|
202
206
|
changed = true;
|
|
203
207
|
}
|
|
204
208
|
|
|
@@ -6,6 +6,7 @@ import { SqliteStorage } from './sqlite-backend.js';
|
|
|
6
6
|
import { SupabaseStorageBackend } from './supabase-backend.js';
|
|
7
7
|
import { NeonStorageBackend, type NeonStorageOptions } from './neon-backend.js';
|
|
8
8
|
import type { SessionStore, Session, SessionMutationEvent, SessionMutationListener } from './types.js';
|
|
9
|
+
import type { McpObservabilityEvent } from '../../shared/events.js';
|
|
9
10
|
|
|
10
11
|
// Re-export types
|
|
11
12
|
export * from './types.js';
|
|
@@ -283,6 +284,76 @@ export function _resetSessionMutationListenersForTesting(): void {
|
|
|
283
284
|
sessionMutationListeners.clear();
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Wraps a SessionStore with a Proxy that emits an McpObservabilityEvent for every
|
|
289
|
+
* method call — reads (`db:read`) and writes (`db:write`) — including duration
|
|
290
|
+
* and any error. Useful for debugging connection flows end-to-end.
|
|
291
|
+
*
|
|
292
|
+
* Usage:
|
|
293
|
+
* ```ts
|
|
294
|
+
* import { sessions, withDbObservability } from '@mcp-ts/sdk/server/storage';
|
|
295
|
+
* const db = withDbObservability(sessions, (event) => console.log('[DB]', event));
|
|
296
|
+
* // Use `db` instead of `sessions` throughout the request
|
|
297
|
+
* ```
|
|
298
|
+
*
|
|
299
|
+
* @param store - The underlying SessionStore instance to wrap.
|
|
300
|
+
* @param emit - Called synchronously after each store method completes
|
|
301
|
+
* (or rejects) with a typed observability event.
|
|
302
|
+
* @returns A drop-in SessionStore replacement.
|
|
303
|
+
*/
|
|
304
|
+
export function withDbObservability(
|
|
305
|
+
store: SessionStore,
|
|
306
|
+
emit: (event: McpObservabilityEvent) => void,
|
|
307
|
+
): SessionStore {
|
|
308
|
+
const readMethods = new Set<keyof SessionStore>([
|
|
309
|
+
'get', 'getCredentials', 'list', 'listIds', 'listAllIds',
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
return new Proxy(store, {
|
|
313
|
+
get(target, prop, receiver) {
|
|
314
|
+
const original = Reflect.get(target, prop, receiver);
|
|
315
|
+
if (typeof original !== 'function') return original;
|
|
316
|
+
|
|
317
|
+
return (...args: any[]) => {
|
|
318
|
+
const method = prop as string;
|
|
319
|
+
const isRead = readMethods.has(prop as keyof SessionStore);
|
|
320
|
+
const start = performance.now();
|
|
321
|
+
|
|
322
|
+
const emitEvent = (error?: string) => {
|
|
323
|
+
emit({
|
|
324
|
+
type: isRead ? 'db:read' : 'db:write',
|
|
325
|
+
level: error ? 'error' : 'debug',
|
|
326
|
+
message: `${method}(${args.map(a =>
|
|
327
|
+
typeof a === 'string' ? a.slice(0, 24) : typeof a,
|
|
328
|
+
).join(', ')})`,
|
|
329
|
+
sessionId: typeof args[1] === 'string' ? args[1] : undefined,
|
|
330
|
+
payload: {
|
|
331
|
+
method,
|
|
332
|
+
argTypes: args.map(a => typeof a),
|
|
333
|
+
durationMs: performance.now() - start,
|
|
334
|
+
...(error ? { error } : {}),
|
|
335
|
+
},
|
|
336
|
+
timestamp: Date.now(),
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
try {
|
|
341
|
+
const result = original.apply(target, args);
|
|
342
|
+
if (result instanceof Promise) {
|
|
343
|
+
return result.then(r => { emitEvent(); return r; })
|
|
344
|
+
.catch(e => { emitEvent(String(e)); throw e; });
|
|
345
|
+
}
|
|
346
|
+
emitEvent();
|
|
347
|
+
return result;
|
|
348
|
+
} catch (e) {
|
|
349
|
+
emitEvent(String(e));
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
286
357
|
/**
|
|
287
358
|
* Global session store instance
|
|
288
359
|
* Uses lazy initialization with a Proxy to handle async setup transparently
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SessionStore, Session, SessionCredentials } from './types.js';
|
|
1
|
+
import type { SessionStore, Session, SessionCredentials, GetOptions, SessionResult } from './types.js';
|
|
2
2
|
import { generateSessionId } from '../../shared/utils.js';
|
|
3
3
|
import { isSessionExpired, mergeSessionUpdate, normalizeNewSession } from './session-lifecycle.js';
|
|
4
4
|
|
|
@@ -9,7 +9,6 @@ import { isSessionExpired, mergeSessionUpdate, normalizeNewSession } from './ses
|
|
|
9
9
|
export class MemoryStorageBackend implements SessionStore {
|
|
10
10
|
// Map<userId:sessionId, Session>
|
|
11
11
|
private sessions = new Map<string, Session>();
|
|
12
|
-
private credentials = new Map<string, SessionCredentials>();
|
|
13
12
|
|
|
14
13
|
// Map<userId, Set<sessionId>>
|
|
15
14
|
private userIdSessions = new Map<string, Set<string>>();
|
|
@@ -63,24 +62,38 @@ export class MemoryStorageBackend implements SessionStore {
|
|
|
63
62
|
|
|
64
63
|
async patchCredentials(userId: string, sessionId: string, data: Partial<SessionCredentials>): Promise<void> {
|
|
65
64
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
66
|
-
|
|
65
|
+
const current = this.sessions.get(sessionKey);
|
|
66
|
+
if (!current) {
|
|
67
67
|
throw new Error(`Session ${sessionId} not found`);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
this.credentials.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
70
|
+
this.sessions.set(sessionKey, { ...current, ...data, sessionId, userId });
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
|
|
75
|
-
async get(userId: string, sessionId: string): Promise<
|
|
74
|
+
async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
|
|
76
75
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
77
|
-
|
|
76
|
+
const session = this.sessions.get(sessionKey) || null;
|
|
77
|
+
if (!session) return session;
|
|
78
|
+
if (!options?.includeCredentials) {
|
|
79
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState, ...sessionOnly } = session;
|
|
80
|
+
return sessionOnly as Session;
|
|
81
|
+
}
|
|
82
|
+
return session;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
async getCredentials(userId: string, sessionId: string): Promise<SessionCredentials | null> {
|
|
81
86
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
const session = this.sessions.get(sessionKey);
|
|
88
|
+
if (!session) return null;
|
|
89
|
+
|
|
90
|
+
const { clientInformation, tokens, codeVerifier, codeVerifierChallenge, codeVerifierNonce, clientId, oauthState } = session;
|
|
91
|
+
return {
|
|
92
|
+
sessionId, userId,
|
|
93
|
+
clientInformation, tokens, codeVerifier,
|
|
94
|
+
codeVerifierChallenge, codeVerifierNonce,
|
|
95
|
+
clientId, oauthState,
|
|
96
|
+
};
|
|
84
97
|
}
|
|
85
98
|
|
|
86
99
|
async clearCredentials(userId: string, sessionId: string): Promise<void> {
|
|
@@ -88,6 +101,8 @@ export class MemoryStorageBackend implements SessionStore {
|
|
|
88
101
|
clientInformation: null,
|
|
89
102
|
tokens: null,
|
|
90
103
|
codeVerifier: null,
|
|
104
|
+
codeVerifierChallenge: null,
|
|
105
|
+
codeVerifierNonce: null,
|
|
91
106
|
clientId: null,
|
|
92
107
|
oauthState: null,
|
|
93
108
|
});
|
|
@@ -115,7 +130,6 @@ export class MemoryStorageBackend implements SessionStore {
|
|
|
115
130
|
async delete(userId: string, sessionId: string): Promise<void> {
|
|
116
131
|
const sessionKey = this.getSessionKey(userId, sessionId);
|
|
117
132
|
this.sessions.delete(sessionKey);
|
|
118
|
-
this.credentials.delete(sessionKey);
|
|
119
133
|
|
|
120
134
|
const set = this.userIdSessions.get(userId);
|
|
121
135
|
if (set) {
|
|
@@ -132,7 +146,6 @@ export class MemoryStorageBackend implements SessionStore {
|
|
|
132
146
|
|
|
133
147
|
async clearAll(): Promise<void> {
|
|
134
148
|
this.sessions.clear();
|
|
135
|
-
this.credentials.clear();
|
|
136
149
|
this.userIdSessions.clear();
|
|
137
150
|
}
|
|
138
151
|
|
|
@@ -141,7 +154,6 @@ export class MemoryStorageBackend implements SessionStore {
|
|
|
141
154
|
if (!isSessionExpired(session)) continue;
|
|
142
155
|
|
|
143
156
|
this.sessions.delete(key);
|
|
144
|
-
this.credentials.delete(key);
|
|
145
157
|
|
|
146
158
|
const set = this.userIdSessions.get(session.userId);
|
|
147
159
|
if (set) {
|