@mcp-ts/sdk 2.5.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +3 -574
  2. package/dist/adapters/agui-adapter.d.mts +4 -4
  3. package/dist/adapters/agui-adapter.d.ts +4 -4
  4. package/dist/adapters/agui-middleware.d.mts +4 -4
  5. package/dist/adapters/agui-middleware.d.ts +4 -4
  6. package/dist/adapters/ai-adapter.d.mts +4 -4
  7. package/dist/adapters/ai-adapter.d.ts +4 -4
  8. package/dist/adapters/langchain-adapter.d.mts +4 -4
  9. package/dist/adapters/langchain-adapter.d.ts +4 -4
  10. package/dist/adapters/mastra-adapter.d.mts +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +2 -2
  13. package/dist/client/index.d.ts +2 -2
  14. package/dist/client/index.js +3 -0
  15. package/dist/client/index.js.map +1 -1
  16. package/dist/client/index.mjs +3 -0
  17. package/dist/client/index.mjs.map +1 -1
  18. package/dist/client/react.d.mts +10 -18
  19. package/dist/client/react.d.ts +10 -18
  20. package/dist/client/react.js +17 -0
  21. package/dist/client/react.js.map +1 -1
  22. package/dist/client/react.mjs +17 -0
  23. package/dist/client/react.mjs.map +1 -1
  24. package/dist/client/vue.d.mts +4 -4
  25. package/dist/client/vue.d.ts +4 -4
  26. package/dist/client/vue.js +3 -0
  27. package/dist/client/vue.js.map +1 -1
  28. package/dist/client/vue.mjs +3 -0
  29. package/dist/client/vue.mjs.map +1 -1
  30. package/dist/{index-B3GPsPsi.d.mts → index-BrDkbut5.d.ts} +2 -1
  31. package/dist/{index-gNdSQTSz.d.ts → index-DHQvlx4K.d.mts} +2 -1
  32. package/dist/index.d.mts +5 -5
  33. package/dist/index.d.ts +5 -5
  34. package/dist/index.js +1056 -863
  35. package/dist/index.js.map +1 -1
  36. package/dist/index.mjs +1048 -858
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/{multi-session-client-CB4oDrQX.d.ts → multi-session-client-CQzymvHl.d.ts} +183 -227
  39. package/dist/{multi-session-client-xj3iQIv6.d.mts → multi-session-client-C_Ja2TuV.d.mts} +183 -227
  40. package/dist/server/index.d.mts +338 -88
  41. package/dist/server/index.d.ts +338 -88
  42. package/dist/server/index.js +1054 -864
  43. package/dist/server/index.js.map +1 -1
  44. package/dist/server/index.mjs +1046 -859
  45. package/dist/server/index.mjs.map +1 -1
  46. package/dist/shared/index.d.mts +4 -4
  47. package/dist/shared/index.d.ts +4 -4
  48. package/dist/shared/index.js.map +1 -1
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-CsKVXbQB.d.mts → tool-router-C1n7OXbl.d.mts} +1 -1
  51. package/dist/{tool-router-C-Mw1_BQ.d.ts → tool-router-CBIawFnt.d.ts} +1 -1
  52. package/dist/{types-6SmXege4.d.mts → types-DX71u9gR.d.mts} +11 -3
  53. package/dist/{types-6SmXege4.d.ts → types-DX71u9gR.d.ts} +11 -3
  54. package/migrations/neon/20260513010000_install_mcp_sessions.sql +4 -34
  55. package/migrations/supabase/20260330195700_install_mcp_sessions.sql +5 -62
  56. package/package.json +2 -1
  57. package/src/client/core/sse-client.ts +5 -0
  58. package/src/client/react/use-mcp.ts +27 -28
  59. package/src/server/handlers/sse-handler.ts +671 -629
  60. package/src/server/index.ts +2 -1
  61. package/src/server/mcp/multi-session-client.ts +34 -10
  62. package/src/server/mcp/oauth-client.ts +197 -205
  63. package/src/server/mcp/storage-oauth-provider.ts +149 -29
  64. package/src/server/mcp/tool-policy-gateway.ts +11 -1
  65. package/src/server/storage/file-backend.ts +25 -21
  66. package/src/server/storage/index.ts +71 -0
  67. package/src/server/storage/memory-backend.ts +24 -12
  68. package/src/server/storage/neon-backend.ts +70 -72
  69. package/src/server/storage/redis-backend.ts +26 -36
  70. package/src/server/storage/sqlite-backend.ts +25 -39
  71. package/src/server/storage/supabase-backend.ts +45 -54
  72. package/src/server/storage/types.ts +22 -2
  73. package/src/shared/types.ts +13 -1
