@mcp-ts/sdk 1.4.0 → 1.5.1

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 (80) hide show
  1. package/README.md +20 -27
  2. package/dist/adapters/agui-adapter.d.mts +16 -0
  3. package/dist/adapters/agui-adapter.d.ts +16 -0
  4. package/dist/adapters/agui-adapter.js +185 -0
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +185 -0
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +2 -0
  9. package/dist/adapters/agui-middleware.d.ts +2 -0
  10. package/dist/adapters/agui-middleware.js.map +1 -1
  11. package/dist/adapters/agui-middleware.mjs.map +1 -1
  12. package/dist/adapters/ai-adapter.d.mts +21 -0
  13. package/dist/adapters/ai-adapter.d.ts +21 -0
  14. package/dist/adapters/ai-adapter.js +175 -0
  15. package/dist/adapters/ai-adapter.js.map +1 -1
  16. package/dist/adapters/ai-adapter.mjs +175 -0
  17. package/dist/adapters/ai-adapter.mjs.map +1 -1
  18. package/dist/adapters/langchain-adapter.d.mts +16 -0
  19. package/dist/adapters/langchain-adapter.d.ts +16 -0
  20. package/dist/adapters/langchain-adapter.js +179 -0
  21. package/dist/adapters/langchain-adapter.js.map +1 -1
  22. package/dist/adapters/langchain-adapter.mjs +179 -0
  23. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  24. package/dist/client/index.d.mts +2 -2
  25. package/dist/client/index.d.ts +2 -2
  26. package/dist/client/react.d.mts +94 -8
  27. package/dist/client/react.d.ts +94 -8
  28. package/dist/client/react.js +364 -26
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +358 -27
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +4 -4
  33. package/dist/client/vue.d.ts +4 -4
  34. package/dist/client/vue.js +11 -2
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +11 -2
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{index-CQr9q0bF.d.mts → index-DcYfpY3H.d.mts} +1 -1
  39. package/dist/{index-nE_7Io0I.d.ts → index-GfC_eNEv.d.ts} +1 -1
  40. package/dist/index.d.mts +4 -3
  41. package/dist/index.d.ts +4 -3
  42. package/dist/index.js +938 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +923 -13
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/server/index.d.mts +2 -2
  47. package/dist/server/index.d.ts +2 -2
  48. package/dist/server/index.js +58 -12
  49. package/dist/server/index.js.map +1 -1
  50. package/dist/server/index.mjs +58 -12
  51. package/dist/server/index.mjs.map +1 -1
  52. package/dist/shared/index.d.mts +86 -4
  53. package/dist/shared/index.d.ts +86 -4
  54. package/dist/shared/index.js +874 -0
  55. package/dist/shared/index.js.map +1 -1
  56. package/dist/shared/index.mjs +865 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
  59. package/dist/tool-router-XnWVxPzv.d.mts +325 -0
  60. package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
  61. package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
  62. package/package.json +3 -2
  63. package/src/adapters/agui-adapter.ts +79 -0
  64. package/src/adapters/ai-adapter.ts +75 -0
  65. package/src/adapters/langchain-adapter.ts +74 -0
  66. package/src/client/react/index.ts +16 -0
  67. package/src/client/react/oauth-popup.tsx +446 -0
  68. package/src/client/react/use-mcp-apps.tsx +50 -32
  69. package/src/client/react/use-mcp.ts +36 -3
  70. package/src/client/vue/use-mcp.ts +38 -3
  71. package/src/server/handlers/sse-handler.ts +39 -0
  72. package/src/server/index.ts +2 -0
  73. package/src/server/mcp/oauth-client.ts +35 -15
  74. package/src/shared/index.ts +36 -0
  75. package/src/shared/meta-tools.ts +387 -0
  76. package/src/shared/schema-compressor.ts +124 -0
  77. package/src/shared/tool-index.ts +499 -0
  78. package/src/shared/tool-router.ts +469 -0
  79. package/src/shared/types.ts +30 -0
  80. package/supabase/migrations/20260421010000_add_session_cleanup_cron.sql +32 -0
@@ -15,7 +15,9 @@ import React, {
15
15
  useImperativeHandle,
16
16
  type MutableRefObject,
17
17
  } from 'react';
18
- import { useAppHost, type UseAppHostOptions } from './use-app-host.js';
18
+ import type { UseAppHostOptions } from './use-app-host.js';
19
+ import { useAppHost } from './use-app-host.js';
20
+ import { resolveMetaToolProxy } from '../../shared/meta-tools.js';
19
21
  import type { SSEClient } from '../core/sse-client.js';
