@mcp-ts/sdk 1.3.4 → 1.3.5
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 +404 -400
- package/dist/adapters/agui-adapter.d.mts +1 -1
- package/dist/adapters/agui-adapter.d.ts +1 -1
- package/dist/adapters/agui-middleware.d.mts +1 -1
- package/dist/adapters/agui-middleware.d.ts +1 -1
- package/dist/adapters/ai-adapter.d.mts +1 -1
- package/dist/adapters/ai-adapter.d.ts +1 -1
- package/dist/adapters/langchain-adapter.d.mts +1 -1
- package/dist/adapters/langchain-adapter.d.ts +1 -1
- package/dist/adapters/mastra-adapter.d.mts +1 -1
- package/dist/adapters/mastra-adapter.d.ts +1 -1
- package/dist/client/index.d.mts +1 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +14 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +14 -5
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.js +15 -6
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +15 -6
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.js +15 -6
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +15 -6
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +201 -158
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +201 -158
- package/dist/index.mjs.map +1 -1
- package/dist/{multi-session-client-FAFpUzZ4.d.ts → multi-session-client-BYLarghq.d.ts} +29 -19
- package/dist/{multi-session-client-DzjmT7FX.d.mts → multi-session-client-CzhMkE0k.d.mts} +29 -19
- package/dist/server/index.d.mts +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.js +193 -151
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +193 -151
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/index.d.ts +2 -2
- package/dist/shared/index.js +2 -2
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +2 -2
- package/dist/shared/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client/core/sse-client.ts +371 -354
- package/src/client/react/use-mcp.ts +31 -31
- package/src/client/vue/use-mcp.ts +77 -77
- package/src/server/handlers/nextjs-handler.ts +194 -197
- package/src/server/handlers/sse-handler.ts +62 -111
- package/src/server/mcp/oauth-client.ts +67 -79
- package/src/server/mcp/storage-oauth-provider.ts +71 -38
- package/src/server/storage/index.ts +15 -13
- package/src/server/storage/redis-backend.ts +93 -23
- package/src/server/storage/types.ts +12 -12
- package/src/shared/constants.ts +2 -2
- package/src/shared/event-routing.ts +28 -0
|
@@ -28,15 +28,18 @@ import {
|
|
|
28
28
|
ReadResourceResult,
|
|
29
29
|
ReadResourceResultSchema,
|
|
30
30
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
31
|
-
import type {
|
|
31
|
+
import type { OAuthTokens, OAuthClientInformationFull } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
32
32
|
import { StorageOAuthClientProvider, type AgentsOAuthProvider } from './storage-oauth-provider.js';
|
|
33
33
|
import { sanitizeServerLabel } from '../../shared/utils.js';
|
|
34
34
|
import { Emitter, type McpConnectionEvent, type McpObservabilityEvent, type McpConnectionState } from '../../shared/events.js';
|
|
35
35
|
import { UnauthorizedError } from '../../shared/errors.js';
|
|
36
36
|
import { storage } from '../storage/index.js';
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
import {
|
|
38
|
+
MCP_CLIENT_NAME,
|
|
39
|
+
MCP_CLIENT_VERSION,
|
|
40
|
+
SESSION_TTL_SECONDS,
|
|
41
|
+
STATE_EXPIRATION_MS,
|
|
42
|
+
} from '../../shared/constants.js';
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
45
|
* Supported MCP transport types
|
|
@@ -300,49 +303,34 @@ export class MCPClient {
|
|
|
300
303
|
throw new Error('Missing required connection metadata');
|
|
301
304
|
}
|
|
302
305
|
|
|
303
|
-
const clientMetadata: OAuthClientMetadata = {
|
|
304
|
-
client_name: this.clientName || 'MCP Assistant',
|
|
305
|
-
redirect_uris: [this.callbackUrl],
|
|
306
|
-
grant_types: ['authorization_code', 'refresh_token'],
|
|
307
|
-
response_types: ['code'],
|
|
308
|
-
token_endpoint_auth_method: this.clientSecret ? 'client_secret_basic' : 'none',
|
|
309
|
-
client_uri: this.clientUri || 'https://mcp-assistant.in',
|
|
310
|
-
logo_uri: this.logoUri || 'https://mcp-assistant.in/logo.png',
|
|
311
|
-
policy_uri: this.policyUri || 'https://mcp-assistant.in/privacy',
|
|
312
|
-
software_id: '@mcp-ts',
|
|
313
|
-
software_version: '1.0.0-beta.4',
|
|
314
|
-
...(this.clientId ? { client_id: this.clientId } : {}),
|
|
315
|
-
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
316
|
-
};
|
|
317
|
-
|
|
318
306
|
if (!this.oauthProvider) {
|
|
319
307
|
if (!this.serverId) {
|
|
320
308
|
throw new Error('serverId required for OAuth provider initialization');
|
|
321
309
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
this.
|
|
325
|
-
this.
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
this.
|
|
329
|
-
|
|
310
|
+
this.oauthProvider = new StorageOAuthClientProvider({
|
|
311
|
+
identity: this.identity,
|
|
312
|
+
serverId: this.serverId,
|
|
313
|
+
sessionId: this.sessionId,
|
|
314
|
+
redirectUrl: this.callbackUrl!,
|
|
315
|
+
clientName: this.clientName,
|
|
316
|
+
clientUri: this.clientUri,
|
|
317
|
+
logoUri: this.logoUri,
|
|
318
|
+
policyUri: this.policyUri,
|
|
319
|
+
clientId: this.clientId,
|
|
320
|
+
clientSecret: this.clientSecret,
|
|
321
|
+
onRedirect: (redirectUrl: string) => {
|
|
330
322
|
if (this.onRedirect) {
|
|
331
323
|
this.onRedirect(redirectUrl);
|
|
332
324
|
}
|
|
333
325
|
}
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
if (this.clientId && this.oauthProvider) {
|
|
337
|
-
this.oauthProvider.clientId = this.clientId;
|
|
338
|
-
}
|
|
326
|
+
});
|
|
339
327
|
}
|
|
340
328
|
|
|
341
329
|
if (!this.client) {
|
|
342
330
|
this.client = new Client(
|
|
343
331
|
{
|
|
344
|
-
name:
|
|
345
|
-
version:
|
|
332
|
+
name: MCP_CLIENT_NAME,
|
|
333
|
+
version: MCP_CLIENT_VERSION,
|
|
346
334
|
},
|
|
347
335
|
{
|
|
348
336
|
capabilities: {
|
|
@@ -363,34 +351,34 @@ export class MCPClient {
|
|
|
363
351
|
if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
|
|
364
352
|
this.createdAt = Date.now();
|
|
365
353
|
console.log(`[MCPClient] Creating initial session ${this.sessionId} for OAuth flow`);
|
|
366
|
-
await storage.createSession({
|
|
367
|
-
sessionId: this.sessionId,
|
|
368
|
-
identity: this.identity,
|
|
369
|
-
serverId: this.serverId,
|
|
370
|
-
serverName: this.serverName,
|
|
371
|
-
serverUrl: this.serverUrl,
|
|
372
|
-
callbackUrl: this.callbackUrl,
|
|
373
|
-
transportType: this.transportType || 'streamable_http',
|
|
374
|
-
createdAt: this.createdAt,
|
|
375
|
-
active: false,
|
|
376
|
-
}, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
|
|
377
|
-
}
|
|
378
|
-
}
|
|
354
|
+
await storage.createSession({
|
|
355
|
+
sessionId: this.sessionId,
|
|
356
|
+
identity: this.identity,
|
|
357
|
+
serverId: this.serverId,
|
|
358
|
+
serverName: this.serverName,
|
|
359
|
+
serverUrl: this.serverUrl,
|
|
360
|
+
callbackUrl: this.callbackUrl,
|
|
361
|
+
transportType: this.transportType || 'streamable_http',
|
|
362
|
+
createdAt: this.createdAt,
|
|
363
|
+
active: false,
|
|
364
|
+
}, Math.floor(STATE_EXPIRATION_MS / 1000)); // Short TTL until connection succeeds
|
|
365
|
+
}
|
|
366
|
+
}
|
|
379
367
|
|
|
380
368
|
/**
|
|
381
369
|
* Saves current session state to storage
|
|
382
370
|
* Creates new session if it doesn't exist, updates if it does
|
|
383
|
-
* @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
|
|
384
|
-
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
385
|
-
* @private
|
|
386
|
-
*/
|
|
387
|
-
private async saveSession(
|
|
388
|
-
ttl: number = SESSION_TTL_SECONDS,
|
|
389
|
-
active: boolean = true
|
|
390
|
-
): Promise<void> {
|
|
391
|
-
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
371
|
+
* @param ttl - Time-to-live in seconds (defaults to 12hr for connected sessions)
|
|
372
|
+
* @param active - Session status marker used to avoid unnecessary TTL rewrites
|
|
373
|
+
* @private
|
|
374
|
+
*/
|
|
375
|
+
private async saveSession(
|
|
376
|
+
ttl: number = SESSION_TTL_SECONDS,
|
|
377
|
+
active: boolean = true
|
|
378
|
+
): Promise<void> {
|
|
379
|
+
if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
394
382
|
|
|
395
383
|
const sessionData = {
|
|
396
384
|
sessionId: this.sessionId,
|
|
@@ -398,11 +386,11 @@ export class MCPClient {
|
|
|
398
386
|
serverId: this.serverId,
|
|
399
387
|
serverName: this.serverName,
|
|
400
388
|
serverUrl: this.serverUrl,
|
|
401
|
-
callbackUrl: this.callbackUrl,
|
|
402
|
-
transportType: (this.transportType || 'streamable_http') as TransportType,
|
|
403
|
-
createdAt: this.createdAt || Date.now(),
|
|
404
|
-
active,
|
|
405
|
-
};
|
|
389
|
+
callbackUrl: this.callbackUrl,
|
|
390
|
+
transportType: (this.transportType || 'streamable_http') as TransportType,
|
|
391
|
+
createdAt: this.createdAt || Date.now(),
|
|
392
|
+
active,
|
|
393
|
+
};
|
|
406
394
|
|
|
407
395
|
// Try to update first, create if doesn't exist
|
|
408
396
|
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
@@ -512,16 +500,16 @@ export class MCPClient {
|
|
|
512
500
|
this.emitStateChange('CONNECTED');
|
|
513
501
|
this.emitProgress('Connected successfully');
|
|
514
502
|
|
|
515
|
-
// Promote short-lived OAuth-pending session TTL to long-lived active TTL once.
|
|
516
|
-
// Also persist when transport negotiation changed the effective transport.
|
|
517
|
-
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
518
|
-
const needsTransportUpdate = !existingSession || existingSession.transportType !== this.transportType;
|
|
519
|
-
const needsTtlPromotion = !existingSession || existingSession.active !== true;
|
|
520
|
-
|
|
521
|
-
if (needsTransportUpdate || needsTtlPromotion) {
|
|
522
|
-
console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
|
|
523
|
-
await this.saveSession(SESSION_TTL_SECONDS, true);
|
|
524
|
-
}
|
|
503
|
+
// Promote short-lived OAuth-pending session TTL to long-lived active TTL once.
|
|
504
|
+
// Also persist when transport negotiation changed the effective transport.
|
|
505
|
+
const existingSession = await storage.getSession(this.identity, this.sessionId);
|
|
506
|
+
const needsTransportUpdate = !existingSession || existingSession.transportType !== this.transportType;
|
|
507
|
+
const needsTtlPromotion = !existingSession || existingSession.active !== true;
|
|
508
|
+
|
|
509
|
+
if (needsTransportUpdate || needsTtlPromotion) {
|
|
510
|
+
console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
|
|
511
|
+
await this.saveSession(SESSION_TTL_SECONDS, true);
|
|
512
|
+
}
|
|
525
513
|
} catch (error) {
|
|
526
514
|
/** Handle Authentication Errors */
|
|
527
515
|
if (
|
|
@@ -531,7 +519,7 @@ export class MCPClient {
|
|
|
531
519
|
this.emitStateChange('AUTHENTICATING');
|
|
532
520
|
// Save session with 10min TTL for OAuth pending state
|
|
533
521
|
console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
|
|
534
|
-
await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
|
|
522
|
+
await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
|
|
535
523
|
|
|
536
524
|
/** Get OAuth authorization URL if available */
|
|
537
525
|
let authUrl = '';
|
|
@@ -620,8 +608,8 @@ export class MCPClient {
|
|
|
620
608
|
|
|
621
609
|
this.client = new Client(
|
|
622
610
|
{
|
|
623
|
-
name:
|
|
624
|
-
version:
|
|
611
|
+
name: MCP_CLIENT_NAME,
|
|
612
|
+
version: MCP_CLIENT_VERSION,
|
|
625
613
|
},
|
|
626
614
|
{
|
|
627
615
|
capabilities: {
|
|
@@ -642,7 +630,7 @@ export class MCPClient {
|
|
|
642
630
|
this.emitStateChange('CONNECTED');
|
|
643
631
|
// Update session with 12hr TTL after successful OAuth
|
|
644
632
|
console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
|
|
645
|
-
await this.saveSession(SESSION_TTL_SECONDS, true);
|
|
633
|
+
await this.saveSession(SESSION_TTL_SECONDS, true);
|
|
646
634
|
|
|
647
635
|
return; // Success, exit function
|
|
648
636
|
|
|
@@ -980,8 +968,8 @@ export class MCPClient {
|
|
|
980
968
|
|
|
981
969
|
this.client = new Client(
|
|
982
970
|
{
|
|
983
|
-
name:
|
|
984
|
-
version:
|
|
971
|
+
name: MCP_CLIENT_NAME,
|
|
972
|
+
version: MCP_CLIENT_VERSION,
|
|
985
973
|
},
|
|
986
974
|
{ capabilities: {} }
|
|
987
975
|
);
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
|
|
2
1
|
import type { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
3
2
|
import type {
|
|
4
|
-
OAuthClientInformation,
|
|
5
3
|
OAuthClientInformationFull,
|
|
4
|
+
OAuthClientInformationMixed,
|
|
6
5
|
OAuthClientMetadata,
|
|
7
6
|
OAuthTokens
|
|
8
7
|
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
9
8
|
import { storage, SessionData } from "../storage/index.js";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_CLIENT_NAME,
|
|
11
|
+
DEFAULT_CLIENT_URI,
|
|
12
|
+
DEFAULT_LOGO_URI,
|
|
13
|
+
DEFAULT_POLICY_URI,
|
|
14
|
+
SOFTWARE_ID,
|
|
15
|
+
SOFTWARE_VERSION,
|
|
16
|
+
TOKEN_EXPIRY_BUFFER_MS,
|
|
17
|
+
} from '../../shared/constants.js';
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* Extension of OAuthClientProvider interface with additional methods
|
|
@@ -26,56 +33,74 @@ export interface AgentsOAuthProvider extends OAuthClientProvider {
|
|
|
26
33
|
setTokenExpiresAt(expiresAt: number): void;
|
|
27
34
|
}
|
|
28
35
|
|
|
36
|
+
export interface StorageOAuthClientProviderOptions {
|
|
37
|
+
identity: string;
|
|
38
|
+
serverId: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
redirectUrl: string;
|
|
41
|
+
clientName?: string;
|
|
42
|
+
clientUri?: string;
|
|
43
|
+
logoUri?: string;
|
|
44
|
+
policyUri?: string;
|
|
45
|
+
clientId?: string;
|
|
46
|
+
clientSecret?: string;
|
|
47
|
+
onRedirect?: (url: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
29
50
|
/**
|
|
30
51
|
* Storage-backed OAuth provider implementation for MCP
|
|
31
52
|
* Stores OAuth tokens, client information, and PKCE verifiers using the configured StorageBackend
|
|
32
53
|
*/
|
|
33
54
|
export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
55
|
+
public readonly identity: string;
|
|
56
|
+
public readonly serverId: string;
|
|
57
|
+
public readonly sessionId: string;
|
|
58
|
+
public readonly redirectUrl: string;
|
|
59
|
+
|
|
60
|
+
private readonly clientName?: string;
|
|
61
|
+
private readonly clientUri?: string;
|
|
62
|
+
private readonly logoUri?: string;
|
|
63
|
+
private readonly policyUri?: string;
|
|
64
|
+
private readonly clientSecret?: string;
|
|
65
|
+
|
|
34
66
|
private _authUrl: string | undefined;
|
|
35
67
|
private _clientId: string | undefined;
|
|
36
68
|
private onRedirectCallback?: (url: string) => void;
|
|
37
69
|
private tokenExpiresAt?: number;
|
|
38
70
|
|
|
39
71
|
/**
|
|
40
|
-
* Creates a new
|
|
41
|
-
* @param
|
|
42
|
-
* @param serverId - Server identifier (for tracking which server this OAuth session belongs to)
|
|
43
|
-
* @param sessionId - Session identifier (used as OAuth state)
|
|
44
|
-
* @param clientName - OAuth client name
|
|
45
|
-
* @param baseRedirectUrl - OAuth callback URL
|
|
46
|
-
* @param onRedirect - Optional callback when redirect to authorization is needed
|
|
72
|
+
* Creates a new storage-backed OAuth provider
|
|
73
|
+
* @param options - Provider configuration
|
|
47
74
|
*/
|
|
48
|
-
constructor(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.
|
|
75
|
+
constructor(options: StorageOAuthClientProviderOptions) {
|
|
76
|
+
this.identity = options.identity;
|
|
77
|
+
this.serverId = options.serverId;
|
|
78
|
+
this.sessionId = options.sessionId;
|
|
79
|
+
this.redirectUrl = options.redirectUrl;
|
|
80
|
+
this.clientName = options.clientName;
|
|
81
|
+
this.clientUri = options.clientUri;
|
|
82
|
+
this.logoUri = options.logoUri;
|
|
83
|
+
this.policyUri = options.policyUri;
|
|
84
|
+
this._clientId = options.clientId;
|
|
85
|
+
this.clientSecret = options.clientSecret;
|
|
86
|
+
this.onRedirectCallback = options.onRedirect;
|
|
57
87
|
}
|
|
58
88
|
|
|
59
89
|
get clientMetadata(): OAuthClientMetadata {
|
|
60
90
|
return {
|
|
61
|
-
client_name: this.clientName,
|
|
62
|
-
client_uri: this.clientUri,
|
|
91
|
+
client_name: this.clientName || DEFAULT_CLIENT_NAME,
|
|
92
|
+
client_uri: this.clientUri || DEFAULT_CLIENT_URI,
|
|
93
|
+
logo_uri: this.logoUri || DEFAULT_LOGO_URI,
|
|
94
|
+
policy_uri: this.policyUri || DEFAULT_POLICY_URI,
|
|
63
95
|
grant_types: ["authorization_code", "refresh_token"],
|
|
64
96
|
redirect_uris: [this.redirectUrl],
|
|
65
97
|
response_types: ["code"],
|
|
66
|
-
token_endpoint_auth_method: "none",
|
|
67
|
-
|
|
98
|
+
token_endpoint_auth_method: this.clientSecret ? "client_secret_basic" : "none",
|
|
99
|
+
software_id: SOFTWARE_ID,
|
|
100
|
+
software_version: SOFTWARE_VERSION,
|
|
68
101
|
};
|
|
69
102
|
}
|
|
70
103
|
|
|
71
|
-
get clientUri() {
|
|
72
|
-
return new URL(this.redirectUrl).origin;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get redirectUrl() {
|
|
76
|
-
return this.baseRedirectUrl;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
104
|
get clientId() {
|
|
80
105
|
return this._clientId;
|
|
81
106
|
}
|
|
@@ -91,7 +116,6 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
91
116
|
private async getSessionData(): Promise<SessionData> {
|
|
92
117
|
const data = await storage.getSession(this.identity, this.sessionId);
|
|
93
118
|
if (!data) {
|
|
94
|
-
// Return empty/partial object if not found
|
|
95
119
|
return {} as SessionData;
|
|
96
120
|
}
|
|
97
121
|
return data;
|
|
@@ -110,14 +134,25 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
110
134
|
/**
|
|
111
135
|
* Retrieves stored OAuth client information
|
|
112
136
|
*/
|
|
113
|
-
async clientInformation(): Promise<
|
|
137
|
+
async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {
|
|
114
138
|
const data = await this.getSessionData();
|
|
115
139
|
|
|
116
140
|
if (data.clientId && !this._clientId) {
|
|
117
141
|
this._clientId = data.clientId;
|
|
118
142
|
}
|
|
119
143
|
|
|
120
|
-
|
|
144
|
+
if (data.clientInformation) {
|
|
145
|
+
return data.clientInformation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!this._clientId) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
client_id: this._clientId,
|
|
154
|
+
...(this.clientSecret ? { client_secret: this.clientSecret } : {}),
|
|
155
|
+
};
|
|
121
156
|
}
|
|
122
157
|
|
|
123
158
|
/**
|
|
@@ -152,7 +187,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
152
187
|
return this.sessionId;
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
async checkState(
|
|
190
|
+
async checkState(_state: string): Promise<{ valid: boolean; serverId?: string; error?: string }> {
|
|
156
191
|
const data = await storage.getSession(this.identity, this.sessionId);
|
|
157
192
|
|
|
158
193
|
if (!data) {
|
|
@@ -162,7 +197,7 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
162
197
|
return { valid: true, serverId: this.serverId };
|
|
163
198
|
}
|
|
164
199
|
|
|
165
|
-
async consumeState(
|
|
200
|
+
async consumeState(_state: string): Promise<void> {
|
|
166
201
|
// No-op
|
|
167
202
|
}
|
|
168
203
|
|
|
@@ -179,8 +214,6 @@ export class StorageOAuthClientProvider implements AgentsOAuthProvider {
|
|
|
179
214
|
if (scope === "all") {
|
|
180
215
|
await storage.removeSession(this.identity, this.sessionId);
|
|
181
216
|
} else {
|
|
182
|
-
const data = await this.getSessionData();
|
|
183
|
-
// Create a copy to modify
|
|
184
217
|
const updates: Partial<SessionData> = {};
|
|
185
218
|
|
|
186
219
|
if (scope === "client") {
|
|
@@ -12,6 +12,13 @@ export { RedisStorageBackend, MemoryStorageBackend, FileStorageBackend, SqliteSt
|
|
|
12
12
|
let storageInstance: StorageBackend | null = null;
|
|
13
13
|
let storagePromise: Promise<StorageBackend> | null = null;
|
|
14
14
|
|
|
15
|
+
async function initializeStorage<T extends StorageBackend>(store: T): Promise<T> {
|
|
16
|
+
if (typeof store.init === 'function') {
|
|
17
|
+
await store.init();
|
|
18
|
+
}
|
|
19
|
+
return store;
|
|
20
|
+
}
|
|
21
|
+
|
|
15
22
|
async function createStorage(): Promise<StorageBackend> {
|
|
16
23
|
const type = process.env.MCP_TS_STORAGE_TYPE?.toLowerCase();
|
|
17
24
|
|
|
@@ -38,17 +45,13 @@ async function createStorage(): Promise<StorageBackend> {
|
|
|
38
45
|
console.warn('[Storage] MCP_TS_STORAGE_TYPE is "file" but MCP_TS_STORAGE_FILE is missing');
|
|
39
46
|
}
|
|
40
47
|
console.log(`[Storage] Using File storage (${filePath}) (Explicit)`);
|
|
41
|
-
|
|
42
|
-
store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
|
|
43
|
-
return store;
|
|
48
|
+
return await initializeStorage(new FileStorageBackend({ path: filePath }));
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
if (type === 'sqlite') {
|
|
47
52
|
const dbPath = process.env.MCP_TS_STORAGE_SQLITE_PATH;
|
|
48
53
|
console.log(`[Storage] Using SQLite storage (${dbPath || 'default'}) (Explicit)`);
|
|
49
|
-
|
|
50
|
-
store.init().catch(err => console.error('[Storage] Failed to initialize SQLite storage:', err));
|
|
51
|
-
return store;
|
|
54
|
+
return await initializeStorage(new SqliteStorage({ path: dbPath }));
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
if (type === 'memory') {
|
|
@@ -72,16 +75,12 @@ async function createStorage(): Promise<StorageBackend> {
|
|
|
72
75
|
|
|
73
76
|
if (process.env.MCP_TS_STORAGE_FILE) {
|
|
74
77
|
console.log(`[Storage] Auto-detected MCP_TS_STORAGE_FILE. Using File storage (${process.env.MCP_TS_STORAGE_FILE}).`);
|
|
75
|
-
|
|
76
|
-
store.init().catch(err => console.error('[Storage] Failed to initialize file storage:', err));
|
|
77
|
-
return store;
|
|
78
|
+
return await initializeStorage(new FileStorageBackend({ path: process.env.MCP_TS_STORAGE_FILE }));
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
if (process.env.MCP_TS_STORAGE_SQLITE_PATH) {
|
|
81
82
|
console.log(`[Storage] Auto-detected MCP_TS_STORAGE_SQLITE_PATH. Using SQLite storage (${process.env.MCP_TS_STORAGE_SQLITE_PATH}).`);
|
|
82
|
-
|
|
83
|
-
store.init().catch(err => console.error('[Storage] Failed to initialize SQLite storage:', err));
|
|
84
|
-
return store;
|
|
83
|
+
return await initializeStorage(new SqliteStorage({ path: process.env.MCP_TS_STORAGE_SQLITE_PATH }));
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
console.log('[Storage] No storage configured. Using In-Memory storage (Default).');
|
|
@@ -94,7 +93,10 @@ async function getStorage(): Promise<StorageBackend> {
|
|
|
94
93
|
}
|
|
95
94
|
|
|
96
95
|
if (!storagePromise) {
|
|
97
|
-
storagePromise = createStorage()
|
|
96
|
+
storagePromise = createStorage().catch((error) => {
|
|
97
|
+
storagePromise = null;
|
|
98
|
+
throw error;
|
|
99
|
+
});
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
storageInstance = await storagePromise;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import type { Redis } from 'ioredis';
|
|
3
3
|
import { customAlphabet } from 'nanoid';
|
|
4
|
-
import { StorageBackend, SessionData
|
|
4
|
+
import { StorageBackend, SessionData } from './types';
|
|
5
5
|
import { SESSION_TTL_SECONDS } from '../../shared/constants.js';
|
|
6
6
|
|
|
7
7
|
/** first char: letters only (required by OpenAI) */
|
|
@@ -22,6 +22,8 @@ const rest = customAlphabet(
|
|
|
22
22
|
export class RedisStorageBackend implements StorageBackend {
|
|
23
23
|
private readonly DEFAULT_TTL = SESSION_TTL_SECONDS;
|
|
24
24
|
private readonly KEY_PREFIX = 'mcp:session:';
|
|
25
|
+
private readonly IDENTITY_KEY_PREFIX = 'mcp:identity:';
|
|
26
|
+
private readonly IDENTITY_KEY_SUFFIX = ':sessions';
|
|
25
27
|
|
|
26
28
|
constructor(private redis: Redis) { }
|
|
27
29
|
|
|
@@ -38,7 +40,42 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
38
40
|
* @private
|
|
39
41
|
*/
|
|
40
42
|
private getIdentityKey(identity: string): string {
|
|
41
|
-
return
|
|
43
|
+
return `${this.IDENTITY_KEY_PREFIX}${identity}${this.IDENTITY_KEY_SUFFIX}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private parseIdentityFromKey(identityKey: string): string {
|
|
47
|
+
return identityKey.slice(
|
|
48
|
+
this.IDENTITY_KEY_PREFIX.length,
|
|
49
|
+
identityKey.length - this.IDENTITY_KEY_SUFFIX.length
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
private async scanKeys(pattern: string): Promise<string[]> {
|
|
54
|
+
const redis = this.redis as Redis & {
|
|
55
|
+
scan?: (cursor: string, ...args: Array<string | number>) => Promise<[string, string[]]>;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (typeof redis.scan !== 'function') {
|
|
59
|
+
return await this.redis.keys(pattern);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const keys = new Set<string>();
|
|
63
|
+
let cursor = '0';
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
do {
|
|
67
|
+
const [nextCursor, batch] = await redis.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
|
|
68
|
+
cursor = nextCursor;
|
|
69
|
+
for (const key of batch) {
|
|
70
|
+
keys.add(key);
|
|
71
|
+
}
|
|
72
|
+
} while (cursor !== '0');
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.warn('[RedisStorage] SCAN failed, falling back to KEYS:', error);
|
|
75
|
+
return await this.redis.keys(pattern);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Array.from(keys);
|
|
42
79
|
}
|
|
43
80
|
|
|
44
81
|
generateSessionId(): string {
|
|
@@ -121,18 +158,14 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
121
158
|
}
|
|
122
159
|
|
|
123
160
|
async getIdentityMcpSessions(identity: string): Promise<string[]> {
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
return await this.redis.smembers(identityKey);
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.error(`[RedisStorage] Failed to get sessions for ${identity}:`, error);
|
|
129
|
-
return [];
|
|
130
|
-
}
|
|
161
|
+
const sessions = await this.getIdentitySessionsData(identity);
|
|
162
|
+
return sessions.map((session) => session.sessionId);
|
|
131
163
|
}
|
|
132
164
|
|
|
133
165
|
async getIdentitySessionsData(identity: string): Promise<SessionData[]> {
|
|
134
166
|
try {
|
|
135
|
-
const
|
|
167
|
+
const identityKey = this.getIdentityKey(identity);
|
|
168
|
+
const sessionIds = await this.redis.smembers(identityKey);
|
|
136
169
|
if (sessionIds.length === 0) return [];
|
|
137
170
|
|
|
138
171
|
const results = await Promise.all(
|
|
@@ -142,6 +175,11 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
142
175
|
})
|
|
143
176
|
);
|
|
144
177
|
|
|
178
|
+
const staleSessionIds = sessionIds.filter((_, index) => results[index] === null);
|
|
179
|
+
if (staleSessionIds.length > 0) {
|
|
180
|
+
await this.redis.srem(identityKey, ...staleSessionIds);
|
|
181
|
+
}
|
|
182
|
+
|
|
145
183
|
return results.filter((session): session is SessionData => session !== null);
|
|
146
184
|
} catch (error) {
|
|
147
185
|
console.error(`[RedisStorage] Failed to get session data for ${identity}:`, error);
|
|
@@ -163,9 +201,24 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
163
201
|
|
|
164
202
|
async getAllSessionIds(): Promise<string[]> {
|
|
165
203
|
try {
|
|
166
|
-
const
|
|
167
|
-
const
|
|
168
|
-
|
|
204
|
+
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
205
|
+
const sessions = await Promise.all(
|
|
206
|
+
keys.map(async (key) => {
|
|
207
|
+
const data = await this.redis.get(key);
|
|
208
|
+
if (!data) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
return (JSON.parse(data) as SessionData).sessionId;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('[RedisStorage] Failed to parse session while listing all session IDs:', error);
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
return sessions.filter((sessionId): sessionId is string => sessionId !== null);
|
|
169
222
|
} catch (error) {
|
|
170
223
|
console.error('[RedisStorage] Failed to get all sessions:', error);
|
|
171
224
|
return [];
|
|
@@ -174,10 +227,11 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
174
227
|
|
|
175
228
|
async clearAll(): Promise<void> {
|
|
176
229
|
try {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
const keys = await this.scanKeys(`${this.KEY_PREFIX}*`);
|
|
231
|
+
const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
|
|
232
|
+
const allKeys = [...keys, ...identityKeys];
|
|
233
|
+
if (allKeys.length > 0) {
|
|
234
|
+
await this.redis.del(...allKeys);
|
|
181
235
|
}
|
|
182
236
|
} catch (error) {
|
|
183
237
|
console.error('[RedisStorage] Failed to clear sessions:', error);
|
|
@@ -186,13 +240,29 @@ export class RedisStorageBackend implements StorageBackend {
|
|
|
186
240
|
|
|
187
241
|
async cleanupExpiredSessions(): Promise<void> {
|
|
188
242
|
try {
|
|
189
|
-
const
|
|
190
|
-
|
|
243
|
+
const identityKeys = await this.scanKeys(`${this.IDENTITY_KEY_PREFIX}*${this.IDENTITY_KEY_SUFFIX}`);
|
|
244
|
+
|
|
245
|
+
for (const identityKey of identityKeys) {
|
|
246
|
+
const identity = this.parseIdentityFromKey(identityKey);
|
|
247
|
+
const sessionIds = await this.redis.smembers(identityKey);
|
|
248
|
+
|
|
249
|
+
if (sessionIds.length === 0) {
|
|
250
|
+
await this.redis.del(identityKey);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const existenceChecks = await Promise.all(
|
|
255
|
+
sessionIds.map((sessionId) => this.redis.exists(this.getSessionKey(identity, sessionId)))
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const staleSessionIds = sessionIds.filter((_, index) => existenceChecks[index] === 0);
|
|
259
|
+
if (staleSessionIds.length > 0) {
|
|
260
|
+
await this.redis.srem(identityKey, ...staleSessionIds);
|
|
261
|
+
}
|
|
191
262
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
await this.redis.del(key);
|
|
263
|
+
const remainingCount = await this.redis.scard(identityKey);
|
|
264
|
+
if (remainingCount === 0) {
|
|
265
|
+
await this.redis.del(identityKey);
|
|
196
266
|
}
|
|
197
267
|
}
|
|
198
268
|
} catch (error) {
|