@mcp-ts/sdk 1.5.1 → 1.5.3

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 (61) hide show
  1. package/README.md +89 -27
  2. package/dist/adapters/agui-adapter.d.mts +1 -1
  3. package/dist/adapters/agui-adapter.d.ts +1 -1
  4. package/dist/adapters/agui-adapter.js +50 -10
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +50 -10
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +5 -1
  9. package/dist/adapters/agui-middleware.d.ts +5 -1
  10. package/dist/adapters/agui-middleware.js +116 -49
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +117 -50
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +1 -1
  15. package/dist/adapters/ai-adapter.d.ts +1 -1
  16. package/dist/adapters/ai-adapter.js +49 -9
  17. package/dist/adapters/ai-adapter.js.map +1 -1
  18. package/dist/adapters/ai-adapter.mjs +49 -9
  19. package/dist/adapters/ai-adapter.mjs.map +1 -1
  20. package/dist/adapters/langchain-adapter.d.mts +1 -1
  21. package/dist/adapters/langchain-adapter.d.ts +1 -1
  22. package/dist/adapters/langchain-adapter.js +49 -9
  23. package/dist/adapters/langchain-adapter.js.map +1 -1
  24. package/dist/adapters/langchain-adapter.mjs +49 -9
  25. package/dist/adapters/langchain-adapter.mjs.map +1 -1
  26. package/dist/client/react.d.mts +10 -0
  27. package/dist/client/react.d.ts +10 -0
  28. package/dist/client/react.js +23 -0
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +23 -0
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +10 -0
  33. package/dist/client/vue.d.ts +10 -0
  34. package/dist/client/vue.js +17 -0
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +17 -0
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/index.d.mts +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +123 -28
  41. package/dist/index.js.map +1 -1
  42. package/dist/index.mjs +123 -28
  43. package/dist/index.mjs.map +1 -1
  44. package/dist/shared/index.d.mts +2 -2
  45. package/dist/shared/index.d.ts +2 -2
  46. package/dist/shared/index.js +123 -28
  47. package/dist/shared/index.js.map +1 -1
  48. package/dist/shared/index.mjs +123 -28
  49. package/dist/shared/index.mjs.map +1 -1
  50. package/dist/{tool-router-XnWVxPzv.d.mts → tool-router-DK0RJblO.d.mts} +3 -0
  51. package/dist/{tool-router-Bo8qZbsD.d.ts → tool-router-DsKhRmJm.d.ts} +3 -0
  52. package/package.json +3 -3
  53. package/src/adapters/agui-adapter.ts +7 -7
  54. package/src/adapters/agui-middleware.ts +163 -59
  55. package/src/adapters/ai-adapter.ts +5 -5
  56. package/src/adapters/langchain-adapter.ts +5 -5
  57. package/src/client/react/use-mcp.ts +48 -0
  58. package/src/client/vue/use-mcp.ts +42 -0
  59. package/src/shared/meta-tools.ts +73 -15
  60. package/src/shared/tool-index.ts +85 -12
  61. package/src/shared/tool-router.ts +8 -7
