@mcp-ts/sdk 2.5.0 → 2.5.2

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 (63) hide show
  1. package/dist/adapters/agui-adapter.d.mts +3 -3
  2. package/dist/adapters/agui-adapter.d.ts +3 -3
  3. package/dist/adapters/agui-middleware.d.mts +3 -3
  4. package/dist/adapters/agui-middleware.d.ts +3 -3
  5. package/dist/adapters/ai-adapter.d.mts +3 -3
  6. package/dist/adapters/ai-adapter.d.ts +3 -3
  7. package/dist/adapters/langchain-adapter.d.mts +3 -3
  8. package/dist/adapters/langchain-adapter.d.ts +3 -3
  9. package/dist/adapters/mastra-adapter.d.mts +2 -2
  10. package/dist/adapters/mastra-adapter.d.ts +2 -2
  11. package/dist/client/index.d.mts +2 -2
  12. package/dist/client/index.d.ts +2 -2
  13. package/dist/client/index.js +3 -0
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/client/index.mjs +3 -0
  16. package/dist/client/index.mjs.map +1 -1
  17. package/dist/client/react.d.mts +4 -4
  18. package/dist/client/react.d.ts +4 -4
  19. package/dist/client/react.js +23 -38
  20. package/dist/client/react.js.map +1 -1
  21. package/dist/client/react.mjs +23 -38
  22. package/dist/client/react.mjs.map +1 -1
  23. package/dist/client/vue.d.mts +5 -4
  24. package/dist/client/vue.d.ts +5 -4
  25. package/dist/client/vue.js +4 -1
  26. package/dist/client/vue.js.map +1 -1
  27. package/dist/client/vue.mjs +4 -1
  28. package/dist/client/vue.mjs.map +1 -1
  29. package/dist/{index-CTURVnom.d.mts → index-B3GPsPsi.d.mts} +2 -1
  30. package/dist/{index-sVcqrhf7.d.ts → index-gNdSQTSz.d.ts} +2 -1
  31. package/dist/index.d.mts +4 -4
  32. package/dist/index.d.ts +4 -4
  33. package/dist/index.js +214 -13
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +214 -13
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/{multi-session-client-CSPSHHla.d.ts → multi-session-client-CB4oDrQX.d.ts} +36 -10
  38. package/dist/{multi-session-client-DMZGVABI.d.mts → multi-session-client-xj3iQIv6.d.mts} +36 -10
  39. package/dist/server/index.d.mts +39 -7
  40. package/dist/server/index.d.ts +39 -7
  41. package/dist/server/index.js +211 -13
  42. package/dist/server/index.js.map +1 -1
  43. package/dist/server/index.mjs +211 -13
  44. package/dist/server/index.mjs.map +1 -1
  45. package/dist/shared/index.d.mts +4 -4
  46. package/dist/shared/index.d.ts +4 -4
  47. package/dist/shared/index.js.map +1 -1
  48. package/dist/shared/index.mjs.map +1 -1
  49. package/dist/{tool-router-CS9l0w4a.d.ts → tool-router-C-Mw1_BQ.d.ts} +1 -1
  50. package/dist/{tool-router-CVLBaCwH.d.mts → tool-router-CsKVXbQB.d.mts} +1 -1
  51. package/dist/{types-DK_NGWd4.d.mts → types-6SmXege4.d.mts} +4 -2
  52. package/dist/{types-DK_NGWd4.d.ts → types-6SmXege4.d.ts} +4 -2
  53. package/package.json +2 -2
  54. package/src/client/core/sse-client.ts +4 -0
  55. package/src/client/react/oauth-popup.tsx +17 -24
  56. package/src/client/react/use-mcp.ts +7 -16
  57. package/src/client/vue/use-mcp.ts +2 -1
  58. package/src/server/handlers/sse-handler.ts +121 -3
  59. package/src/server/mcp/oauth-client.ts +44 -20
  60. package/src/server/mcp/tool-policy-gateway.ts +98 -2
  61. package/src/server/storage/tool-policy.ts +102 -0
  62. package/src/shared/events.ts +1 -0
  63. package/src/shared/types.ts +4 -0