20
22
  import { APP_HOST_DEFAULTS } from '../core/constants.js';
21
23
  import type { SandboxConfig } from '../core/app-host.js';
@@ -56,9 +58,10 @@ export interface McpAppRendererHandle {
56
58
  /** Props for {@link useMcpApps}'s `McpAppRenderer` (client is supplied via the hook). */
57
59
  export interface McpAppRendererProps extends Pick<UseAppHostOptions, 'sandbox' | 'hostContext' | 'onCallTool' | 'onReadResource' | 'onFallbackRequest' | 'onMessage' | 'onOpenLink' | 'onLoggingMessage' | 'onSizeChanged' | 'onError'> {
58
60
  name: string;
61
+ client?: McpClient | null;
59
62
  toolResourceUri?: string;
60
63
  html?: string;
61
- input?: Record<string, unknown>;
64
+ input?: Record<string, unknown> | null;
62
65
  result?: unknown;
63
66
  status?: 'executing' | 'inProgress' | 'complete' | 'idle';
64
67
  toolInputPartial?: any;
@@ -102,8 +105,10 @@ const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(functi
102
105
  },
103
106
  ref,
104
107
  ) {
108
+
105
109
  const mcpClient = clientRef.current;
106
- const metadata = getMcpAppMetadata(mcpClient, name);
110
+ const { toolName: resolvedToolName, args: resolvedInput } = resolveMetaToolProxy(name, input);
111
+ const metadata = getMcpAppMetadata(mcpClient, resolvedToolName, resolvedInput);
107
112
  const sseClient = mcpClient?.sseClient ?? null;
108
113
  const resourceUri = toolResourceUri || metadata?.resourceUri;
109
114
  const appSessionId = metadata?.sessionId;
@@ -194,7 +199,7 @@ const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(functi
194
199
 
195
200
  const sentInputRef = useRef(false);
196
201
  const sentResultRef = useRef(false);
197
- const lastInputRef = useRef(input);
202
+ const lastInputRef = useRef(resolvedInput);
198
203
  const lastResultRef = useRef(result);
199
204
  const lastStatusRef = useRef(status);
200
205
 
@@ -236,15 +241,16 @@ const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(functi
236
241
  .catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
237
242
  }, [host, resourceUri, html, appSessionId]);
238
243
 
244
+ // Send tool inputs
239
245
  useEffect(() => {
240
- if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
246
+ if (!host || !isLaunched || !resourceUri || !appSessionId || !resolvedInput) return;
241
247
 
242
- if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
248
+ if (!sentInputRef.current || JSON.stringify(resolvedInput) !== JSON.stringify(lastInputRef.current)) {
243
249
  sentInputRef.current = true;
244
- lastInputRef.current = input;
245
- host.sendToolInput(input);
250
+ lastInputRef.current = resolvedInput;
251
+ host.sendToolInput(resolvedInput);
246
252
  }
247
- }, [host, isLaunched, input, resourceUri, appSessionId, name]);
253
+ }, [host, isLaunched, resolvedInput, resourceUri, appSessionId, resolvedToolName]);
248
254
 
249
255
  useEffect(() => {
250
256
  if (!host || !isLaunched || !resourceUri || !appSessionId || result === undefined) return;
@@ -259,7 +265,7 @@ const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(functi
259
265
  : result;
260
266
  host.sendToolResult(formattedResult);
261
267
  }
262
- }, [host, isLaunched, result, status, resourceUri, appSessionId, name]);
268
+ }, [host, isLaunched, result, status, resourceUri, appSessionId, resolvedToolName]);
263
269
 