@@ -246,12 +246,12 @@ export class AguiAdapter {
246
246
  const filteredTools = await router.getFilteredTools();
247
247
 
248
248
  return filteredTools.map(tool => {
249
- const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
250
- const namespace = routedTool.serverName ?? routedTool.sessionId;
249
+ const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
250
+ const namespace = routedTool.serverId ?? routedTool.sessionId;
251
251
  return {
252
252
  name: isMetaTool(tool.name)
253
253
  ? tool.name
254
- : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName),
254
+ : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId),
255
255
  description: tool.description || `Execute ${tool.name}`,
256
256
  parameters: cleanSchema(tool.inputSchema),
257
257
  handler: async (args: any) => {
@@ -279,19 +279,19 @@ export class AguiAdapter {
279
279
  private async getToolDefinitionsViaRouter(router: ToolRouter): Promise<AguiToolDefinition[]> {
280
280
  const filteredTools = await router.getFilteredTools();
281
281
  return filteredTools.map(tool => {
282
- const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
282
+ const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
283
283
  return {
284
284
  name: isMetaTool(tool.name)
285
285
  ? tool.name
286
- : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName),
286
+ : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId),
287
287
  description: tool.description || `Execute ${tool.name}`,
288
288
  parameters: cleanSchema(tool.inputSchema)
289
289
  };
290
290
  });
291
291
  }
292
292
 
293
- private getRouterToolKey(toolName: string, sessionId?: string, serverName?: string): string {
294
- const namespace = sessionId ?? serverName ?? 'mcp';
293
+ private getRouterToolKey(toolName: string, sessionId?: string, serverId?: string): string {
294
+ const namespace = sessionId ?? serverId ?? 'mcp';
295
295
  const normalized = namespace
296
296
  .toLowerCase()
297
297
  .replace(/[^a-z0-9]+/g, '_')
@@ -8,7 +8,7 @@
8
8
  * @requires rxjs - Uses RxJS Observables for event streaming
9
9
  */
10
10
 
11
- import { Observable, Subscriber } from 'rxjs';
11
+ import { Observable, Subscriber, Subscription } from 'rxjs';
12
12
  import {
13
13
  Middleware,
14
14
  EventType,
@@ -28,11 +28,19 @@ interface ToolResult {
28
28
  messageId: string;
29
29
  }
30
30
 
31
+ interface SnapshotToolCall {
32
+ id: string;
33
+ name: string;
34
+ arguments: string;
35
+ }
36
+
31
37
  /** State for tracking tool calls during a run */
32
38
  interface RunState {
33
39
  toolCallArgsBuffer: Map<string, string>;
34
40
  toolCallNames: Map<string, string>;
35
41
  pendingMcpCalls: Set<string>;
42
+ completedMcpCalls: Set<string>;
43
+ assistantMessageId?: string;
36
44
  textContent?: string;
37
45
  error: boolean;
38
46
  }
@@ -87,6 +95,10 @@ export class McpMiddleware extends Middleware {
87
95
  }
88
96
  }
89
97
 
98
+ private normalizeArgsString(argsString: string): string {
99
+ return JSON.stringify(this.parseArgs(argsString));
100
+ }
101
+
90
102
  private async executeTool(toolName: string, args: Record<string, any>): Promise<string> {
91
103
  const tool = this.tools.find(t => t.name === toolName);
92
104
  if (!tool?.handler) {
@@ -116,13 +128,82 @@ export class McpMiddleware extends Middleware {
116
128
  if (!anyInput.runId) anyInput.runId = this.generateId('mcp_run');
117
129
  }
118
130
 
131
+ private getSnapshotToolCalls(message: any): SnapshotToolCall[] {
132
+ const rawToolCalls = Array.isArray(message?.toolCalls)
133
+ ? message.toolCalls
134
+ : (Array.isArray(message?.tool_calls) ? message.tool_calls : []);
135
+
136
+ return rawToolCalls.flatMap((toolCall: any) => {
137
+ if (!toolCall || typeof toolCall !== 'object') return [];
138
+
139
+ const id = typeof toolCall.id === 'string' ? toolCall.id : undefined;
140
+ const fn = toolCall.function && typeof toolCall.function === 'object'
141
+ ? toolCall.function
142
+ : undefined;
143
+ const name = typeof fn?.name === 'string'
144
+ ? fn.name
145
+ : (typeof toolCall.name === 'string' ? toolCall.name : undefined);
146
+
147
+ if (!id || !name) return [];
148
+
149
+ const rawArgs = fn?.arguments ?? toolCall.arguments ?? toolCall.args;
150
+ const argsString = typeof rawArgs === 'string'
151
+ ? rawArgs
152
+ : (rawArgs && typeof rawArgs === 'object' ? JSON.stringify(rawArgs) : '{}');
153
+
154
+ return [{ id, name, arguments: argsString }];
155
+ });
156
+ }
157
+
158
+ private getResolvedToolCallIds(messages: any[]): Set<string> {
159
+ const resolved = new Set<string>();
160
+ for (const message of messages) {
161
+ if (message?.role !== 'tool') continue;
162
+ const toolCallId = message.toolCallId ?? message.tool_call_id;
163
+ if (typeof toolCallId === 'string') {
164
+ resolved.add(toolCallId);
165
+ }
166
+ }
167
+ return resolved;
168
+ }
169
+
170
+ private shouldFilterMessagesSnapshot(event: BaseEvent, state: RunState): boolean {
171
+ if (state.completedMcpCalls.size === 0 || state.pendingMcpCalls.size > 0) {
172
+ return false;
173
+ }
174
+
175
+ // Only suppress snapshots that replay an assistant answer we already streamed.
176
+ // Snapshot-only final answers still need to reach the UI.
177
+ if (!state.assistantMessageId && !state.textContent) {
178
+ return false;
179
+ }
180
+
181
+ const messages = (event as any).messages;
182
+ if (!Array.isArray(messages)) {
183
+ return false;
184
+ }
185
+
186
+ const lastAssistant = [...messages].reverse().find((message) => message?.role === 'assistant');
187
+ if (!lastAssistant) {
188
+ return false;
189
+ }
190
+
191
+ return (
192
+ (typeof lastAssistant.id === 'string' && lastAssistant.id === state.assistantMessageId) ||
193
+ (typeof lastAssistant.content === 'string' && lastAssistant.content === state.textContent)
194
+ );
195
+ }
196
+
119
197
  /** Process tool call events and update state */
120
198
  private handleToolCallEvent(event: BaseEvent, state: RunState): void {
121
- const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls } = state;
199
+ const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls, completedMcpCalls } = state;
122
200
 
123
201
  // Accumulate text content for reconstruction
124
- if (event.type === EventType.TEXT_MESSAGE_CHUNK) {
202
+ if (event.type === EventType.TEXT_MESSAGE_CHUNK || event.type === EventType.TEXT_MESSAGE_CONTENT) {
125
203
  const e = event as any;
204
+ if (typeof e.messageId === 'string') {
205
+ state.assistantMessageId = e.messageId;
206
+ }
126
207
  if (e.delta) {
127
208
  state.textContent = (state.textContent || '') + e.delta;
128
209
  }
@@ -132,7 +213,7 @@ export class McpMiddleware extends Middleware {
132
213
  const e = event as any;
133
214
  if (e.toolCallId && e.toolCallName) {
134
215
  toolCallNames.set(e.toolCallId, e.toolCallName);
135
- if (this.isMcpTool(e.toolCallName)) {
216
+ if (this.isMcpTool(e.toolCallName) && !completedMcpCalls.has(e.toolCallId)) {
136
217
  pendingMcpCalls.add(e.toolCallId);
137
218
  }
138
219
  console.log(`[McpMiddleware] TOOL_CALL_START: ${e.toolCallName} (id: ${e.toolCallId}, isMCP: ${this.isMcpTool(e.toolCallName)})`);
@@ -152,33 +233,52 @@ export class McpMiddleware extends Middleware {
152
233
  console.log(`[McpMiddleware] TOOL_CALL_END: ${toolCallNames.get(e.toolCallId) ?? 'unknown'} (id: ${e.toolCallId})`);
153
234
  }
154
235
 
236
+ if (event.type === EventType.TOOL_CALL_RESULT) {
237
+ const e = event as any;
238
+ if (typeof e.toolCallId === 'string') {
239
+ pendingMcpCalls.delete(e.toolCallId);
240
+ completedMcpCalls.add(e.toolCallId);
241
+ }
242
+ }
243
+
155
244
  // Workaround: Extract parallel tool calls from MESSAGES_SNAPSHOT
156
245
  if (event.type === EventType.MESSAGES_SNAPSHOT) {
157
246
  const messages = (event as any).messages || [];
158
247
  if (messages.length > 0) {
159
- const lastMsg = messages[messages.length - 1];
160
- // Update text content from snapshot if available (often more reliable)
161
- if (lastMsg.role === 'assistant' && lastMsg.content) {
162
- state.textContent = lastMsg.content;
248
+ const resolvedToolCallIds = this.getResolvedToolCallIds(messages);
249
+
250
+ for (const toolCallId of [...pendingMcpCalls]) {
251
+ if (resolvedToolCallIds.has(toolCallId) || completedMcpCalls.has(toolCallId)) {
252
+ pendingMcpCalls.delete(toolCallId);
253
+ }
163
254
  }
164
255
 
165
- // Discover tools
256
+ // Discover unresolved tools. LangGraph snapshots include historical
257
+ // assistant tool calls, so only calls without a tool result are pending.
166
258
  for (let i = messages.length - 1; i >= 0; i--) {
167
259
  const msg = messages[i];
168
- const tools = Array.isArray(msg.toolCalls) ? msg.toolCalls :
169
- (Array.isArray(msg.tool_calls) ? msg.tool_calls : []);
260
+ const tools = this.getSnapshotToolCalls(msg)
261
+ .filter(tc =>
262
+ !resolvedToolCallIds.has(tc.id) &&
263
+ !completedMcpCalls.has(tc.id) &&
264
+ this.isMcpTool(tc.name)
265
+ );
170
266
 
171
267
  if (msg.role === 'assistant' && tools.length > 0) {
172
268
  for (const tc of tools) {
173
- if (tc.id && tc.function?.name && !toolCallNames.has(tc.id)) {
174
- toolCallNames.set(tc.id, tc.function.name);
175
- toolCallArgsBuffer.set(tc.id, tc.function.arguments || '{}');
176
- if (this.isMcpTool(tc.function.name)) {
177
- pendingMcpCalls.add(tc.id);
178
- console.log(`[McpMiddleware] MESSAGES_SNAPSHOT: Discovered ${tc.function.name} (id: ${tc.id})`);
179
- }
269
+ if (!toolCallNames.has(tc.id)) {
270
+ toolCallNames.set(tc.id, tc.name);
271
+ toolCallArgsBuffer.set(tc.id, tc.arguments || '{}');
272
+ pendingMcpCalls.add(tc.id);
273
+ console.log(`[McpMiddleware] MESSAGES_SNAPSHOT: Discovered ${tc.name} (id: ${tc.id})`);
180
274
  }
181
275
  }
276
+ if (typeof msg.id === 'string') {
277
+ state.assistantMessageId = msg.id;
278
+ }
279
+ if (typeof msg.content === 'string') {
280
+ state.textContent = msg.content;
281
+ }
182
282
  break;
183
283
  }
184
284
  }
@@ -188,10 +288,15 @@ export class McpMiddleware extends Middleware {
188
288
 
189
289
  /** Execute pending MCP tools and return results */
190
290
  private async executeTools(state: RunState): Promise<ToolResult[]> {
191
- const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls } = state;
291
+ const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls, completedMcpCalls } = state;
192
292
  const results: ToolResult[] = [];
193
293
 
194
294
  const promises = [...pendingMcpCalls].map(async (toolCallId) => {
295
+ if (completedMcpCalls.has(toolCallId)) {
296
+ pendingMcpCalls.delete(toolCallId);
297
+ return;
298
+ }
299
+
195
300
  const toolName = toolCallNames.get(toolCallId);
196
301
  if (!toolName) return;
197
302
 
@@ -206,6 +311,7 @@ export class McpMiddleware extends Middleware {
206
311
  messageId: this.generateId('mcp_result'),
207
312
  });
208
313
  pendingMcpCalls.delete(toolCallId);
314
+ completedMcpCalls.add(toolCallId);
209
315
  });
210
316
 
211
317
  await Promise.all(promises);
@@ -229,10 +335,13 @@ export class McpMiddleware extends Middleware {
229
335
 
230
336
  run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
231
337
  return new Observable<BaseEvent>((observer: Subscriber<BaseEvent>) => {
338
+ const subscriptions = new Set<Subscription>();
339
+ let continuationInProgress = false;
232
340
  const state: RunState = {
233
341
  toolCallArgsBuffer: new Map(),
234
342
  toolCallNames: new Map(),
235
343
  pendingMcpCalls: new Set(),
344
+ completedMcpCalls: new Set(),
236
345
  textContent: '',
237
346
  error: false,
238
347
  };
@@ -265,12 +374,13 @@ export class McpMiddleware extends Middleware {
265
374
  }
266
375
 
267
376
  console.log(`[McpMiddleware] RUN_FINISHED with ${state.pendingMcpCalls.size} pending calls`);
377
+ continuationInProgress = true;
268
378
 
269
379
  // Reconstruct the Assistant Message that triggered these tools
270
380
  const toolCalls = [];
271
381
  for (const toolCallId of state.pendingMcpCalls) {
272
382
  const name = state.toolCallNames.get(toolCallId);
273
- const args = state.toolCallArgsBuffer.get(toolCallId) || '{}';
383
+ const args = this.normalizeArgsString(state.toolCallArgsBuffer.get(toolCallId) || '{}');
274
384
  if (name) {
275
385
  toolCalls.push({
276
386
  id: toolCallId,
@@ -283,10 +393,10 @@ export class McpMiddleware extends Middleware {
283
393
  // Add the Assistant Message to history FIRST
284
394
  if (toolCalls.length > 0 || state.textContent) {
285
395
  const assistantMsg = {
286
- id: this.generateId('msg_ast'),
396
+ id: state.assistantMessageId || this.generateId('msg_ast'),
287
397
  role: 'assistant',
288
398
  content: state.textContent || null, // Ensure null if empty string for strict LLMs
289
- tool_calls: toolCalls.length > 0 ? toolCalls : undefined
399
+ toolCalls: toolCalls.length > 0 ? toolCalls : undefined
290
400
  };
291
401
  input.messages.push(assistantMsg as any);
292
402
  console.log(`[McpMiddleware] Added assistant message to history before tools: ${state.textContent?.slice(0, 50)}... [${toolCalls.length} tools]`);
@@ -304,7 +414,7 @@ export class McpMiddleware extends Middleware {
304
414
  input.messages.push({
305
415
  id: messageId,
306
416
  role: 'tool',
307
- tool_call_id: toolCallId,
417
+ toolCallId,
308
418
  content: result,
309
419
  } as any);
310
420
  }
@@ -312,35 +422,45 @@ export class McpMiddleware extends Middleware {
312
422
  // Reset state for next turn
313
423
  state.toolCallArgsBuffer.clear();
314
424
  state.toolCallNames.clear();
425
+ state.assistantMessageId = undefined;
315
426
  state.textContent = ''; // Clear text content for next turn
316
427
 
317
- anyInput.runId = this.generateId('mcp_run');
318
428
  console.log(`[McpMiddleware] === CONTINUATION RUN === messages: ${input.messages.length}`);
319
429
 
320
- // Subscribe to continuation
321
- next.run(input).subscribe({
430
+ subscribeToRun(true);
431
+ };
432
+
433
+ const subscribeToRun = (isContinuation: boolean): Subscription => {
434
+ let subscription = new Subscription();
435
+ subscription = next.run(input).subscribe({
322
436
  next: (event) => {
323
437
  if (state.error) return;
324
438
 
325
439
  this.handleToolCallEvent(event, state);
326
440
 
327
441
  if (event.type === EventType.RUN_ERROR) {
328
- console.log(`[McpMiddleware] RUN_ERROR received in continuation`);
442
+ console.log(`[McpMiddleware] RUN_ERROR received${isContinuation ? ' in continuation' : ''}`);
329
443
  state.error = true;
330
444
  observer.next(event);
331
445
  observer.complete();
332
446
  return;
333
447
  }
334
448
 
335
- if (event.type === EventType.RUN_STARTED) {
449
+ if (isContinuation && event.type === EventType.RUN_STARTED) {
336
450
  console.log(`[McpMiddleware] Filtering RUN_STARTED from continuation`);
337
451
  return;
338
452
  }
339
453
 
454
+ if (event.type === EventType.MESSAGES_SNAPSHOT && this.shouldFilterMessagesSnapshot(event, state)) {
455
+ console.log(`[McpMiddleware] Filtering completed MCP MESSAGES_SNAPSHOT to preserve streamed message order`);
456
+ return;
457
+ }
458
+
340
459
  if (event.type === EventType.RUN_FINISHED) {
341
460
  if (state.pendingMcpCalls.size > 0) {
342
461
  handleRunFinished();
343
462
  } else {
463
+ continuationInProgress = false;
344
464
  observer.next(event);
345
465
  observer.complete();
346
466
  }
@@ -350,44 +470,28 @@ export class McpMiddleware extends Middleware {
350
470
  },
351
471
  error: (err) => {
352
472
  state.error = true;
473
+ continuationInProgress = false;
353
474
  observer.error(err);
354
475
  },
355
476
  complete: () => {
356
- if (!state.error && state.pendingMcpCalls.size === 0) observer.complete();
477
+ subscriptions.delete(subscription);
478
+ if (!state.error && state.pendingMcpCalls.size === 0 && !continuationInProgress) observer.complete();
357
479
  },
358
480
  });
481
+ if (!subscription.closed) {
482
+ subscriptions.add(subscription);
483
+ }
484
+ return subscription;
359
485
  };
360
486
 
361
- const subscription = next.run(input).subscribe({
362
- next: (event) => {
363
- if (state.error) return;
364
-
365
- this.handleToolCallEvent(event, state);
487
+ subscribeToRun(false);
366
488
 
367
- if (event.type === EventType.RUN_ERROR) {
368
- console.log(`[McpMiddleware] RUN_ERROR received`);
369
- state.error = true;
370
- observer.next(event);
371
- observer.complete();
372
- return;
373
- }
374
-
375
- if (event.type === EventType.RUN_FINISHED) {
376
- handleRunFinished();
377
- return;
378
- }
379
- observer.next(event);
380
- },
381
- error: (err) => {
382
- state.error = true;
383
- observer.error(err);
384
- },
385
- complete: () => {
386
- if (!state.error && state.pendingMcpCalls.size === 0) observer.complete();
387
- },
388
- });
389
-
390
- return () => subscription.unsubscribe();
489
+ return () => {
490
+ for (const subscription of subscriptions) {
491
+ subscription.unsubscribe();
492
+ }
493
+ subscriptions.clear();
494
+ };
391
495
  });
392
496
  }
393
497
  }
@@ -410,4 +514,4 @@ export { createMcpMiddleware as createMcpToolMiddleware };
410
514
 
411
515
  // Re-exports
412
516
  export { Middleware, EventType };
413
- export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent, Tool };
517
+ export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent, Tool };
@@ -137,11 +137,11 @@ export class AIAdapter {
137
137
  // @ts-ignore: ToolSet type inference can be tricky with dynamic imports
138
138
  return Object.fromEntries(
139
139
  filteredTools.map((tool) => {
140
- const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
141
- const namespace = routedTool.serverName ?? routedTool.sessionId;
140
+ const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
141
+ const namespace = routedTool.serverId ?? routedTool.sessionId;
142
142
  const toolKey = isMetaTool(tool.name)
143
143
  ? tool.name
144
- : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName);
144
+ : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId);
145
145
 
146
146
  return [
147
147
  toolKey,
@@ -172,8 +172,8 @@ export class AIAdapter {
172
172
  );
173
173
  }
174
174
 
175
- private getRouterToolKey(toolName: string, sessionId?: string, serverName?: string): string {
176
- const namespace = sessionId ?? serverName ?? 'mcp';
175
+ private getRouterToolKey(toolName: string, sessionId?: string, serverId?: string): string {
176
+ const namespace = sessionId ?? serverId ?? 'mcp';
177
177
  const normalized = namespace
178
178
  .toLowerCase()
179
179
  .replace(/[^a-z0-9]+/g, '_')
@@ -144,14 +144,14 @@ export class LangChainAdapter {
144
144
  const filteredTools = await router.getFilteredTools();
145
145
 
146
146
  return filteredTools.map((tool) => {
147
- const routedTool = tool as typeof tool & { sessionId?: string; serverName?: string };
148
- const namespace = routedTool.serverName ?? routedTool.sessionId;
147
+ const routedTool = tool as typeof tool & { sessionId?: string; serverId?: string; serverName?: string };
148
+ const namespace = routedTool.serverId ?? routedTool.sessionId;
149
149
  const schema = this.jsonSchemaToZod(tool.inputSchema);
150
150
 
151
151
  return new this.DynamicStructuredTool!({
152
152
  name: isMetaTool(tool.name)
153
153
  ? tool.name
154
- : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverName),
154
+ : this.getRouterToolKey(tool.name, routedTool.sessionId, routedTool.serverId),
155
155
  description: tool.description || `Tool ${tool.name}`,
156
156
  schema: schema,
157
157
  func: async (args: any) => {
@@ -183,8 +183,8 @@ export class LangChainAdapter {
183
183
  });
184
184
  }
185
185
 
186
- private getRouterToolKey(toolName: string, sessionId?: string, serverName?: string): string {
187
- const namespace = sessionId ?? serverName ?? 'mcp';
186
+ private getRouterToolKey(toolName: string, sessionId?: string, serverId?: string): string {
187
+ const namespace = sessionId ?? serverId ?? 'mcp';
188
188
  const normalized = namespace
189
189
  .toLowerCase()
190
190
  .replace(/[^a-z0-9]+/g, '_')
@@ -118,6 +118,17 @@ export interface McpClient {
118
118
  */
119
119
  disconnect: (sessionId: string) => Promise<void>;
120
120
 
121
+ /**
122
+ * Reconnect to an MCP server (disconnects existing session first)
123
+ */
124
+ reconnect: (params: {
125
+ serverId: string;
126
+ serverName: string;
127
+ serverUrl: string;
128
+ callbackUrl: string;
129
+ transportType?: 'sse' | 'streamable_http';
130
+ }) => Promise<string>;
131
+
121
132
  /**
122
133
  * Get connection by session ID
123
134
  */
@@ -499,6 +510,41 @@ export function useMcp(options: UseMcpOptions): McpClient {
499
510
  []
500
511
  );
501
512
 
513
+ /**
514
+ * Reconnect to an MCP server (tears down existing session, then connects fresh)
515
+ */
516
+ const reconnect = useCallback(
517
+ async (params: {
518
+ serverId: string;
519
+ serverName: string;
520
+ serverUrl: string;
521
+ callbackUrl: string;
522
+ transportType?: 'sse' | 'streamable_http';
523
+ }): Promise<string> => {
524
+ if (!clientRef.current) {
525
+ throw new Error('SSE client not initialized');
526
+ }
527
+
528
+ // Find and disconnect existing session for the same server
529
+ const existing = connections.find(
530
+ (c: McpConnection) => c.serverId === params.serverId || c.serverUrl === params.serverUrl
531
+ );
532
+ if (existing) {
533
+ await clientRef.current.disconnectFromServer(existing.sessionId);
534
+ if (isMountedRef.current) {
535
+ setConnections((prev: McpConnection[]) =>
536
+ prev.filter((c: McpConnection) => c.sessionId !== existing.sessionId)
537
+ );
538
+ }
539
+ }
540
+
541
+ // Connect fresh
542
+ const result = await clientRef.current.connectToServer(params);
543
+ return result.sessionId;
544
+ },
545
+ [connections]
546
+ );
547
+
502
548
  /**
503
549
  * Disconnect from an MCP server
504
550
  */
@@ -668,6 +714,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
668
714
  status,
669
715
  isInitializing,
670
716
  connect,
717
+ reconnect,
671
718
  disconnect,
672
719
  getConnection,
673
720
  getConnectionByServerId,
@@ -691,6 +738,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
691
738
  status,
692
739
  isInitializing,
693
740
  connect,
741
+ reconnect,
694
742
  disconnect,
695
743
  getConnection,
696
744
  getConnectionByServerId,
@@ -117,6 +117,17 @@ export interface McpClient {
117
117
  */
118
118
  disconnect: (sessionId: string) => Promise<void>;
119
119
 
120
+ /**
121
+ * Reconnect to an MCP server (disconnects existing session first)
122
+ */
123
+ reconnect: (params: {
124
+ serverId: string;
125
+ serverName: string;
126
+ serverUrl: string;
127
+ callbackUrl: string;
128
+ transportType?: 'sse' | 'streamable_http';
129
+ }) => Promise<string>;
130
+
120
131
  /**
121
132
  * Get connection by session ID
122
133
  */
@@ -492,6 +503,36 @@ export function useMcp(options: UseMcpOptions): McpClient {
492
503
  return result.sessionId;
493
504
  };
494
505
 
506
+ /**
507
+ * Reconnect to an MCP server (tears down existing session, then connects fresh)
508
+ */
509
+ const reconnect = async (params: {
510
+ serverId: string;
511
+ serverName: string;
512
+ serverUrl: string;
513
+ callbackUrl: string;
514
+ transportType?: 'sse' | 'streamable_http';
515
+ }): Promise<string> => {
516
+ if (!clientRef.value) {
517
+ throw new Error('SSE client not initialized');
518
+ }
519
+
520
+ // Find and disconnect existing session for the same server
521
+ const existing = connections.value.find(
522
+ (c) => c.serverId === params.serverId || c.serverUrl === params.serverUrl
523
+ );
524
+ if (existing) {
525
+ await clientRef.value.disconnectFromServer(existing.sessionId);
526
+ if (isMountedRef.value) {
527
+ connections.value = connections.value.filter((c) => c.sessionId !== existing.sessionId);
528
+ }
529
+ }
530
+
531
+ // Connect fresh
532
+ const result = await clientRef.value.connectToServer(params);
533
+ return result.sessionId;
534
+ };
535
+
495
536
  /**
496
537
  * Disconnect from an MCP server
497
538
  */
@@ -642,6 +683,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
642
683
  status: status as unknown as { value: 'connecting' | 'connected' | 'disconnected' | 'error' },
643
684
  isInitializing: isInitializing as unknown as { value: boolean },
644
685
  connect,
686
+ reconnect,
645
687
  disconnect,
646
688
  getConnection,
647
689
  getConnectionByServerId,