@mcp-ts/sdk 2.5.2 → 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.
Files changed (73) hide show
  1. package/README.md +3 -574
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -4
  10. package/dist/adapters/mastra-adapter.d.mts +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +3 -0
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +3 -0
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +10 -18
  19. package/dist/client/react.d.ts +10 -18
  20. package/dist/client/react.js +17 -0
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +17 -0
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +4 -4
  25. package/dist/client/vue.d.ts +4 -4
  26. package/dist/client/vue.js +3 -0
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +3 -0
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-B3GPsPsi.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-gNdSQTSz.d.ts → index-DHQvlx4K.d.mts} +2 -1
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +1056 -863
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -858
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CB4oDrQX.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-xj3iQIv6.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
  40. package/dist/server/index.d.mts +338 -88
  41. package/dist/server/index.d.ts +338 -88
  42. package/dist/server/index.js +1054 -864
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1046 -859
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +4 -4
  47. package/dist/shared/index.d.ts +4 -4
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-CsKVXbQB.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-C-Mw1_BQ.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-6SmXege4.d.mts → types-DX71u9gR.d.mts} +11 -3
  53. package/dist/{types-6SmXege4.d.ts → types-DX71u9gR.d.ts} +11 -3
  54. package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
  55. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
  56. package/package.json +2 -1
  57. package/src/client/core/sse-client.ts +5 -0
  58. package/src/client/react/use-mcp.ts +27 -28
  59. package/src/server/handlers/sse-handler.ts +671 -629
  60. package/src/server/index.ts +2 -1
  61. package/src/server/mcp/multi-session-client.ts +34 -10
  62. package/src/server/mcp/oauth-client.ts +197 -205
  63. package/src/server/mcp/storage-oauth-provider.ts +149 -29
  64. package/src/server/mcp/tool-policy-gateway.ts +11 -1
  65. package/src/server/storage/file-backend.ts +25 -21
  66. package/src/server/storage/index.ts +71 -0
  67. package/src/server/storage/memory-backend.ts +24 -12
  68. package/src/server/storage/neon-backend.ts +70 -72
  69. package/src/server/storage/redis-backend.ts +26 -36
  70. package/src/server/storage/sqlite-backend.ts +25 -39
  71. package/src/server/storage/supabase-backend.ts +45 -54
  72. package/src/server/storage/types.ts +22 -2
  73. package/src/shared/types.ts +13 -1
@@ -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 sessions.getCredentials(this.userId, this.sessionId);
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,31 +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 sessions.patchCredentials(this.userId, this.sessionId, data);
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
- const data = await this.getCredentials();
138
-
139
- if (data.clientId && !this._clientId) {
140
- this._clientId = data.clientId;
190
+ if (this._clientId) {
191
+ return {
192
+ client_id: this._clientId,
193
+ ...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
194
+ };
141
195
  }
142
196
 
143
- if (data.clientInformation) {
144
- return data.clientInformation;
197
+ const data = await this.getCredentials();
198
+
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;
145
202
  }
146
203
 
147
- if (!this._clientId) {
148
- return undefined;
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
+ };
149
213
  }
150
214
 
151
- return {
152
- client_id: this._clientId,
153
- ...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
154
- };
215
+ return undefined;
155
216
  }
156
217
 
157
218
  /**
@@ -170,6 +231,24 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
170
231
  */
171
232
  async saveTokens(tokens: OAuthTokens): Promise<void> {
172
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
+ }
173
252
  }
174
253
 
175
254
  get authUrl() {
@@ -177,6 +256,8 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
177
256
  }
178
257
 