@@ -1,5 +1,5 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
- import { A as ToolClientProvider, T as ToolClient } from './types-DK_NGWd4.js';
2
+ import { A as ToolClientProvider, T as ToolClient } from './types-6SmXege4.js';
3
3
 
4
4
  /**
5
5
  * ToolIndex — Lightweight in-memory search index for MCP tool discovery.
@@ -1,5 +1,5 @@
1
1
  import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
- import { A as ToolClientProvider, T as ToolClient } from './types-DK_NGWd4.mjs';
2
+ import { A as ToolClientProvider, T as ToolClient } from './types-6SmXege4.mjs';
3
3
 
4
4
  /**
5
5
  * ToolIndex — Lightweight in-memory search index for MCP tool discovery.
@@ -57,6 +57,7 @@ type McpConnectionEvent = {
57
57
  serverId: string;
58
58
  toolCount: number;
59
59
  tools: any[];
60
+ allTools?: any[];
60
61
  timestamp: number;
61
62
  } | {
62
63
  type: 'auth_required';
@@ -210,7 +211,7 @@ interface ToolPolicy {
210
211
  toolIds: string[];
211
212
  updatedAt: number;
212
213
  }
213
- type McpRpcMethod = 'connect' | 'disconnect' | 'listTools' | 'callTool' | 'listSessions' | 'getSession' | 'finishAuth' | 'listPrompts' | 'getPrompt' | 'listResources' | 'readResource' | 'setToolPolicy' | 'getToolPolicy';
214
+ type McpRpcMethod = 'connect' | 'disconnect' | 'reconnect' | 'listTools' | 'callTool' | 'listSessions' | 'getSession' | 'finishAuth' | 'listPrompts' | 'getPrompt' | 'listResources' | 'readResource' | 'setToolPolicy' | 'getToolPolicy';
214
215
  interface McpRpcRequest {
215
216
  id: string;
216
217
  method: McpRpcMethod;
@@ -235,6 +236,7 @@ interface ConnectParams {
235
236
  interface DisconnectParams {
236
237
  sessionId: string;
237
238
  }
239
+ type ReconnectParams = ConnectParams;
238
240
  interface SessionParams {
239
241
  sessionId: string;
240
242
  }
@@ -266,7 +268,7 @@ interface SetToolPolicyParams {
266
268
  interface GetToolPolicyParams {
267
269
  sessionId: string;
268
270
  }
269
- type McpRpcParams = ConnectParams | DisconnectParams | SessionParams | CallToolParams | GetPromptParams | ReadResourceParams | FinishAuthParams | SetToolPolicyParams | GetToolPolicyParams | undefined;
271
+ type McpRpcParams = ConnectParams | DisconnectParams | ReconnectParams | SessionParams | CallToolParams | GetPromptParams | ReadResourceParams | FinishAuthParams | SetToolPolicyParams | GetToolPolicyParams | undefined;
270
272
  interface SessionInfo {
271
273
  sessionId: string;
272
274
  serverId?: string;
@@ -57,6 +57,7 @@ type McpConnectionEvent = {
57
57
  serverId: string;
58
58
  toolCount: number;
59
59
  tools: any[];
60
+ allTools?: any[];
60
61
  timestamp: number;
61
62
  } | {
62
63
  type: 'auth_required';
@@ -210,7 +211,7 @@ interface ToolPolicy {
210
211
  toolIds: string[];
211
212
  updatedAt: number;
212
213
  }
213
- type McpRpcMethod = 'connect' | 'disconnect' | 'listTools' | 'callTool' | 'listSessions' | 'getSession' | 'finishAuth' | 'listPrompts' | 'getPrompt' | 'listResources' | 'readResource' | 'setToolPolicy' | 'getToolPolicy';
214
+ type McpRpcMethod = 'connect' | 'disconnect' | 'reconnect' | 'listTools' | 'callTool' | 'listSessions' | 'getSession' | 'finishAuth' | 'listPrompts' | 'getPrompt' | 'listResources' | 'readResource' | 'setToolPolicy' | 'getToolPolicy';
214
215
  interface McpRpcRequest {
215
216
  id: string;
216
217
  method: McpRpcMethod;
@@ -235,6 +236,7 @@ interface ConnectParams {
235
236
  interface DisconnectParams {
236
237
  sessionId: string;
237
238
  }
239
+ type ReconnectParams = ConnectParams;
238
240
  interface SessionParams {
239
241
  sessionId: string;
240
242
  }
@@ -266,7 +268,7 @@ interface SetToolPolicyParams {
266
268
  interface GetToolPolicyParams {
267
269
  sessionId: string;
268
270
  }
269
- type McpRpcParams = ConnectParams | DisconnectParams | SessionParams | CallToolParams | GetPromptParams | ReadResourceParams | FinishAuthParams | SetToolPolicyParams | GetToolPolicyParams | undefined;
271
+ type McpRpcParams = ConnectParams | DisconnectParams | ReconnectParams | SessionParams | CallToolParams | GetPromptParams | ReadResourceParams | FinishAuthParams | SetToolPolicyParams | GetToolPolicyParams | undefined;
270
272
  interface SessionInfo {
271
273
  sessionId: string;
272
274
  serverId?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-ts/sdk",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -206,4 +206,4 @@
206
206
  "engines": {
207
207
  "node": ">=18.0.0"
208
208
  }
209
- }
209
+ }
@@ -102,6 +102,10 @@ export class SSEClient {
102
102
  return this.sendRequest<DisconnectResult>('disconnect', { sessionId });
103
103
  }
104
104
 
105
+ async reconnectToServer(params: ConnectParams): Promise<ConnectResult> {
106
+ return this.sendRequest<ConnectResult>('reconnect', params);
107
+ }
108
+
105
109
  async setToolPolicy(
106
110
  sessionId: string,
107
111
  toolPolicy: Pick<ToolPolicy, 'mode'> & { toolIds?: string[] }
@@ -135,7 +135,6 @@ export function createOAuthPopupRedirectHandler(
135
135
  openCenteredPopup(url, {
136
136
  ...options,
137
137
  onBlocked: options.onBlocked ?? ((blockedUrl) => {
138
- window.alert('Popup blocked! Allow popups for this site to complete authentication.');
139
138
  window.location.href = blockedUrl;
140
139
  }),
141
140
  });
@@ -179,30 +178,26 @@ export function useMcpOAuthPopup<TConnection extends OAuthPopupConnectionLike>(
179
178
  : '';
180
179
  const targetSessionId = rawState ? parseOAuthState(rawState)?.sessionId || rawState : '';
181
180
 
182
- if (popupWindow && targetSessionId) {
181
+ if (targetSessionId) {
183
182
  pendingPopupsRef.current.set(targetSessionId, { popupWindow, state: rawState });
184
183
  }
185
184
 
186
185
  if (!targetSessionId) {
187
- if (popupWindow) {
188
- postPopupResult(popupWindow, {
189
- success: false,
190
- error: 'Missing OAuth session identifier',
191
- });
192
- }
186
+ postPopupResult(popupWindow, {
187
+ success: false,
188
+ error: 'Missing OAuth session identifier',
189
+ });
193
190
  return;
194
191
  }
195
192
 
196
193
  const targetSession = connections.find((connection) => connection.sessionId === targetSessionId);
197
194
  if (!targetSession) {
198
- if (popupWindow) {
199
- postPopupResult(popupWindow, {
200
- sessionId: targetSessionId,
201
- state: rawState,
202
- success: false,
203
- error: 'OAuth session not found in the current client state',
204
- });
205
- }
195
+ postPopupResult(popupWindow, {
196
+ sessionId: targetSessionId,
197
+ state: rawState,
198
+ success: false,
199
+ error: 'OAuth session not found in the current client state',
200
+ });
206
201
  return;
207
202
  }
208
203
 
@@ -217,14 +212,12 @@ export function useMcpOAuthPopup<TConnection extends OAuthPopupConnectionLike>(
217
212
  } catch (error) {
218
213
  processingCodesRef.current.delete(codeKey);
219
214
  pendingPopupsRef.current.delete(targetSession.sessionId);
220
- if (popupWindow) {
221
- postPopupResult(popupWindow, {
222
- sessionId: rawState,
223
- state: rawState,
224
- success: false,
225
- error: error instanceof Error ? error.message : 'Failed to finish auth',
226
- });
227
- }
215
+ postPopupResult(popupWindow, {
216
+ sessionId: rawState,
217
+ state: rawState,
218
+ success: false,
219
+ error: error instanceof Error ? error.message : 'Failed to finish auth',
220
+ });
228
221
  }
229
222
  };
230
223
 
@@ -371,7 +371,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
371
371
  }
372
372
 
373
373
  return prev.map((c: McpConnection) =>
374
- c.sessionId === event.sessionId ? { ...c, tools: event.tools, state: 'READY', updatedAt: new Date() } : c
374
+ c.sessionId === event.sessionId ? { ...c, tools: event.tools, allTools: (event as any).allTools, state: 'READY', updatedAt: new Date() } : c
375
375
  );
376
376
  }
377
377
 
@@ -516,24 +516,15 @@ export function useMcp(options: UseMcpOptions): McpClient {
516
516
  throw new Error('SSE client not initialized');
517
517
  }
518
518
 
519
- // Find and disconnect existing session for the same server
520
- const existing = connections.find(
521
- (c: McpConnection) => c.serverId === params.serverId || c.serverUrl === params.serverUrl
522
- );
523
- if (existing) {
524
- await clientRef.current.disconnectFromServer(existing.sessionId);
525
- if (isMountedRef.current) {
526
- setConnections((prev: McpConnection[]) =>
527
- prev.filter((c: McpConnection) => c.sessionId !== existing.sessionId)
528
- );
529
- }
530
- }
519
+ const result = await clientRef.current.reconnectToServer(params);
520
+
521
+ // The server emits connection events for the new session via SSE,
522
+ // so local state is kept in sync automatically. No manual removal
523
+ // of the old session or insertion of the new one needed here.
531
524
 
532
- // Connect fresh
533
- const result = await clientRef.current.connectToServer(params);
534
525
  return result.sessionId;
535
526
  },
536
- [connections]
527
+ []
537
528
  );
538
529
 
539
530
  /**
@@ -84,6 +84,7 @@ export interface McpConnection {
84
84
  transport?: string;
85
85
  state: McpConnectionState;
86
86
  tools: ToolInfo[];
87
+ allTools?: ToolInfo[];
87
88
  authUrl?: string;
88
89
  error?: string;
89
90
  createdAt?: Date;
@@ -294,7 +295,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
294
295
  case 'tools_discovered': {
295
296
  const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
296
297
  if (index !== -1) {
297
- connections.value[index] = { ...connections.value[index], tools: event.tools, state: 'READY', updatedAt: new Date() };
298
+ connections.value[index] = { ...connections.value[index], tools: event.tools, allTools: (event as any).allTools, state: 'READY', updatedAt: new Date() };
298
299
  }
299
300
  break;
300
301
  }
@@ -18,6 +18,7 @@ import type {
18
18
  McpRpcResponse,
19
19
  ConnectParams,
20
20
  DisconnectParams,
21
+ ReconnectParams,
21
22
  SessionParams,
22
23
  CallToolParams,
23
24
  GetPromptParams,
@@ -166,6 +167,10 @@ export class SSEConnectionManager {
166
167
  result = await this.connect(request.params as ConnectParams);
167
168
  break;
168
169
 
170
+ case 'reconnect':
171
+ result = await this.reconnect(request.params as ReconnectParams);
172
+ break;
173
+
169
174
  case 'disconnect':
170
175
  result = await this.disconnect(request.params as DisconnectParams);
171
176
  break;
@@ -363,6 +368,87 @@ export class SSEConnectionManager {
363
368
  }
364
369
  }
365
370
 
371
+ /**
372
+ * Reconnect to an MCP server — tears down the active client transport/connection
373
+ * and creates a fresh connection while reusing the existing session credentials in a single RPC call.
374
+ */
375
+ private async reconnect(params: ReconnectParams): Promise<ConnectResult> {
376
+ const { serverId: rawServerId, serverName, serverUrl, callbackUrl, transportType } = params;
377
+ const headers = normalizeHeaders(params.headers);
378
+
379
+ // Normalize serverId the same way connect() does
380
+ const serverId = rawServerId && rawServerId.length <= 12
381
+ ? rawServerId
382
+ : generateServerId();
383
+
384
+ // Find existing session for the same server to reuse its session ID
385
+ const existingSessions = await sessions.list(this.userId);
386
+ const duplicate = existingSessions.find(s =>
387
+ s.serverId === serverId || s.serverUrl === serverUrl
388
+ );
389
+
390
+ // Reuse the duplicate sessionId if present, otherwise generate a new one
391
+ const sessionId = duplicate ? duplicate.sessionId : await sessions.generateSessionId();
392
+
393
+ if (duplicate) {
394
+ // Disconnect any active in-memory client transport without deleting the database session
395
+ const existingClient = this.clients.get(duplicate.sessionId);
396
+ if (existingClient) {
397
+ await existingClient.disconnect();
398
+ this.clients.delete(duplicate.sessionId);
399
+ }
400
+ }
401
+
402
+ try {
403
+ const clientMetadata = await this.getResolvedClientMetadata();
404
+
405
+ // Create a new client instantiating the reused session ID (which preserves DCR credentials and tokens)
406
+ const client = new MCPClient({
407
+ userId: this.userId,
408
+ sessionId,
409
+ serverId,
410
+ serverName,
411
+ serverUrl,
412
+ callbackUrl,
413
+ transportType,
414
+ headers,
415
+ ...clientMetadata,
416
+ });
417
+
418
+ this.clients.set(sessionId, client);
419
+
420
+ client.onConnectionEvent((event) => {
421
+ this.emitConnectionEvent(event);
422
+ });
423
+
424
+ client.onObservabilityEvent((event) => {
425
+ this.sendEvent(event);
426
+ });
427
+
428
+ await client.connect();
429
+ await this.listPolicyFilteredTools(sessionId);
430
+
431
+ return { sessionId, success: true };
432
+ } catch (error) {
433
+ if (error instanceof UnauthorizedError) {
434
+ this.clients.delete(sessionId);
435
+ return { sessionId, success: true };
436
+ }
437
+
438
+ this.emitConnectionEvent({
439
+ type: 'error',
440
+ sessionId,
441
+ serverId,
442
+ error: error instanceof Error ? error.message : 'Connection failed',
443
+ errorType: 'connection',
444
+ timestamp: Date.now(),
445
+ });
446
+
447
+ this.clients.delete(sessionId);
448
+ throw error;
449
+ }
450
+ }
451
+
366
452
  /**
367
453
  * Disconnect from an MCP server
368
454
  */