264
270
  useEffect(() => {
265
271
  if (status === 'executing' && lastStatusRef.current !== 'executing') {
@@ -354,34 +360,44 @@ const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(functi
354
360
  const McpAppView = memo(McpAppViewInner);
355
361
  McpAppView.displayName = 'McpAppView';
356
362
 
363
+ /**
364
+ * Renders an interactive MCP application inside a sandboxed iframe.
365
+ */
366
+ export const McpAppRenderer = memo(
367
+ forwardRef<McpAppRendererHandle, McpAppRendererProps>(function McpAppRenderer(
368
+ { client, ...props },
369
+ ref
370
+ ) {
371
+ const clientRef = useRef(client || null);
372
+ clientRef.current = client || null;
373
+
374
+ return <McpAppView ref={ref} clientRef={clientRef} {...props} />;
375
+ })
376
+ );
377
+
357
378
  /**
358
379
  * Helpers scoped to one `mcpClient`. Pass the client here once; `McpAppRenderer` only needs per-tool props (`name`, `input`, `result`, `status`).
359
380
  *
360
381
  * @param mcpClient - From `useMcp()` or context (for example `useMcpContext()`).
382
+ * @deprecated Use the standalone `<McpAppRenderer>` component and `getMcpAppMetadata` utility directly.
361
383
  */
362
384
  export function useMcpApps(mcpClient: McpClient | null) {
363
- // Stable `McpAppRenderer` type: parent re-renders and `connections` updates must not remount the iframe.
364
- const clientRef = useRef(mcpClient);
365
- clientRef.current = mcpClient;
366
-
367
385
  const getAppMetadata = useCallback(
368
- (toolName: string) => getMcpAppMetadata(clientRef.current, toolName),
369
- []
386
+ (toolName: string) => getMcpAppMetadata(mcpClient, toolName),
387
+ [mcpClient]
370
388
  );
371
389
 
372
- const McpAppRenderer = useMemo(() => {
373
- const Inner = forwardRef<McpAppRendererHandle, McpAppRendererProps>(function McpAppRenderer(
374
- props,
375
- ref,
376
- ) {
377
- return <McpAppView ref={ref} clientRef={clientRef} {...props} />;
378
- });
379
- const Renderer = memo(Inner);
380
- Renderer.displayName = 'McpAppRenderer';
381
- return Renderer;
382
- }, []);
383
-
384
- return { getAppMetadata, McpAppRenderer };
390
+ const BoundMcpAppRenderer = useMemo(() => {
391
+ const Renderer = forwardRef<McpAppRendererHandle, Omit<McpAppRendererProps, 'client'>>(
392
+ function BoundMcpAppRenderer(props, ref) {
393
+ return <McpAppRenderer ref={ref} client={mcpClient} {...props} />;
394
+ }
395
+ );
396
+ Renderer.displayName = 'BoundMcpAppRenderer';
397
+ return memo(Renderer);
398
+ }, [mcpClient]);
399
+
400
+ return { getAppMetadata, McpAppRenderer: BoundMcpAppRenderer };
385
401
  }
386
402
 
387
403
  function extractToolName(fullName: string): string {
@@ -389,13 +405,15 @@ function extractToolName(fullName: string): string {
389
405
  return match?.[1] || fullName;
390
406
  }
391
407
 
392
- function getMcpAppMetadata(
408
+ export function getMcpAppMetadata(
393
409
  mcpClient: McpClient | null,
394
- toolName: string
410
+ toolName: string,
411
+ input?: Record<string, unknown> | null
395
412
  ): McpAppMetadata | undefined {
396
413
  if (!mcpClient) return undefined;
397
414
 
398
- const extractedName = extractToolName(toolName);
415
+ const { toolName: proxyToolName } = resolveMetaToolProxy(toolName, input);
416
+ const extractedName = extractToolName(proxyToolName);
399
417
 
400
418
  for (const conn of mcpClient.connections) {
401
419
  for (const tool of conn.tools) {
@@ -290,17 +290,48 @@ export function useMcp(options: UseMcpOptions): McpClient {
290
290
  state === 'CONNECTED' ||
291
291
  state === 'DISCOVERING';
292
292
 
293
+ const getVisibleState = (
294
+ incomingState: McpConnectionState,
295
+ existingState?: McpConnectionState,
296
+ previousState?: McpConnectionState
297
+ ): McpConnectionState => {
298
+ // `INITIALIZING` has two meanings in practice:
299
+ // 1. genuine cold start / reconnect work
300
+ // 2. an internal setup step that happens mid-OAuth completion
301
+ //
302
+ // For case (2), showing raw `INITIALIZING` creates a confusing user-facing
303
+ // sequence like AUTHENTICATING -> INITIALIZING -> AUTHENTICATED.
304
+ // We keep the raw event stream intact for observability, but collapse the
305
+ // visible state back into the current auth phase in the UI.
306
+ if (
307
+ incomingState === 'INITIALIZING' &&
308
+ (existingState === 'AUTHENTICATING' ||
309
+ existingState === 'AUTHENTICATED' ||
310
+ previousState === 'AUTHENTICATING' ||
311
+ previousState === 'AUTHENTICATED')
312
+ ) {
313
+ return existingState === 'AUTHENTICATED' || previousState === 'AUTHENTICATED'
314
+ ? 'AUTHENTICATED'
315
+ : 'AUTHENTICATING';
316
+ }
317
+
318
+ return incomingState;
319
+ };
320
+
293
321
  setConnections((prev: McpConnection[]) => {
294
322
  switch (event.type) {
295
323
  case 'state_changed': {
296
324
  const existing = prev.find((c: McpConnection) => c.sessionId === event.sessionId);
297
325
  if (existing) {
326
+ // Normalize the incoming backend state into the smoother user-facing
327
+ // state we want to render for this existing connection.
328
+ const normalizedState = getVisibleState(event.state, existing.state, event.previousState);
298
329
  // In stateless per-request transport, tool calls can emit transient reconnect states.
299
330
  // Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
300
331
  const nextState =
301
- existing.state === 'READY' && isTransientReconnectState(event.state)
332
+ existing.state === 'READY' && isTransientReconnectState(normalizedState)
302
333
  ? existing.state
303
- : event.state;
334
+ : normalizedState;
304
335
 
305
336
  return prev.map((c: McpConnection) =>
306
337
  c.sessionId === event.sessionId ? {
@@ -323,7 +354,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
323
354
  serverId: event.serverId,
324
355
  serverName: event.serverName,
325
356
  serverUrl: event.serverUrl,
326
- state: event.state,
357
+ // New connections do not have prior local state, so we normalize
358
+ // only against the server-reported previous state.
359
+ state: getVisibleState(event.state, undefined, event.previousState),
327
360
  createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
328
361
  tools: [],
329
362
  },
@@ -240,16 +240,49 @@ export function useMcp(options: UseMcpOptions): McpClient {
240
240
  state === 'CONNECTED' ||
241
241
  state === 'DISCOVERING';
242
242
 
243
+ const getVisibleState = (
244
+ incomingState: McpConnectionState,
245
+ existingState?: McpConnectionState,
246
+ previousState?: McpConnectionState
247
+ ): McpConnectionState => {
248
+ // `INITIALIZING` has two meanings in practice:
249
+ // 1. genuine cold start / reconnect work
250
+ // 2. an internal setup step that happens mid-OAuth completion
251
+ //
252
+ // For case (2), showing raw `INITIALIZING` creates a confusing user-facing
253
+ // sequence like AUTHENTICATING -> INITIALIZING -> AUTHENTICATED.
254
+ // We keep the raw event stream intact for observability, but collapse the
255
+ // visible state back into the current auth phase in the UI.
256
+ if (
257
+ incomingState === 'INITIALIZING' &&
258
+ (
259
+ existingState === 'AUTHENTICATING' ||
260
+ existingState === 'AUTHENTICATED' ||
261
+ previousState === 'AUTHENTICATING' ||
262
+ previousState === 'AUTHENTICATED'
263
+ )
264
+ ) {
265
+ return existingState === 'AUTHENTICATED' || previousState === 'AUTHENTICATED'
266
+ ? 'AUTHENTICATED'
267
+ : 'AUTHENTICATING';
268
+ }
269
+
270
+ return incomingState;
271
+ };
272
+
243
273
  switch (event.type) {
244
274
  case 'state_changed': {
245
275
  const existing = connections.value.find((c) => c.sessionId === event.sessionId);
246
276
  if (existing) {
277
+ // Normalize the incoming backend state into the smoother user-facing
278
+ // state we want to render for this existing connection.
279
+ const normalizedState = getVisibleState(event.state, existing.state, event.previousState);
247
280
  // In stateless per-request transport, tool calls can emit transient reconnect states.
248
281
  // Keep READY sticky to avoid UI flicker from READY -> CONNECTING -> CONNECTED.
249
282
  const nextState =
250
- existing.state === 'READY' && isTransientReconnectState(event.state)
283
+ existing.state === 'READY' && isTransientReconnectState(normalizedState)
251
284
  ? existing.state
252
- : event.state;
285
+ : normalizedState;
253
286
 
254
287
  const index = connections.value.indexOf(existing);
255
288
  connections.value[index] = {
@@ -268,7 +301,9 @@ export function useMcp(options: UseMcpOptions): McpClient {
268
301
  sessionId: event.sessionId,
269
302
  serverId: event.serverId,
270
303
  serverName: event.serverName,
271
- state: event.state,
304
+ // New connections do not have prior local state, so we normalize
305
+ // only against the server-reported previous state.
306
+ state: getVisibleState(event.state, undefined, event.previousState),
272
307
  createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
273
308
  tools: [],
274
309
  }];
@@ -363,9 +363,24 @@ export class SSEConnectionManager {
363
363
  return existing;
364
364
  }
365
365
 
366
+ const session = await storage.getSession(this.identity, sessionId);
367
+ if (!session) {
368
+ throw new Error('Session not found');
369
+ }
370
+
366
371
  const client = new MCPClient({
367
372
  identity: this.identity,
368
373
  sessionId,
374
+ // These fields are optional in MCPClient, but when rehydrating a known
375
+ // stored session on the server we pass them explicitly to preserve the
376
+ // original transport/connection metadata instead of relying on lazy
377
+ // reloading during initialize().
378
+ serverId: session.serverId,
379
+ serverName: session.serverName,
380
+ serverUrl: session.serverUrl,
381
+ callbackUrl: session.callbackUrl,
382
+ transportType: session.transportType,
383
+ headers: session.headers,
369
384
  });
370
385
 
371
386
  // Subscribe to events before connecting
@@ -437,6 +452,16 @@ export class SSEConnectionManager {
437
452
  const client = new MCPClient({
438
453
  identity: this.identity,
439
454
  sessionId,
455
+ // These fields are optional in MCPClient, but when rehydrating a known
456
+ // stored session on the server we pass them explicitly to preserve the
457
+ // original transport/connection metadata instead of relying on lazy
458
+ // reloading during initialize().
459
+ serverId: session.serverId,
460
+ serverName: session.serverName,
461
+ serverUrl: session.serverUrl,
462
+ callbackUrl: session.callbackUrl,
463
+ transportType: session.transportType,
464
+ headers: session.headers,
440
465
  ...clientMetadata,
441
466
  });
442
467
 
@@ -478,6 +503,20 @@ export class SSEConnectionManager {
478
503
  const client = new MCPClient({
479
504
  identity: this.identity,
480
505
  sessionId,
506
+ // These fields are optional in MCPClient, but when rehydrating a known
507
+ // stored session on the server we pass them explicitly to preserve the
508
+ // original connection metadata instead of relying on lazy
509
+ // reloading during initialize().
510
+ serverId: session.serverId,
511
+ serverName: session.serverName,
512
+ serverUrl: session.serverUrl,
513
+ callbackUrl: session.callbackUrl,
514
+ // NOTE: transportType is intentionally omitted here.
515
+ // The session's stored transportType is a placeholder ('streamable_http')
516
+ // set before transport negotiation. Omitting it lets MCPClient auto-negotiate
517
+ // (try streamable_http → SSE fallback), which is critical for servers like
518
+ // Neon that only support SSE transport.
519
+ headers: session.headers,
481
520
  });
482
521
 
483
522
  client.onConnectionEvent((event) => this.emitConnectionEvent(event));
@@ -32,6 +32,8 @@ export type {
32
32
  } from '../shared/events';
33
33
 
34
34
  export type {
35
+ ToolClient,
36
+ ToolClientProvider,
35
37
  ToolInfo,
36
38
  McpRpcRequest,
37
39
  McpRpcResponse,
@@ -500,16 +500,10 @@ export class MCPClient {
500
500
  this.emitStateChange('CONNECTED');
501
501
  this.emitProgress('Connected successfully');
502
502
 
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
- }
503
+ // Refresh session metadata on every successful connect so active sessions
504
+ // record ongoing usage and don't look dormant to storage cleanup jobs.
505
+ console.log(`[MCPClient] Saving session ${this.sessionId} with 12hr TTL (connect success)`);
506
+ await this.saveSession(SESSION_TTL_SECONDS, true);
513
507
  } catch (error) {
514
508
  /** Handle Authentication Errors */
515
509
  if (
@@ -537,11 +531,17 @@ export class MCPClient {
537
531
  : `OAuth authorization URL not available: ${detail}`;
538
532
  this.emitError(message, 'auth');
539
533
  this.emitStateChange('FAILED');
534
+
535
+ // Proactive Cleanup: This session has reached a terminal failure state.
536
+ // We remove it now to ensure the database remains lean, bypassing the
537
+ // automated lifecycle sweep.
540
538
  try {
541
539
  await storage.removeSession(this.identity, this.sessionId);
542
540
  } catch {
543
- // best-effort cleanup
541
+ // Non-blocking: Proactive cleanup failures are suppressed to prioritize
542
+ // the original error context.
544
543
  }
544
+
545
545
  throw new Error(message);
546
546
  }
547
547
 
@@ -571,6 +571,19 @@ export class MCPClient {
571
571
  const errorMessage = error instanceof Error ? error.message : 'Connection failed';
572
572
  this.emitError(errorMessage, 'connection');
573
573
  this.emitStateChange('FAILED');
574
+
575
+ // Terminal Handshake Failure: only purge transient sessions. Active
576
+ // sessions may still hold valid credentials for a later reconnect.
577
+ try {
578
+ const existingSession = await storage.getSession(this.identity, this.sessionId);
579
+ if (!existingSession || existingSession.active !== true) {
580
+ await storage.removeSession(this.identity, this.sessionId);
581
+ }
582
+ } catch {
583
+ // Non-blocking: Cleanup is performed on a best-effort basis and should
584
+ // not interfere with the primary error propagation.
585
+ }
586
+
574
587
  throw error;
575
588
  }
576
589
  }
@@ -606,6 +619,7 @@ export class MCPClient {
606
619
 
607
620
  let lastError: unknown;
608
621
  let tokensExchanged = false;
622
+ let authenticatedStateEmitted = false;
609
623
 
610
624
  for (const currentType of transportsToTry) {
611
625
  const isLastAttempt = currentType === transportsToTry[transportsToTry.length - 1];
@@ -623,10 +637,11 @@ export class MCPClient {
623
637
  this.emitProgress(`Tokens already exchanged, skipping auth step for ${currentType}...`);
624
638
  }
625
639
 
626
- /** Success! Update transport type */
627
- this.transportType = currentType;
640
+ if (!authenticatedStateEmitted) {
641
+ this.emitStateChange('AUTHENTICATED');
642
+ authenticatedStateEmitted = true;
643
+ }
628
644
 
629
- this.emitStateChange('AUTHENTICATED');
630
645
  this.emitProgress('Creating authenticated client...');
631
646
 
632
647
  this.client = new Client(
@@ -650,6 +665,9 @@ export class MCPClient {
650
665
  /** We explicitly try to connect with the transport we just auth'd with first */
651
666
  await this.client.connect(this.transport);
652
667
 
668
+ /** Connection succeeded — lock in the transport type */
669
+ this.transportType = currentType;
670
+
653
671
  this.emitStateChange('CONNECTED');
654
672
  // Update session with 12hr TTL after successful OAuth
655
673
  console.log(`[MCPClient] Updating session ${this.sessionId} to 12hr TTL (OAuth complete)`);
@@ -1108,7 +1126,9 @@ export class MCPClient {
1108
1126
  * @returns Server name or undefined
1109
1127
  */
1110
1128
  getServerName(): string | undefined {
1111
- return this.serverName;
1129
+ const info = (this.client as any)?.getServerVersion();
1130
+ console.log('server info ->', info);
1131
+ return info?.title ?? info?.name ?? this.serverName;
1112
1132
  }
1113
1133
 
1114
1134
  /**
@@ -22,6 +22,8 @@ export * from './errors';
22
22
 
23
23
  // Types
24
24
  export type {
25
+ ToolClient,
26
+ ToolClientProvider,
25
27
  ToolInfo,
26
28
  McpRpcRequest,
27
29
  McpRpcResponse,
@@ -73,3 +75,37 @@ export {
73
75
  type ToolUiConfig,
74
76
  } from './tool-utils.js';
75
77
 
78
+ // Tool Router — Context window optimization
79
+ export {
80
+ ToolRouter,
81
+ type ToolRouterOptions,
82
+ type ToolRouterStrategy,
83
+ type ToolRouterClientInput,
84
+ type ToolGroupInfo,
85
+ } from './tool-router.js';
86
+
87
+ export {
88
+ ToolIndex,
89
+ type ToolSummary,
90
+ type IndexedTool,
91
+ type ToolIndexOptions,
92
+ type EmbedFn,
93
+ } from './tool-index.js';
94
+
95
+ export {
96
+ SchemaCompressor,
97
+ type CompactTool,
98
+ type CompressionStats,
99
+ } from './schema-compressor.js';
100
+
101
+ export {
102
+ createSearchToolDefinition,
103
+ createRegexSearchToolDefinition,
104
+ createGetSchemaToolDefinition,
105
+ createExecuteToolDefinition,
106
+ executeMetaTool,
107
+ isMetaTool,
108
+ resolveMetaToolProxy,
109
+ type CallToolFn,
110
+ } from './meta-tools.js';
111
+