179
258
  async state(): Promise<string> {
259
+ this._cachedCodeVerifier = undefined;
260
+ this._hasCodeVerifier = false;
180
261
  const nonce = nanoid(32);
181
262
  await this.patchCredentials({
182
263
  oauthState: {
@@ -200,7 +281,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
200
281
  return { valid: false, error: "OAuth state mismatch" };
201
282
  }
202
283
 
203
- const data = await sessions.getCredentials(this.userId, parsed.sessionId);
284
+ const data = await this._store.getCredentials(this.userId, parsed.sessionId);
204
285
 
205
286
  if (!data) {
206
287
  return { valid: false, error: "Session not found" };
@@ -237,7 +318,24 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
237
318
 
238
319
  async redirectToAuthorization(authUrl: URL): Promise<void> {
239
320
  this._authUrl = authUrl.toString();
240
- await sessions.update(this.userId, this.sessionId, { authUrl: this._authUrl });
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
+
241
339
  if (this.onRedirectCallback) {
242
340
  this.onRedirectCallback(authUrl.toString());
243
341
  }
@@ -247,7 +345,8 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
247
345
  scope: "all" | "client" | "tokens" | "verifier"
248
346
  ): Promise<void> {
249
347
  if (scope === "all") {
250
- await sessions.delete(this.userId, this.sessionId);
348
+ this._cachedTokens = undefined;
349
+ await this._store.delete(this.userId, this.sessionId);
251
350
  } else {
252
351
  const updates: Partial<SessionCredentials> = {};
253
352
 
@@ -255,8 +354,11 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
255
354
  updates.clientInformation = null;
256
355
  updates.clientId = null;
257
356
  } else if (scope === "tokens") {
357
+ this._cachedTokens = undefined;
258
358
  updates.tokens = null;
259
359
  } else if (scope === "verifier") {
360
+ this._cachedCodeVerifier = undefined;
361
+ this._hasCodeVerifier = false;
260
362
  updates.codeVerifier = null;
261
363
  }
262
364
  await this.patchCredentials(updates);
@@ -264,34 +366,52 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
264
366
  }
265
367
 
266
368
  async saveCodeVerifier(verifier: string): Promise<void> {
267
- const data = await this.getCredentials();
268
- if (data.codeVerifier) {
369
+ if (this._hasCodeVerifier) {
269
370
  return;
270
371
  }
271
372
 
272
- await this.patchCredentials({ codeVerifier: verifier });
373
+ this._cachedCodeVerifier = verifier;
374
+ this._hasCodeVerifier = true;
273
375
  }
274
376
 
275
377
  async codeVerifier(): Promise<string> {
276
- const data = await this.getCredentials();
378
+ if (this._cachedCodeVerifier) {
379
+ return this._cachedCodeVerifier;
380
+ }
277
381
 
278
- if (data.clientId && !this._clientId) {
279
- this._clientId = data.clientId;
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;
280
387
  }
281
388
 
282
- if (!data.codeVerifier) {
283
- throw new Error("No code verifier found");
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;
284
393
  }
285
- return data.codeVerifier;
394
+
395
+ throw new Error("No code verifier found");
286
396
  }
287
397
 
288
398
  async deleteCodeVerifier(): Promise<void> {
289
- await this.patchCredentials({ codeVerifier: null });
399
+ this._cachedCodeVerifier = undefined;
400
+ this._hasCodeVerifier = false;
401
+ await this.patchCredentials({
402
+ codeVerifier: null,
403
+ });
290
404
  }
291
405
 
292
406
  async tokens(): Promise<OAuthTokens | undefined> {
407
+ if (this._cachedTokens !== undefined) {
408
+ return this._cachedTokens ?? undefined;
409
+ }
410
+
293
411
  const data = await this.getCredentials();
294
412
 
413
+ this._cachedTokens = data.tokens ?? null;
414
+
295
415
  if (data.clientId && !this._clientId) {
296
416
  this._clientId = data.clientId;
297
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 || !this.credentialsCache) return;
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
- if (!this.memoryCache!.has(sessionKey)) {
118
+ const current = this.memoryCache!.get(sessionKey);
119
+ if (!current) {
128
120
  throw new Error(`Session ${sessionId} not found`);
129
121
  }
130
122
 
131
- const current = this.credentialsCache!.get(sessionKey) ?? { sessionId, userId };
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<Session | null> {
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
- return this.memoryCache!.get(sessionKey) || null;
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
- if (!this.memoryCache!.has(sessionKey)) return null;
146
- return this.credentialsCache!.get(sessionKey) ?? { sessionId, userId };
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
- if (!this.sessions.has(sessionKey)) {
65
+ const current = this.sessions.get(sessionKey);
66
+ if (!current) {
67
67
  throw new Error(`Session ${sessionId} not found`);
68
68
  }
69
69
 
70
- const current = this.credentials.get(sessionKey) ?? { sessionId, userId };
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<Session | null> {
74
+ async get(userId: string, sessionId: string, options?: GetOptions): Promise<SessionResult | null> {
76
75
  const sessionKey = this.getSessionKey(userId, sessionId);
77
- return this.sessions.get(sessionKey) || null;
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
- if (!this.sessions.has(sessionKey)) return null;
83
- return this.credentials.get(sessionKey) ?? { sessionId, userId };
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) {