@@ -422,6 +508,22 @@ export class SSEConnectionManager {
422
508
  return client;
423
509
  }
424
510
 
511
+ /**
512
+ * Fetches all tools from the remote MCP server and emits a `tools_discovered` event.
513
+ *
514
+ * Two lists are always published together:
515
+ * - `tools` — policy-filtered list that agents are allowed to call.
516
+ * - `allTools` — the complete, unfiltered list used by the management UI so
517
+ * that blocked tools still appear as checkboxes in the dialog.
518
+ *
519
+ * `fetchTools()` is called first (populates the in-memory cache), then
520
+ * `gateway.listTools()` re-uses that cache internally — so only one remote
521
+ * network round-trip is made regardless of how many callers follow.
522
+ *
523
+ * @param sessionId - The session whose tools should be discovered.
524
+ * @returns The session record and the policy-filtered tool list.
525
+ * @throws {Error} When the session does not exist in the store.
526
+ */
425
527
  private async listPolicyFilteredTools(sessionId: string): Promise<{ session: Session; result: ListToolsRpcResult }> {
426
528
  const session = await sessions.get(this.userId, sessionId);
427
529
  if (!session) {
@@ -429,6 +531,7 @@ export class SSEConnectionManager {
429
531
  }
430
532
 
431
533
  const client = await this.getOrCreateClient(sessionId);
534
+ const allTools = await client.fetchTools().catch(() => ({ tools: [] }));
432
535
  const gateway = createToolPolicyGateway(this.userId, sessionId, client);
433
536
  const result = await gateway.listTools();
434
537
 
@@ -438,13 +541,17 @@ export class SSEConnectionManager {
438
541
  serverId: session.serverId ?? 'unknown',
439
542
  toolCount: result.tools.length,
440
543
  tools: result.tools,
544
+ allTools: allTools.tools,
441
545
  timestamp: Date.now(),
442
546
  });
443
547
 
444
548
  return { session, result };
445
549
  }