@@ -24,16 +24,19 @@ import {
24
24
  ReadResourceRequest,
25
25
  ReadResourceResult,
26
26
  ReadResourceResultSchema,
27
+ type Implementation,
27
28
  } from '@modelcontextprotocol/sdk/types.js';
28
29
  import { StorageOAuthClientProvider, type AgentsOAuthProvider } from './storage-oauth-provider.js';
29
30
  import { Emitter, type McpConnectionEvent, type McpObservabilityEvent, type McpConnectionState } from '../../shared/events.js';
30
31
  import { UnauthorizedError } from '../../shared/errors.js';
31
32
  import { sessions } from '../storage/index.js';
32
- import type { Session, SessionStatus } from '../storage/types.js';
33
+ import type { Session, SessionStatus, SessionStore } from '../storage/types.js';
33
34
  import {
34
35
  MCP_CLIENT_NAME,
35
36
  MCP_CLIENT_VERSION,
36
37
  } from '../../shared/constants.js';
38
+ import type { OAuthTokens } from '@modelcontextprotocol/sdk/shared/auth.js';
39
+ import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js';
37
40
 
38
41
  /**
39
42
  * Supported MCP transport types
@@ -71,6 +74,25 @@ export interface MCPOAuthClientOptions {
71
74
  clientUri?: string;
72
75
  logoUri?: string;
73
76
  policyUri?: string;
77
+ /**
78
+ * Credentials already loaded by the caller (e.g. via get({includeCredentials: true})).
79
+ * When provided, the StorageOAuthClientProvider uses these cached values to
80
+ * skip redundant DB reads for tokens() calls during reconnection.
81
+ */
82
+ cachedCredentials?: { tokens?: OAuthTokens };
83
+ /**
84
+ * When true, skips the redundant session-existence check in ensureSession().
85
+ * Set by callers that have already confirmed the session exists in storage
86
+ * (e.g. getOrCreateClient, getSession, finishAuth in sse-handler).
87
+ * Saves one round-trip per reconnection.
88
+ */
89
+ hasSession?: boolean;
90
+ /**
91
+ * Custom session store override. When provided, all storage operations
92
+ * (get/create/update/delete) use this store instead of the default global.
93
+ * Used for wrapping with DB observability or other decorators.
94
+ */
95
+ sessionStore?: SessionStore;
74
96
  }
75
97
 
76
98
  /**
@@ -80,26 +102,12 @@ export interface MCPOAuthClientOptions {
80
102
  */
