@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.
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-BRZO_JyI.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-C3YGagi2.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 -876
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -871
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CuacvZaQ.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-DqzT8Oo7.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 +1053 -876
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1045 -871
  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-B0qpui-V.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-2qUyZ-cP.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-BkJ_rgzo.d.mts → types-DX71u9gR.d.mts} +11 -5
  53. package/dist/{types-BkJ_rgzo.d.ts → types-DX71u9gR.d.ts} +11 -5
  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 -633
  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 +148 -41
  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 -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 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,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 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 we have clientId/clientSecret from constructor, but they aren't stored
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
- if (data.clientInformation) {
157
- return data.clientInformation;
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 (!this._clientId) {
161
- 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
+ };
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 sessions.getCredentials(this.userId, parsed.sessionId);
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
- 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
+
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
- await sessions.delete(this.userId, this.sessionId);
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
- const data = await this.getCredentials();
281
- if (data.codeVerifier) {
369
+ if (this._hasCodeVerifier) {
282
370
  return;
283
371
  }
284
372
 
285
- await this.patchCredentials({ codeVerifier: verifier });
373
+ this._cachedCodeVerifier = verifier;
374
+ this._hasCodeVerifier = true;
286
375
  }
287
376
 
288
377
  async codeVerifier(): Promise<string> {
289
- const data = await this.getCredentials();
378
+ if (this._cachedCodeVerifier) {
379
+ return this._cachedCodeVerifier;
380
+ }
290
381
 
291
- if (data.clientId && !this._clientId) {
292
- 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;
293
387
  }
294
388
 
295
- if (!data.codeVerifier) {
296
- 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;
297
393
  }
298
- return data.codeVerifier;
394
+
395
+ throw new Error("No code verifier found");
299
396
  }
300
397
 
301
398
  async deleteCodeVerifier(): Promise<void> {
302
- await this.patchCredentials({ codeVerifier: null });
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 || !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) {