550
+
446
551
  /**
447
- * List tools from a session
552
+ * Returns the policy-filtered tool list for a session (agent-facing).
553
+ * Internally re-uses `listPolicyFilteredTools` which also emits a
554
+ * `tools_discovered` SSE event to keep client state up to date.
448
555
  */
449
556
  private async listTools(params: SessionParams): Promise<ListToolsRpcResult> {
450
557
  const { sessionId } = params;
@@ -485,7 +592,15 @@ export class SSEConnectionManager {
485
592
  }
486
593
 
487
594
  /**
488
- * Update per-session tool access policy.
595
+ * Persists a new tool access policy for a session and broadcasts the updated
596
+ * filtered tool list to all connected browser clients via a `tools_discovered` event.
597
+ *
598
+ * Both `tools` (policy-filtered) and `allTools` (complete list) are emitted
599
+ * so the management UI can immediately reflect the new checkbox states without
600
+ * an additional round-trip to the server.
601
+ *
602
+ * @param params - Session ID and the new `{ mode, toolIds }` policy to apply.
603
+ * @throws {Error} When the session does not exist or the policy references unknown tool IDs.
489
604
  */
490
605
  private async setToolPolicy(params: SetToolPolicyParams): Promise<SetToolPolicyResult> {
491
606
  const { sessionId } = params;
@@ -508,6 +623,7 @@ export class SSEConnectionManager {
508
623
  serverId: session.serverId ?? 'unknown',
509
624
  toolCount: filteredTools.length,
510
625
  tools: filteredTools,
626
+ allTools: allTools.tools,
511
627
  timestamp: Date.now(),
512
628
  });
513
629
 
@@ -518,8 +634,10 @@ export class SSEConnectionManager {
518
634
  toolCount: filteredTools.length,
519
635
  };
520
636
  }