81
103
  export class MCPClient {
82
104
  private client: Client;
83
- public oauthProvider: AgentsOAuthProvider | null = null;
105
+ public oauthProvider: OAuthClientProvider | null = null;
84
106
  private transport: StreamableHTTPClientTransport | SSEClientTransport | null = null;
85
- private userId: string;
86
- private serverId?: string;
87
- private sessionId: string;
88
- private serverName?: string;
89
- private transportType: TransportType | undefined;
90
- private serverUrl: string | undefined;
91
- private callbackUrl: string | undefined;
92
- private onRedirect: ((url: string) => void) | undefined;
93
- private clientId?: string;
94
- private clientSecret?: string;
95
- private headers?: Record<string, string>;
96
- /** OAuth Client Metadata */
97
- private clientName?: string;
98
- private clientUri?: string;
99
- private logoUri?: string;
100
- private policyUri?: string;
107
+ private config!: MCPOAuthClientOptions;
101
108
  private createdAt?: number;
102
-
109
+ private _serverInfo: Implementation | undefined;
110
+ private _store!: SessionStore;
103
111
 
104
112
  /** Event emitters for connection lifecycle */
105
113
  private readonly _onConnectionEvent = new Emitter<McpConnectionEvent>();
@@ -116,21 +124,8 @@ export class MCPClient {
116
124
  * @param options - Client configuration options
117
125
  */
118
126
  constructor(options: MCPOAuthClientOptions) {
119
- this.serverUrl = options.serverUrl;
120
- this.serverName = options.serverName;
121
- this.callbackUrl = options.callbackUrl;
122
- this.onRedirect = options.onRedirect;
123
- this.userId = options.userId;
124
- this.serverId = options.serverId;
125
- this.sessionId = options.sessionId;
126
- this.transportType = options.transportType;
127
- this.clientId = options.clientId;
128
- this.clientSecret = options.clientSecret;
129
- this.headers = options.headers;
130
- this.clientName = options.clientName;
131
- this.clientUri = options.clientUri;
132
- this.logoUri = options.logoUri;
133
- this.policyUri = options.policyUri;
127
+ this.config = { ...options };
128
+ this._store = options.sessionStore ?? sessions;
134
129
 
135
130
  this.client = new Client(
136
131
  {
@@ -149,6 +144,22 @@ export class MCPClient {
149
144
  );
150
145
  }
151
146
 
147
+ /** Shared session-shaped data for ensureSession and saveSession */
148
+ private get session() {
149
+ return {
150
+ sessionId: this.config.sessionId,
151
+ userId: this.config.userId,
152
+ serverId: this.config.serverId!,
153
+ serverName: this.config.serverName,
154
+ serverUrl: this.config.serverUrl!,
155
+ callbackUrl: this.config.callbackUrl!,
156
+ transportType: (this.config.transportType || 'streamable-http') as TransportType,
157
+ headers: this.config.headers,
158
+ createdAt: this.createdAt ?? Date.now(),
159
+ updatedAt: Date.now(),
160
+ };
161
+ }
162
+
152
163
  /**
153
164
  * Emit a connection state change event
154
165
  * @private
@@ -157,14 +168,14 @@ export class MCPClient {
157
168
  const previousState = this.currentState;
158
169
  this.currentState = newState;
159
170
 
160
- if (!this.serverId) return;
171
+ if (!this.config.serverId) return;
161
172
 
162
173
  this._onConnectionEvent.fire({
163
174
  type: 'state_changed',
164
- sessionId: this.sessionId,
165
- serverId: this.serverId,
166
- serverName: this.serverName || this.serverId,
167
- serverUrl: this.serverUrl || '',
175
+ sessionId: this.config.sessionId,
176
+ serverId: this.config.serverId,
177
+ serverName: this.config.serverName || this.config.serverId,
178
+ serverUrl: this.config.serverUrl || '',
168
179
  createdAt: this.createdAt,
169
180
  state: newState,
170
181
  previousState,
@@ -176,8 +187,8 @@ export class MCPClient {
176
187
  level: 'info',
177
188
  message: `Connection state: ${previousState} → ${newState}`,
178
189
  displayMessage: `State changed to ${newState}`,
179
- sessionId: this.sessionId,
180
- serverId: this.serverId,
190
+ sessionId: this.config.sessionId,
191
+ serverId: this.config.serverId,
181
192
  payload: { previousState, newState },
182
193
  timestamp: Date.now(),
183
194
  id: nanoid(),
@@ -189,12 +200,12 @@ export class MCPClient {
189
200
  * @private
190
201
  */
191
202
  private emitError(error: string, errorType: 'connection' | 'auth' | 'validation' | 'unknown' = 'unknown'): void {
192
- if (!this.serverId) return;
203
+ if (!this.config.serverId) return;
193
204
 
194
205
  this._onConnectionEvent.fire({
195
206
  type: 'error',
196
- sessionId: this.sessionId,
197
- serverId: this.serverId,
207
+ sessionId: this.config.sessionId,
208
+ serverId: this.config.serverId,
198
209
  error,
199
210
  errorType,
200
211
  timestamp: Date.now(),
@@ -205,8 +216,8 @@ export class MCPClient {
205
216
  level: 'error',
206
217
  message: error,
207
218
  displayMessage: error,
208
- sessionId: this.sessionId,
209
- serverId: this.serverId,
219
+ sessionId: this.config.sessionId,
220
+ serverId: this.config.serverId,
210
221
  payload: { errorType, error },
211
222
  timestamp: Date.now(),
212
223
  id: nanoid(),
@@ -218,12 +229,12 @@ export class MCPClient {
218
229
  * @private
219
230
  */
220
231
  private emitProgress(message: string): void {
221
- if (!this.serverId) return;
232
+ if (!this.config.serverId) return;
222
233
 
223
234
  this._onConnectionEvent.fire({
224
235
  type: 'progress',
225
- sessionId: this.sessionId,
226
- serverId: this.serverId,
236
+ sessionId: this.config.sessionId,
237
+ serverId: this.config.serverId,
227
238
  message,
228
239
  timestamp: Date.now(),
229
240
  });
@@ -243,16 +254,16 @@ export class MCPClient {
243
254
  * @private
244
255
  */
245
256
  private getTransport(type: TransportType): StreamableHTTPClientTransport | SSEClientTransport {
246
- if (!this.serverUrl) {
257
+ if (!this.config.serverUrl) {
247
258
  throw new Error('Server URL is required to create transport');
248
259
  }
249
260
 
250
- const baseUrl = new URL(this.serverUrl);
251
- const hasAuthorizationHeader = Object.keys(this.headers || {})
261
+ const baseUrl = new URL(this.config.serverUrl);
262
+ const hasAuthorizationHeader = Object.keys(this.config.headers || {})
252
263
  .some((key) => key.toLowerCase() === 'authorization');
253
- const transportOptions = {
254
- ...(!hasAuthorizationHeader && { authProvider: this.oauthProvider! }),
255
- ...(this.headers && { requestInit: { headers: this.headers } }),
264
+ const transportOptions: Record<string, any> = {
265
+ ...(!hasAuthorizationHeader && { authProvider: this.oauthProvider }),
266
+ ...(this.config.headers && { requestInit: { headers: this.config.headers } }),
256
267
  /**
257
268
  * Custom fetch implementation to handle connection timeouts.
258
269
  * Observation: SDK 1.24.0+ connections may hang indefinitely in some environments.
@@ -296,96 +307,87 @@ export class MCPClient {
296
307
  * @private
297
308
  */
298
309
  private async ensureSession(): Promise<void> {
299
- if (this.oauthProvider) {
300
- return;
301
- }
310
+ if (this.oauthProvider) return;
302
311
 
303
312
  this.emitStateChange('INITIALIZING');
304
313
  this.emitProgress('Loading session configuration...');
305
314
 
306
- let existingSession: Session | null = null;
307
-
308
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
309
- existingSession = await sessions.get(this.userId, this.sessionId);
315
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
316
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
310
317
  if (!existingSession) {
311
- throw new Error(`Session not found: ${this.sessionId}`);
318
+ throw new Error(`Session not found: ${this.config.sessionId}`);
312
319
  }
313
320
 
314
- this.serverUrl = this.serverUrl || existingSession.serverUrl;
315
- this.callbackUrl = this.callbackUrl || existingSession.callbackUrl;
316
- /**
317
- * Do NOT load transportType from session if not explicitly provided.
318
- * We want to re-negotiate (try streamable -> sse) on new connections if in "Auto" mode.
319
- * this.transportType = this.transportType || sessionData.transportType;
320
- */
321
- this.serverName = this.serverName || existingSession.serverName;
322
- this.serverId = this.serverId || existingSession.serverId || 'unknown';
323
- this.headers = this.headers || existingSession.headers;
321
+ this.config.serverUrl = this.config.serverUrl || existingSession.serverUrl;
322
+ this.config.callbackUrl = this.config.callbackUrl || existingSession.callbackUrl;
323
+ this.config.serverName = this.config.serverName || existingSession.serverName;
324
+ this.config.serverId = this.config.serverId || existingSession.serverId || 'unknown';
325
+ this.config.headers = this.config.headers || existingSession.headers;
324
326
  this.createdAt = existingSession.createdAt;
325
327
  }
326
328
 
327
- if (!this.serverUrl || !this.callbackUrl || !this.serverId) {
329
+ if (!this.config.serverUrl || !this.config.callbackUrl || !this.config.serverId) {
328
330
  throw new Error('Missing required connection metadata');
329
331
  }
330
332
 
331
- if (!this.oauthProvider) {
332
- if (!this.serverId) {
333
- throw new Error('serverId required for OAuth provider initialization');
334
- }
335
- this.oauthProvider = new StorageOAuthClientProvider({
336
- userId: this.userId,
337
- serverId: this.serverId,
338
- sessionId: this.sessionId,
339
- redirectUrl: this.callbackUrl!,
340
- clientName: this.clientName,
341
- clientUri: this.clientUri,
342
- logoUri: this.logoUri,
343
- policyUri: this.policyUri,
344
- clientId: this.clientId,
345
- clientSecret: this.clientSecret,
346
- onRedirect: (redirectUrl: string) => {
347
- if (this.onRedirect) {
348
- this.onRedirect(redirectUrl);
349
- }
333
+ this.oauthProvider = new StorageOAuthClientProvider({
334
+ userId: this.config.userId,
335
+ serverId: this.config.serverId!,
336
+ sessionId: this.config.sessionId,
337
+ redirectUrl: this.config.callbackUrl!,
338
+ clientName: this.config.clientName,
339
+ clientUri: this.config.clientUri,
340
+ logoUri: this.config.logoUri,
341
+ policyUri: this.config.policyUri,
342
+ clientId: this.config.clientId,
343
+ clientSecret: this.config.clientSecret,
344
+ cachedTokens: this.config.cachedCredentials?.tokens,
345
+ sessionStore: this._store,
346
+ onRedirect: (redirectUrl: string) => {
347
+ if (this.config.serverId) {
348
+ this._onConnectionEvent.fire({
349
+ type: 'auth_required',
350
+ sessionId: this.config.sessionId,
351
+ serverId: this.config.serverId,
352
+ authUrl: redirectUrl,
353
+ timestamp: Date.now(),
354
+ });
350
355
  }
351
- });
352
- }
356
+ if (this.config.onRedirect) {
357
+ this.config.onRedirect(redirectUrl);
358
+ }
359
+ },
360
+ });
353
361
 
354
- // Create session in the session store if it doesn't exist yet
355
- // This is needed BEFORE OAuth flow starts because the OAuth provider
356
- // will call saveCodeVerifier() which requires the session to exist
357
- if (existingSession === null) {
358
- existingSession = await sessions.get(this.userId, this.sessionId);
359
- }
360
- if (!existingSession && this.serverId && this.serverUrl && this.callbackUrl) {
362
+ // Create session row BEFORE persisting credentials (FK constraint on mcp_credentials)
363
+ // When hasSession is set by the caller, the session is guaranteed
364
+ // to exist skip the redundant round-trip.
365
+ const existingSession = this.config.hasSession ? {} as Session : await this._store.get(this.config.userId, this.config.sessionId);
366
+ if (!existingSession) {
361
367
  this.createdAt = Date.now();
362
368
  const updatedAt = this.createdAt;
363
369
  this._onObservabilityEvent.fire({
364
370
  type: 'mcp:client:session_created',
365
371
  level: 'info',
366
- message: `Creating pending session ${this.sessionId} for connection setup`,
367
- sessionId: this.sessionId,
368
- serverId: this.serverId,
372
+ message: `Creating pending session ${this.config.sessionId} for connection setup`,
373
+ sessionId: this.config.sessionId,
374
+ serverId: this.config.serverId,
369
375
  timestamp: Date.now(),
370
376
  id: nanoid(),
371
377
  });
372
- await sessions.create({
373
- sessionId: this.sessionId,
374
- userId: this.userId,
375
- serverId: this.serverId,
376
- serverName: this.serverName,
377
- serverUrl: this.serverUrl,
378
- callbackUrl: this.callbackUrl,
379
- transportType: this.transportType || 'streamable-http',
380
- headers: this.headers,
381
- createdAt: this.createdAt,
378
+ await this._store.create({
379
+ ...this.session,
382
380
  updatedAt,
383
381
  status: 'pending',
384
382
  });
385
383
  }
386
- }
387
-
388
384
 
385
+ // Only persist credentials on first connect. Existing sessions already have
386
+ // credentials from the initial connect — no DB read needed to verify.
387
+ if (!existingSession && this.oauthProvider instanceof StorageOAuthClientProvider) {
388
+ await this.oauthProvider.initializeCredentials();
389
+ }
390
+ }
389
391
 
390
392
  /**
391
393
  * Saves current session state to the session store
@@ -393,40 +395,15 @@ export class MCPClient {
393
395
  * @param status - Session lifecycle status used by storage cleanup
394
396
  * @private
395
397
  */
396
- private async saveSession(
397
- status: SessionStatus = 'active',
398
- existingSession?: Session | null
399
- ): Promise<void> {
400
- if (!this.sessionId || !this.serverId || !this.serverUrl || !this.callbackUrl) {
398
+ private async saveSession(status: SessionStatus = 'active'): Promise<void> {
399
+ if (!this.config.sessionId || !this.config.serverId || !this.config.serverUrl || !this.config.callbackUrl) {
401
400
  return;
402
401
  }
403
402
 
404
- const sessionData = {
405
- sessionId: this.sessionId,
406
- userId: this.userId,
407
- serverId: this.serverId,
408
- serverName: this.serverName,
409
- serverUrl: this.serverUrl,
410
- callbackUrl: this.callbackUrl,
411
- transportType: (this.transportType || 'streamable-http') as TransportType,
412
- headers: this.headers,
413
- createdAt: this.createdAt || Date.now(),
414
- updatedAt: Date.now(),
403
+ await this._store.update(this.config.userId, this.config.sessionId, {
404
+ ...this.session,
415
405
  status,
416
- };
417
- if (status === 'active') {
418
- (sessionData as typeof sessionData & { authUrl: null }).authUrl = null;
419
- }
420
-
421
- // Try to update first, create if doesn't exist
422
- if (existingSession === undefined) {
423
- existingSession = await sessions.get(this.userId, this.sessionId);
424
- }
425
- if (existingSession) {
426
- await sessions.update(this.userId, this.sessionId, sessionData);
427
- } else {
428
- await sessions.create(sessionData);
429
- }
406
+ });
430
407
  }
431
408
 
432
409
  /**
@@ -435,7 +412,7 @@ export class MCPClient {
435
412
  */
436
413
  private async deleteTransientSession(): Promise<void> {
437
414
  try {
438
- await sessions.delete(this.userId, this.sessionId);
415
+ await this._store.delete(this.config.userId, this.config.sessionId);
439
416
  } catch {
440
417
  // Best effort only: preserve the original connection/auth error.
441
418
  }
@@ -451,8 +428,8 @@ export class MCPClient {
451
428
  * If exact transport type is known, only try that.
452
429
  * Otherwise (auto mode), try streamable_http first, then sse.
453
430
  */
454
- const transportsToTry: TransportType[] = this.transportType
455
- ? [this.transportType]
431
+ const transportsToTry: TransportType[] = this.config.transportType
432
+ ? [this.config.transportType]
456
433
  : ['streamable-http', 'sse'];
457
434
 
458
435
  let lastError: unknown;
@@ -469,6 +446,9 @@ export class MCPClient {
469
446
  /** Race connection against timeout */
470
447
  await this.client!.connect(transport);
471
448
 
449
+ /** Capture server metadata from the initialize response */
450
+ this._serverInfo = this.client.getServerVersion();
451
+
472
452
  /** Success! Return the type that worked */
473
453
  return { transportType: currentType };
474
454
 
@@ -494,8 +474,8 @@ export class MCPClient {
494
474
  this._onObservabilityEvent.fire({
495
475
  level: 'warn',
496
476
  message: `Transport ${currentType} failed, falling back`,
497
- sessionId: this.sessionId,
498
- serverId: this.serverId,
477
+ sessionId: this.config.sessionId,
478
+ serverId: this.config.serverId,
499
479
  metadata: {
500
480
  failedTransport: currentType,
501
481
  error: errorMessage
@@ -550,7 +530,7 @@ export class MCPClient {
550
530
  const { transportType } = await this.tryConnect();
551
531
 
552
532
  /** Update transport type to the one that actually worked */
553
- this.transportType = transportType;
533
+ this.config.transportType = transportType;
554
534
 
555
535
  this.emitStateChange('CONNECTED');
556
536
  this.emitProgress('Connected successfully');
@@ -560,9 +540,9 @@ export class MCPClient {
560
540
  this._onObservabilityEvent.fire({
561
541
  type: 'mcp:client:session_saved',
562
542
  level: 'info',
563
- message: `Saving active session ${this.sessionId} (connect success)`,
564
- sessionId: this.sessionId,
565
- serverId: this.serverId,
543
+ message: `Saving active session ${this.config.sessionId} (connect success)`,
544
+ sessionId: this.config.sessionId,
545
+ serverId: this.config.serverId,
566
546
  timestamp: Date.now(),
567
547
  id: nanoid(),
568
548
  });
@@ -576,9 +556,8 @@ export class MCPClient {
576
556
  /** Set when the SDK calls redirectToAuthorization on the OAuth provider */
577
557
  let authUrl = '';
578
558
  if (this.oauthProvider) {
579
- authUrl = (this.oauthProvider.authUrl || '').trim();
559
+ authUrl = ((this.oauthProvider as any).authUrl || '').trim();
580
560
  }
581
-
582
561
  /**
583
562
  * 401 without a usable URL means metadata/DCR failed or the server never started
584
563
  * an interactive OAuth flow — not recoverable as "pending OAuth".
@@ -606,25 +585,25 @@ export class MCPClient {
606
585
  this._onObservabilityEvent.fire({
607
586
  type: 'mcp:client:session_saved',
608
587
  level: 'info',
609
- message: `Saving pending OAuth session ${this.sessionId}`,
610
- sessionId: this.sessionId,
611
- serverId: this.serverId,
588
+ message: `Saving pending OAuth session ${this.config.sessionId}`,
589
+ sessionId: this.config.sessionId,
590
+ serverId: this.config.serverId,
612
591
  timestamp: Date.now(),
613
592
  id: nanoid(),
614
593
  });
615
594
  await this.saveSession('pending');
616
595
 
617
- if (this.serverId) {
596
+ if (this.config.serverId) {
618
597
  this._onConnectionEvent.fire({
619
598
  type: 'auth_required',
620
- sessionId: this.sessionId,
621
- serverId: this.serverId,
599
+ sessionId: this.config.sessionId,
600
+ serverId: this.config.serverId,
622
601
  authUrl,
623
602
  timestamp: Date.now(),
624
603
  });
625
604
 
626
- if (authUrl && this.onRedirect) {
627
- this.onRedirect(authUrl);
605
+ if (authUrl && this.config.onRedirect) {
606
+ this.config.onRedirect(authUrl);
628
607
  }
629
608
  }
630
609
 
@@ -639,9 +618,9 @@ export class MCPClient {
639
618
  // Remove transient sessions that failed before becoming restorable.
640
619
  // Existing active sessions may still hold usable credentials for reconnect.
641
620
  try {
642
- const existingSession = await sessions.get(this.userId, this.sessionId);
621
+ const existingSession = await this._store.get(this.config.userId, this.config.sessionId);
643
622
  if (!existingSession || existingSession.status !== 'active') {
644
- await sessions.delete(this.userId, this.sessionId);
623
+ await this._store.delete(this.config.userId, this.config.sessionId);
645
624
  }
646
625
  } catch {
647
626
  // Best effort only: preserve the original connection error.
@@ -673,23 +652,22 @@ export class MCPClient {
673
652
  }
674
653
 
675
654
  if (state) {
676
- const stateCheck = await this.oauthProvider.checkState(state);
677
- if (!stateCheck.valid) {
678
- const error = stateCheck.error || 'Invalid OAuth state';
679
- this.emitError(error, 'auth');
655
+ try {
656
+ await (this.oauthProvider as AgentsOAuthProvider).consumeState(state);
657
+ } catch (error) {
658
+ const msg = error instanceof Error ? error.message : 'Invalid OAuth state';
659
+ this.emitError(msg, 'auth');
680
660
  this.emitStateChange('FAILED');
681
- throw new Error(error);
661
+ throw error;
682
662
  }
683
-
684
- await this.oauthProvider.consumeState(state);
685
663
  }
686
664
 
687
665
  /**
688
666
  * Determine which transports to try for finishing auth
689
667
  * If transportType is set, use only that. Otherwise try streamable_http then sse.
690
668
  */
691
- const transportsToTry: TransportType[] = this.transportType
692
- ? [this.transportType]
669
+ const transportsToTry: TransportType[] = this.config.transportType
670
+ ? [this.config.transportType]
693
671
  : ['streamable-http', 'sse'];
694
672
 
695
673
  let lastError: unknown;
@@ -728,16 +706,19 @@ export class MCPClient {
728
706
 
729
707
  await this.client.connect(this.transport);
730
708
 
709
+ /** Capture server metadata from the initialize response */
710
+ this._serverInfo = this.client.getServerVersion();
711
+
731
712
  /** Connection succeeded — lock in the transport type */
732
- this.transportType = currentType;
713
+ this.config.transportType = currentType;
733
714
 
734
715
  this.emitStateChange('CONNECTED');
735
716
  this._onObservabilityEvent.fire({
736
717
  type: 'mcp:client:session_saved',
737
718
  level: 'info',
738
- message: `Saving active session ${this.sessionId} (OAuth complete)`,
739
- sessionId: this.sessionId,
740
- serverId: this.serverId,
719
+ message: `Saving active session ${this.config.sessionId} (OAuth complete)`,
720
+ sessionId: this.config.sessionId,
721
+ serverId: this.config.serverId,
741
722
  timestamp: Date.now(),
742
723
  id: nanoid(),
743
724
  });
@@ -837,11 +818,11 @@ export class MCPClient {
837
818
  try {
838
819
  const result = await this.fetchTools();
839
820
 
840
- if (this.serverId) {
821
+ if (this.config.serverId) {
841
822
  this._onConnectionEvent.fire({
842
823
  type: 'tools_discovered',
843
- sessionId: this.sessionId,
844
- serverId: this.serverId,
824
+ sessionId: this.config.sessionId,
825
+ serverId: this.config.serverId,
845
826
  toolCount: result.tools.length,
846
827
  tools: result.tools,
847
828
  timestamp: Date.now(),
@@ -885,8 +866,8 @@ export class MCPClient {
885
866
  level: 'info',
886
867
  message: `Tool ${toolName} called successfully`,
887
868
  displayMessage: `Called tool ${toolName}`,
888
- sessionId: this.sessionId,
889
- serverId: this.serverId,
869
+ sessionId: this.config.sessionId,
870
+ serverId: this.config.serverId,
890
871
  payload: {
891
872
  toolName,
892
873
  args: toolArgs,
@@ -904,8 +885,8 @@ export class MCPClient {
904
885
  level: 'error',
905
886
  message: errorMessage,
906
887
  displayMessage: `Failed to call tool ${toolName}`,
907
- sessionId: this.sessionId,
908
- serverId: this.serverId,
888
+ sessionId: this.config.sessionId,
889
+ serverId: this.config.serverId,
909
890
  payload: {
910
891
  errorType: 'tool_execution',
911
892
  error: errorMessage,
@@ -1070,8 +1051,8 @@ export class MCPClient {
1070
1051
  type: 'mcp:client:error',
1071
1052
  level: 'warn',
1072
1053
  message: 'Initialization failed during clearSession',
1073
- sessionId: this.sessionId,
1074
- serverId: this.serverId,
1054
+ sessionId: this.config.sessionId,
1055
+ serverId: this.config.serverId,
1075
1056
  payload: { error: String(error) },
1076
1057
  timestamp: Date.now(),
1077
1058
  id: nanoid(),
@@ -1082,7 +1063,7 @@ export class MCPClient {
1082
1063
  await (this.oauthProvider as any).invalidateCredentials('all');
1083
1064
  }
1084
1065
 
1085
- await sessions.delete(this.userId, this.sessionId);
1066
+ await this._store.delete(this.config.userId, this.config.sessionId);
1086
1067
  await this.disconnect();
1087
1068
  }
1088
1069
 
@@ -1125,20 +1106,20 @@ export class MCPClient {
1125
1106
  this.transport = null;
1126
1107
 
1127
1108
  // Emit disconnected event
1128
- if (this.serverId) {
1109
+ if (this.config.serverId) {
1129
1110
  this._onConnectionEvent.fire({
1130
1111
  type: 'disconnected',
1131
- sessionId: this.sessionId,
1132
- serverId: this.serverId,
1112
+ sessionId: this.config.sessionId,
1113
+ serverId: this.config.serverId,
1133
1114
  timestamp: Date.now(),
1134
1115
  });
1135
1116
 
1136
1117
  this._onObservabilityEvent.fire({
1137
1118
  type: 'mcp:client:disconnect',
1138
1119
  level: 'info',
1139
- message: `Disconnected from ${this.serverId}`,
1140
- sessionId: this.sessionId,
1141
- serverId: this.serverId,
1120
+ message: `Disconnected from ${this.config.serverId}`,
1121
+ sessionId: this.config.sessionId,
1122
+ serverId: this.config.serverId,
1142
1123
  payload: {},
1143
1124
  timestamp: Date.now(),
1144
1125
  id: nanoid(),
@@ -1166,7 +1147,7 @@ export class MCPClient {
1166
1147
  * @returns Server URL or empty string if not set
1167
1148
  */
1168
1149
  getServerUrl(): string {
1169
- return this.serverUrl || '';
1150
+ return this.config.serverUrl || '';
1170
1151
  }
1171
1152
 
1172
1153
  /**
@@ -1174,7 +1155,7 @@ export class MCPClient {
1174
1155
  * @returns Callback URL or empty string if not set
1175
1156
  */
1176
1157
  getCallbackUrl(): string {
1177
- return this.callbackUrl || '';
1158
+ return this.config.callbackUrl || '';
1178
1159
  }
1179
1160
 
1180
1161
  /**
@@ -1182,18 +1163,29 @@ export class MCPClient {
1182
1163
  * @returns Transport type (defaults to 'streamable-http')
1183
1164
  */
1184
1165
  getTransportType(): TransportType {
1185
- return this.transportType || 'streamable-http';
1166
+ return this.config.transportType || 'streamable-http';
1167
+ }
1168
+
1169
+ /**
1170
+ * Gets the full server metadata from the MCP initialize response.
1171
+ * Includes name, version, icons, title, description, and website URL.
1172
+ * Returns undefined if the client hasn't connected yet.
1173
+ */
1174
+ getServerInfo(): Implementation | undefined {
1175
+ return this._serverInfo;
1186
1176
  }
1187
1177
 
1188
1178
  /**
1189
- * Gets the human-readable server name
1179
+ * Gets the human-readable server name.
1180
+ * Prefers the server's reported title/name from the initialize response,
1181
+ * falling back to the name provided at construction or session metadata.
1190
1182
  * @returns Server name or undefined
1191
1183
  */
1192
1184
  getServerName(): string | undefined {
1193
1185
  // Temporarily avoid deriving serverName from serverVersion metadata.
1194
1186
  // const info = (this.client as any)?.getServerVersion();
1195
- // return info?.title ?? info?.name ?? this.serverName;
1196
- return this.serverName;
1187
+ // return info?.title ?? info?.name ?? this.config.serverName;
1188
+ return this.config.serverName;
1197
1189
  }
1198
1190
 
1199
1191
  /**
@@ -1201,7 +1193,7 @@ export class MCPClient {
1201
1193
  * @returns Server ID or undefined
1202
1194
  */
1203
1195
  getServerId(): string | undefined {
1204
- return this.serverId;
1196
+ return this.config.serverId;
1205
1197
  }
1206
1198
 
1207
1199
  /**
@@ -1209,6 +1201,6 @@ export class MCPClient {
1209
1201
  * @returns Session ID
1210
1202
  */
1211
1203
  getSessionId(): string {
1212
- return this.sessionId;
1204
+ return this.config.sessionId;
1213
1205
  }
1214
1206
  }