@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.
- 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-B3GPsPsi.d.mts → index-BrDkbut5.d.ts} +2 -1
- package/dist/{index-gNdSQTSz.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 -863
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1048 -858
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-CB4oDrQX.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
- package/dist/{multi-session-client-xj3iQIv6.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 +1054 -864
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +1046 -859
- 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-CsKVXbQB.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
- package/dist/{tool-router-C-Mw1_BQ.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
- package/dist/{types-6SmXege4.d.mts → types-DX71u9gR.d.mts} +11 -3
- package/dist/{types-6SmXege4.d.ts → types-DX71u9gR.d.ts} +11 -3
- 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 -629
- 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 +149 -29
- 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 -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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
148
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
268
|
-
if (data.codeVerifier) {
|
|
369
|
+
if (this._hasCodeVerifier) {
|
|
269
370
|
return;
|
|
270
371
|
}
|
|
271
372
|
|
|
272
|
-
|
|
373
|
+
this._cachedCodeVerifier = verifier;
|
|
374
|
+
this._hasCodeVerifier = true;
|
|
273
375
|
}
|
|
274
376
|
|
|
275
377
|
async codeVerifier(): Promise<string> {
|
|
276
|
-
|
|
378
|
+
if (this._cachedCodeVerifier) {
|
|
379
|
+
return this._cachedCodeVerifier;
|
|
380
|
+
}
|
|
277
381
|
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
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
|
-
|
|
394
|
+
|
|
395
|
+
throw new Error("No code verifier found");
|
|
286
396
|
}
|
|
287
397
|
|
|
288
398
|
async deleteCodeVerifier(): Promise<void> {
|
|
289
|
-
|
|
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
|
|
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) {
|