637
+
521
638
  /**
522
- * Call a tool on the MCP server
639
+ * Proxies a tool invocation to the remote MCP server.
640
+ * Resolves the client for the given session and delegates to the tool router.
523
641
  */
524
642
  private async callTool(params: CallToolParams): Promise<CallToolResult> {
525
643
  const { sessionId, toolName, toolArgs } = params;
@@ -509,13 +509,20 @@ export class MCPClient {
509
509
  }
510
510
 
511
511
  /**
512
- * Connects to the MCP server
513
- * Automatically validates and refreshes OAuth tokens if needed
514
- * Saves session to Redis on first successful connection
515
- * @throws {UnauthorizedError} When OAuth authorization is required
516
- * @throws {Error} When connection fails for other reasons
512
+ * Connects to the MCP server.
513
+ *
514
+ * Automatically validates and refreshes OAuth tokens if needed.
515
+ * Saves the session to Redis on first successful connection.
516
+ *
517
+ * The in-memory tools cache (`cachedTools`) is cleared at the start of every
518
+ * call so that a reconnection always fetches a fresh tool list from the remote
519
+ * server — even if the same `MCPClient` instance is reused.
520
+ *
521
+ * @throws {UnauthorizedError} When OAuth authorization is required.
522
+ * @throws {Error} When connection fails for other reasons.
517
523
  */
518
524
  async connect(): Promise<void> {
525
+ this.cachedTools = null;
519
526
  // Close any existing transport so we can negotiate a fresh session.
520
527
  // The SDK Client throws if asked to connect() while a transport is
521
528
  // already attached; close() detaches it cleanly so the same Client
@@ -782,18 +789,41 @@ export class MCPClient {
782
789
  }
783
790
 
784
791
  /**
785
- * Lists all available tools from the connected MCP server without emitting discovery events.
786
- * Gateways use this to apply policy before publishing tools to agents or UI state.
792
+ * In-memory cache for the remote server's full tools list.
793
+ *
794
+ * Populated on the first `fetchTools()` call and reused for the lifetime of
795
+ * the connection. Cleared to `null` at the start of `connect()` so that a
796
+ * reconnect always retrieves a fresh list, and also in `dispose()` to release
797
+ * the memory when the client is no longer needed.
798
+ */
799
+ private cachedTools: ListToolsResult | null = null;
800
+
801
+ /**
802
+ * Lists all available tools from the connected MCP server without emitting
803
+ * discovery events. The result is cached in memory for the lifetime of the
804
+ * connection — subsequent callers (e.g. `gateway.listTools()` running right
805
+ * after `fetchTools()`) pay zero extra network cost.
806
+ *
807
+ * Gateways use this to apply a tool-access policy before publishing the
808
+ * filtered list to agents or UI state.
809
+ *
810
+ * @returns The full `ListToolsResult` from the remote server.
811
+ * @throws {Error} When the client is not connected or the request times out.
787
812
  */
788
813
  async fetchTools(): Promise<ListToolsResult> {
814
+ if (this.cachedTools) {
815
+ return this.cachedTools;
816
+ }
789
817
  const request: ListToolsRequest = {
790
818
  method: 'tools/list',
791
819
  params: {},
792
820
  };
793
821
 
794
- return await this.withRetry(() =>
822
+ const result = await this.withRetry(() =>
795
823
  this.client!.request(request, ListToolsResultSchema)
796
824
  );
825
+ this.cachedTools = result;
826
+ return result;
797
827
  }
798
828
 
799
829
  /**
@@ -1119,10 +1149,14 @@ export class MCPClient {
1119
1149
  }
1120
1150
 
1121
1151
  /**
1122
- * Dispose of all event emitters
1123
- * Call this when the client is no longer needed
1152
+ * Disposes all event emitters and releases cached state.
1153
+ *
1154
+ * Clears `cachedTools` to free memory, and disposes the connection and
1155
+ * observability event emitters so downstream listeners are unsubscribed.
1156
+ * Call this when the client is permanently shut down (not just disconnected).
1124
1157
  */
1125
1158
  dispose(): void {
1159
+ this.cachedTools = null;
1126
1160
  this._onConnectionEvent.dispose();
1127
1161
  this._onObservabilityEvent.dispose();
1128
1162
  }
@@ -1177,14 +1211,4 @@ export class MCPClient {
1177
1211
  getSessionId(): string {
1178
1212
  return this.sessionId;
1179
1213
  }
1180
-
1181
1214
  }
1182
-
1183
-
1184
-
1185
-
1186
-
1187
-
1188
-
1189
-
1190
-