@mcp-ts/sdk 1.0.0 → 1.1.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 (77) hide show
  1. package/README.md +25 -13
  2. package/dist/adapters/agui-adapter.d.mts +21 -44
  3. package/dist/adapters/agui-adapter.d.ts +21 -44
  4. package/dist/adapters/agui-adapter.js +93 -67
  5. package/dist/adapters/agui-adapter.js.map +1 -1
  6. package/dist/adapters/agui-adapter.mjs +93 -68
  7. package/dist/adapters/agui-adapter.mjs.map +1 -1
  8. package/dist/adapters/agui-middleware.d.mts +32 -134
  9. package/dist/adapters/agui-middleware.d.ts +32 -134
  10. package/dist/adapters/agui-middleware.js +314 -350
  11. package/dist/adapters/agui-middleware.js.map +1 -1
  12. package/dist/adapters/agui-middleware.mjs +314 -351
  13. package/dist/adapters/agui-middleware.mjs.map +1 -1
  14. package/dist/adapters/ai-adapter.d.mts +2 -2
  15. package/dist/adapters/ai-adapter.d.ts +2 -2
  16. package/dist/adapters/langchain-adapter.d.mts +2 -2
  17. package/dist/adapters/langchain-adapter.d.ts +2 -2
  18. package/dist/adapters/mastra-adapter.d.mts +2 -2
  19. package/dist/adapters/mastra-adapter.d.ts +2 -2
  20. package/dist/client/index.d.mts +184 -57
  21. package/dist/client/index.d.ts +184 -57
  22. package/dist/client/index.js +535 -130
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/client/index.mjs +535 -131
  25. package/dist/client/index.mjs.map +1 -1
  26. package/dist/client/react.d.mts +40 -6
  27. package/dist/client/react.d.ts +40 -6
  28. package/dist/client/react.js +587 -142
  29. package/dist/client/react.js.map +1 -1
  30. package/dist/client/react.mjs +586 -143
  31. package/dist/client/react.mjs.map +1 -1
  32. package/dist/client/vue.d.mts +5 -5
  33. package/dist/client/vue.d.ts +5 -5
  34. package/dist/client/vue.js +545 -140
  35. package/dist/client/vue.js.map +1 -1
  36. package/dist/client/vue.mjs +545 -141
  37. package/dist/client/vue.mjs.map +1 -1
  38. package/dist/{events-BP6WyRNh.d.mts → events-BgeztGYZ.d.mts} +12 -1
  39. package/dist/{events-BP6WyRNh.d.ts → events-BgeztGYZ.d.ts} +12 -1
  40. package/dist/index.d.mts +4 -4
  41. package/dist/index.d.ts +4 -4
  42. package/dist/index.js +779 -248
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +775 -245
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/{multi-session-client-DMF3ED2O.d.mts → multi-session-client-CxogNckF.d.mts} +1 -1
  47. package/dist/{multi-session-client-BOFgPypS.d.ts → multi-session-client-cox_WXUj.d.ts} +1 -1
  48. package/dist/server/index.d.mts +44 -40
  49. package/dist/server/index.d.ts +44 -40
  50. package/dist/server/index.js +242 -116
  51. package/dist/server/index.js.map +1 -1
  52. package/dist/server/index.mjs +238 -112
  53. package/dist/server/index.mjs.map +1 -1
  54. package/dist/shared/index.d.mts +2 -2
  55. package/dist/shared/index.d.ts +2 -2
  56. package/dist/shared/index.js.map +1 -1
  57. package/dist/shared/index.mjs.map +1 -1
  58. package/dist/{types-SbDlA2VX.d.mts → types-CLccx9wW.d.mts} +1 -1
  59. package/dist/{types-SbDlA2VX.d.ts → types-CLccx9wW.d.ts} +1 -1
  60. package/package.json +8 -1
  61. package/src/adapters/agui-adapter.ts +121 -107
  62. package/src/adapters/agui-middleware.ts +474 -512
  63. package/src/client/core/app-host.ts +417 -0
  64. package/src/client/core/sse-client.ts +365 -212
  65. package/src/client/core/types.ts +31 -0
  66. package/src/client/index.ts +1 -0
  67. package/src/client/react/index.ts +1 -0
  68. package/src/client/react/use-mcp-app.ts +73 -0
  69. package/src/client/react/useMcp.ts +18 -0
  70. package/src/server/handlers/nextjs-handler.ts +8 -7
  71. package/src/server/handlers/sse-handler.ts +131 -164
  72. package/src/server/mcp/oauth-client.ts +32 -2
  73. package/src/server/storage/index.ts +17 -1
  74. package/src/server/storage/sqlite-backend.ts +185 -0
  75. package/src/server/storage/types.ts +1 -1
  76. package/src/shared/events.ts +12 -0
  77. package/src/shared/types.ts +4 -2
@@ -1,512 +1,474 @@
1
- /**
2
- * AG-UI Middleware for MCP Tool Execution
3
- *
4
- * This middleware intercepts tool calls from remote agents (e.g., LangGraph, AutoGen)
5
- * and executes MCP tools server-side, returning results back to the agent.
6
- *
7
- * ## How It Works
8
- *
9
- * 1. **Tool Injection**: When a run starts, the middleware injects MCP tool definitions
10
- * into `input.tools` so the remote agent knows about available MCP tools.
11
- *
12
- * 2. **Event Interception**: The middleware subscribes to the agent's event stream and
13
- * tracks tool calls using AG-UI events:
14
- * - `TOOL_CALL_START`: Records tool name and ID
15
- * - `TOOL_CALL_ARGS`: Accumulates streamed arguments
16
- * - `TOOL_CALL_END`: Marks tool call as complete
17
- * - `RUN_FINISHED`: Triggers execution of pending MCP tools
18
- *
19
- * 3. **Server-Side Execution**: When `RUN_FINISHED` arrives with pending MCP tool calls,
20
- * the middleware:
21
- * - Executes each MCP tool via the MCP client
22
- * - Emits `TOOL_CALL_RESULT` events with the results
23
- * - Adds results to `input.messages` for context
24
- * - Emits `RUN_FINISHED` to close the current run
25
- * - Triggers a new run so the agent can process tool results
26
- *
27
- * 4. **Recursive Processing**: If the new run makes more MCP tool calls, the cycle
28
- * repeats until the agent completes without pending MCP calls.
29
- *
30
- * ## Tool Identification
31
- *
32
- * MCP tools are identified by a configurable prefix (default: `server-`).
33
- * Tools not matching this prefix are passed through without interception.
34
- *
35
- * @requires @ag-ui/client - This middleware requires @ag-ui/client as a peer dependency
36
- * @requires rxjs - Uses RxJS Observables for event streaming
37
- *
38
- * @example
39
- * ```typescript
40
- * import { HttpAgent } from '@ag-ui/client';
41
- * import { McpMiddleware } from '@mcp-ts/sdk/adapters/agui-middleware';
42
- * import { AguiAdapter } from '@mcp-ts/sdk/adapters/agui-adapter';
43
- *
44
- * // Create MCP client and adapter
45
- * const mcpClient = new MultiSessionClient('user_123');
46
- * await mcpClient.connect();
47
- *
48
- * const adapter = new AguiAdapter(mcpClient);
49
- * const actions = await adapter.getActions();
50
- *
51
- * // Create middleware with pre-loaded actions
52
- * const middleware = new McpMiddleware({
53
- * client: mcpClient,
54
- * actions,
55
- * toolPrefix: 'server-',
56
- * });
57
- *
58
- * // Use with HttpAgent
59
- * const agent = new HttpAgent({ url: 'http://localhost:8000/agent' });
60
- * agent.use(middleware);
61
- * ```
62
- */
63
-
64
- import { Observable, Subscriber } from 'rxjs';
65
- import {
66
- Middleware,
67
- EventType,
68
- type AbstractAgent,
69
- type RunAgentInput,
70
- type BaseEvent,
71
- type ToolCallEndEvent,
72
- } from '@ag-ui/client';
73
- import { MCPClient } from '../server/mcp/oauth-client.js';
74
- import { MultiSessionClient } from '../server/mcp/multi-session-client.js';
75
- import type { AguiTool } from './agui-adapter.js';
76
-
77
- /**
78
- * Tool definition format for AG-UI input.tools
79
- */
80
- export interface AgUiTool {
81
- name: string;
82
- description: string;
83
- parameters?: Record<string, any>;
84
- }
85
-
86
- /**
87
- * Configuration for McpMiddleware
88
- */
89
- export interface McpMiddlewareConfig {
90
- /**
91
- * MCP client or MultiSessionClient for executing tools
92
- */
93
- client: MCPClient | MultiSessionClient;
94
-
95
- /**
96
- * Prefix used to identify MCP tool names.
97
- * Tools starting with this prefix will be executed server-side.
98
- * @default 'server-'
99
- */
100
- toolPrefix?: string;
101
-
102
- /**
103
- * Pre-loaded tools with handlers for execution.
104
- * If not provided, tools will be loaded from the MCP client on first use.
105
- */
106
- tools?: AguiTool[];
107
- }
108
-
109
- /**
110
- * AG-UI Middleware that executes MCP tools server-side.
111
- *
112
- * This middleware intercepts tool calls for MCP tools (identified by prefix),
113
- * executes them via the MCP client, and returns results to the agent.
114
- *
115
- * @see {@link createMcpMiddleware} for a simpler factory function
116
- */
117
- export class McpMiddleware extends Middleware {
118
- private client: MCPClient | MultiSessionClient;
119
- private toolPrefix: string;
120
- private actions: AguiTool[] | null;
121
- private tools: AgUiTool[] | null;
122
- private actionsLoaded: boolean = false;
123
-
124
- constructor(config: McpMiddlewareConfig) {
125
- super();
126
- this.client = config.client;
127
- this.toolPrefix = config.toolPrefix ?? 'server-';
128
- this.actions = config.tools ?? null;
129
- this.tools = null;
130
- if (this.actions) {
131
- this.actionsLoaded = true;
132
- this.tools = this.actionsToTools(this.actions);
133
- }
134
- }
135
-
136
- /**
137
- * Convert actions to AG-UI tool format
138
- */
139
- private actionsToTools(actions: AguiTool[]): AgUiTool[] {
140
- return actions.map(action => ({
141
- name: action.name,
142
- description: action.description,
143
- parameters: action.parameters || { type: 'object', properties: {} },
144
- }));
145
- }
146
-
147
- /**
148
- * Check if a tool name is an MCP tool (matches the configured prefix)
149
- */
150
- private isMcpTool(toolName: string): boolean {
151
- return toolName.startsWith(this.toolPrefix);
152
- }
153
-
154
- /**
155
- * Load actions from the MCP client if not already loaded
156
- */
157
- private async ensureActionsLoaded(): Promise<void> {
158
- if (this.actionsLoaded) return;
159
-
160
- const { AguiAdapter } = await import('./agui-adapter.js');
161
- const adapter = new AguiAdapter(this.client);
162
- this.actions = await adapter.getTools();
163
- this.actionsLoaded = true;
164
- }
165
-
166
- /**
167
- * Execute an MCP tool and return the result as a string
168
- */
169
- private async executeTool(toolName: string, args: Record<string, any>): Promise<string> {
170
- await this.ensureActionsLoaded();
171
-
172
- const action = this.actions?.find(a => a.name === toolName);
173
- if (!action) {
174
- return `Error: Tool not found: ${toolName}`;
175
- }
176
-
177
- if (!action.handler) {
178
- return `Error: Tool has no handler: ${toolName}`;
179
- }
180
-
181
- try {
182
- console.log(`[McpMiddleware] Executing tool: ${toolName}`, args);
183
- const result = await action.handler(args);
184
- console.log(`[McpMiddleware] Tool result:`, typeof result === 'string' ? result.slice(0, 200) : result);
185
- return typeof result === 'string' ? result : JSON.stringify(result);
186
- } catch (error: any) {
187
- console.error(`[McpMiddleware] Error executing tool:`, error);
188
- return `Error executing tool: ${error.message || String(error)}`;
189
- }
190
- }
191
-
192
- /**
193
- * Generate a unique message ID for tool results
194
- */
195
- private generateMessageId(): string {
196
- return `mcp_result_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
197
- }
198
-
199
- /**
200
- * Run the middleware, intercepting and executing MCP tool calls
201
- */
202
- run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
203
- return new Observable<BaseEvent>((observer: Subscriber<BaseEvent>) => {
204
- // State for this run
205
- const toolCallArgsBuffer = new Map<string, string>();
206
- const toolCallNames = new Map<string, string>();
207
- const pendingMcpCalls = new Set<string>();
208
-
209
- console.log(`[McpMiddleware] Starting run with ${this.actions?.length ?? 0} registered actions`);
210
- console.log(`[McpMiddleware] Tool prefix: "${this.toolPrefix}"`);
211
-
212
- // Inject MCP tools into input.tools
213
- if (this.tools && this.tools.length > 0) {
214
- const existingTools = input.tools || [];
215
- input.tools = [...existingTools, ...this.tools];
216
- console.log(`[McpMiddleware] Injected ${this.tools.length} MCP tools into input.tools`);
217
- console.log(`[McpMiddleware] Total tools: ${input.tools.length}`);
218
- console.log(`[McpMiddleware] Tool names:`, this.tools.map(t => t.name));
219
- }
220
-
221
- const handleRunFinished = async (event: BaseEvent) => {
222
- if (pendingMcpCalls.size === 0) {
223
- observer.next(event);
224
- observer.complete();
225
- return;
226
- }
227
-
228
- console.log(`[McpMiddleware] RUN_FINISHED received with ${pendingMcpCalls.size} pending MCP calls`);
229
-
230
- // Execute all pending MCP tool calls
231
- const callPromises = [...pendingMcpCalls].map(async (toolCallId) => {
232
- const toolName = toolCallNames.get(toolCallId);
233
- if (!toolName) return;
234
-
235
- const argsString = toolCallArgsBuffer.get(toolCallId) || '{}';
236
- let args: Record<string, any> = {};
237
- try {
238
- args = JSON.parse(argsString);
239
- } catch (e) {
240
- console.error(`[McpMiddleware] Failed to parse args:`, argsString);
241
- }
242
-
243
- console.log(`[McpMiddleware] Executing pending tool: ${toolName}`);
244
- const result = await this.executeTool(toolName, args);
245
- const messageId = this.generateMessageId();
246
-
247
- // Emit tool result event
248
- const resultEvent: BaseEvent = {
249
- type: EventType.TOOL_CALL_RESULT,
250
- toolCallId,
251
- messageId,
252
- content: result,
253
- role: 'tool',
254
- timestamp: Date.now(),
255
- } as any;
256
-
257
- console.log(`[McpMiddleware] Emitting TOOL_CALL_RESULT for: ${toolName}`);
258
- observer.next(resultEvent);
259
-
260
- // Add tool result to messages for the next run
261
- input.messages.push({
262
- id: messageId,
263
- role: 'tool',
264
- toolCallId,
265
- content: result,
266
- } as any);
267
-
268
- pendingMcpCalls.delete(toolCallId);
269
- });
270
-
271
- await Promise.all(callPromises);
272
-
273
- // Emit RUN_FINISHED before starting new run
274
- console.log(`[McpMiddleware] All MCP tools executed, emitting RUN_FINISHED`);
275
- observer.next({
276
- type: EventType.RUN_FINISHED,
277
- threadId: (input as any).threadId,
278
- runId: (input as any).runId,
279
- timestamp: Date.now(),
280
- } as any);
281
-
282
- // Trigger a new run to continue the conversation
283
- console.log(`[McpMiddleware] Triggering new run`);
284
- this.triggerNewRun(observer, input, next, toolCallArgsBuffer, toolCallNames, pendingMcpCalls);
285
- };
286
-
287
- const subscription = next.run(input).subscribe({
288
- next: (event: BaseEvent) => {
289
- // Track tool call names from TOOL_CALL_START events
290
- if (event.type === EventType.TOOL_CALL_START) {
291
- const startEvent = event as any;
292
- if (startEvent.toolCallId && startEvent.toolCallName) {
293
- toolCallNames.set(startEvent.toolCallId, startEvent.toolCallName);
294
- const isMcp = this.isMcpTool(startEvent.toolCallName);
295
- console.log(`[McpMiddleware] TOOL_CALL_START: ${startEvent.toolCallName} (id: ${startEvent.toolCallId}, isMCP: ${isMcp})`);
296
-
297
- if (isMcp) {
298
- pendingMcpCalls.add(startEvent.toolCallId);
299
- }
300
- }
301
- }
302
-
303
- // Accumulate tool call arguments from TOOL_CALL_ARGS events
304
- if (event.type === EventType.TOOL_CALL_ARGS) {
305
- const argsEvent = event as any;
306
- if (argsEvent.toolCallId && argsEvent.delta) {
307
- const existing = toolCallArgsBuffer.get(argsEvent.toolCallId) || '';
308
- toolCallArgsBuffer.set(argsEvent.toolCallId, existing + argsEvent.delta);
309
- }
310
- }
311
-
312
- // Track TOOL_CALL_END
313
- if (event.type === EventType.TOOL_CALL_END) {
314
- const endEvent = event as ToolCallEndEvent;
315
- const toolName = toolCallNames.get(endEvent.toolCallId);
316
- console.log(`[McpMiddleware] TOOL_CALL_END: ${toolName ?? 'unknown'} (id: ${endEvent.toolCallId})`);
317
- }
318
-
319
- // Handle RUN_FINISHED - execute pending MCP tools
320
- if (event.type === EventType.RUN_FINISHED) {
321
- handleRunFinished(event);
322
- return;
323
- }
324
-
325
- // Pass through all other events
326
- observer.next(event);
327
- },
328
- error: (error) => {
329
- observer.error(error);
330
- },
331
- complete: () => {
332
- if (pendingMcpCalls.size === 0) {
333
- observer.complete();
334
- }
335
- },
336
- });
337
-
338
- return () => {
339
- subscription.unsubscribe();
340
- };
341
- });
342
- }
343
-
344
- private triggerNewRun(
345
- observer: Subscriber<BaseEvent>,
346
- input: RunAgentInput,
347
- next: AbstractAgent,
348
- toolCallArgsBuffer: Map<string, string>,
349
- toolCallNames: Map<string, string>,
350
- pendingMcpCalls: Set<string>,
351
- ): void {
352
- toolCallArgsBuffer.clear();
353
- toolCallNames.clear();
354
- pendingMcpCalls.clear();
355
-
356
- console.log(`[McpMiddleware] Starting new run with updated messages`);
357
-
358
- const subscription = next.run(input).subscribe({
359
- next: (event: BaseEvent) => {
360
- if (event.type === EventType.TOOL_CALL_START) {
361
- const startEvent = event as any;
362
- if (startEvent.toolCallId && startEvent.toolCallName) {
363
- toolCallNames.set(startEvent.toolCallId, startEvent.toolCallName);
364
- const isMcp = this.isMcpTool(startEvent.toolCallName);
365
- console.log(`[McpMiddleware] TOOL_CALL_START: ${startEvent.toolCallName} (id: ${startEvent.toolCallId}, isMCP: ${isMcp})`);
366
-
367
- if (isMcp) {
368
- pendingMcpCalls.add(startEvent.toolCallId);
369
- }
370
- }
371
- }
372
-
373
- if (event.type === EventType.TOOL_CALL_ARGS) {
374
- const argsEvent = event as any;
375
- if (argsEvent.toolCallId && argsEvent.delta) {
376
- const existing = toolCallArgsBuffer.get(argsEvent.toolCallId) || '';
377
- toolCallArgsBuffer.set(argsEvent.toolCallId, existing + argsEvent.delta);
378
- }
379
- }
380
-
381
- if (event.type === EventType.TOOL_CALL_END) {
382
- const endEvent = event as ToolCallEndEvent;
383
- const toolName = toolCallNames.get(endEvent.toolCallId);
384
- console.log(`[McpMiddleware] TOOL_CALL_END: ${toolName ?? 'unknown'} (id: ${endEvent.toolCallId})`);
385
- }
386
-
387
- if (event.type === EventType.RUN_FINISHED) {
388
- if (pendingMcpCalls.size > 0) {
389
- console.log(`[McpMiddleware] RUN_FINISHED with ${pendingMcpCalls.size} pending calls, executing...`);
390
- this.handlePendingCalls(observer, input, next, toolCallArgsBuffer, toolCallNames, pendingMcpCalls);
391
- } else {
392
- observer.next(event);
393
- observer.complete();
394
- }
395
- return;
396
- }
397
-
398
- observer.next(event);
399
- },
400
- error: (error) => observer.error(error),
401
- complete: () => {
402
- if (pendingMcpCalls.size === 0) {
403
- observer.complete();
404
- }
405
- },
406
- });
407
- }
408
-
409
- private async handlePendingCalls(
410
- observer: Subscriber<BaseEvent>,
411
- input: RunAgentInput,
412
- next: AbstractAgent,
413
- toolCallArgsBuffer: Map<string, string>,
414
- toolCallNames: Map<string, string>,
415
- pendingMcpCalls: Set<string>,
416
- ): Promise<void> {
417
- const callPromises = [...pendingMcpCalls].map(async (toolCallId) => {
418
- const toolName = toolCallNames.get(toolCallId);
419
- if (!toolName) return;
420
-
421
- const argsString = toolCallArgsBuffer.get(toolCallId) || '{}';
422
- let args: Record<string, any> = {};
423
- try {
424
- args = JSON.parse(argsString);
425
- } catch (e) {
426
- console.error(`[McpMiddleware] Failed to parse args:`, argsString);
427
- }
428
-
429
- console.log(`[McpMiddleware] Executing pending tool: ${toolName}`);
430
- const result = await this.executeTool(toolName, args);
431
- const messageId = this.generateMessageId();
432
-
433
- const resultEvent: BaseEvent = {
434
- type: EventType.TOOL_CALL_RESULT,
435
- toolCallId,
436
- messageId,
437
- content: result,
438
- role: 'tool',
439
- timestamp: Date.now(),
440
- } as any;
441
-
442
- console.log(`[McpMiddleware] Emitting TOOL_CALL_RESULT for: ${toolName}`);
443
- observer.next(resultEvent);
444
-
445
- input.messages.push({
446
- id: messageId,
447
- role: 'tool',
448
- toolCallId,
449
- content: result,
450
- } as any);
451
-
452
- pendingMcpCalls.delete(toolCallId);
453
- });
454
-
455
- await Promise.all(callPromises);
456
-
457
- console.log(`[McpMiddleware] Pending tools executed, emitting RUN_FINISHED`);
458
- observer.next({
459
- type: EventType.RUN_FINISHED,
460
- threadId: (input as any).threadId,
461
- runId: (input as any).runId,
462
- timestamp: Date.now(),
463
- } as any);
464
-
465
- console.log(`[McpMiddleware] Triggering new run`);
466
- this.triggerNewRun(observer, input, next, toolCallArgsBuffer, toolCallNames, pendingMcpCalls);
467
- }
468
- }
469
-
470
- /**
471
- * Factory function to create MCP middleware.
472
- *
473
- * This is a convenience wrapper around McpMiddleware that returns a function
474
- * compatible with the AG-UI middleware pattern.
475
- *
476
- * @param client - MCP client or MultiSessionClient
477
- * @param options - Configuration options
478
- * @returns Middleware function
479
- *
480
- * @example
481
- * ```typescript
482
- * import { HttpAgent } from '@ag-ui/client';
483
- * import { createMcpMiddleware } from '@mcp-ts/sdk/adapters/agui-middleware';
484
- *
485
- * const agent = new HttpAgent({ url: 'http://localhost:8000/agent' });
486
- * agent.use(createMcpMiddleware(multiSessionClient, {
487
- * toolPrefix: 'server-',
488
- * actions: mcpActions,
489
- * }));
490
- * ```
491
- */
492
- export function createMcpMiddleware(
493
- client: MCPClient | MultiSessionClient,
494
- options: { toolPrefix?: string; tools?: AguiTool[] } = {}
495
- ) {
496
- const middleware = new McpMiddleware({
497
- client,
498
- ...options,
499
- });
500
-
501
- return (input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> => {
502
- return middleware.run(input, next);
503
- };
504
- }
505
-
506
- // Legacy exports for backward compatibility
507
- export { McpMiddleware as McpToolExecutorMiddleware };
508
- export { createMcpMiddleware as createMcpToolMiddleware };
509
-
510
- // Re-export types for convenience
511
- export { Middleware, EventType };
512
- export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent };
1
+ /**
2
+ * AG-UI Middleware for MCP Tool Execution
3
+ *
4
+ * This middleware intercepts tool calls from remote agents and executes
5
+ * MCP tools server-side, returning results back to the agent.
6
+ *
7
+ * @requires @ag-ui/client - Peer dependency for AG-UI types
8
+ * @requires rxjs - Uses RxJS Observables for event streaming
9
+ */
10
+
11
+ import { Observable, Subscriber } from 'rxjs';
12
+ import {
13
+ Middleware,
14
+ EventType,
15
+ type AbstractAgent,
16
+ type RunAgentInput,
17
+ type BaseEvent,
18
+ type ToolCallEndEvent,
19
+ type Tool,
20
+ } from '@ag-ui/client';
21
+ import { type AguiTool, cleanSchema } from './agui-adapter.js';
22
+
23
+ /** New event type for MCP UI triggers */
24
+ export const MCP_APP_UI_EVENT = 'mcp-apps-ui';
25
+ /**
26
+ * MCP Apps UI trigger event.
27
+ *
28
+ * IMPORTANT: This must be emitted as an AG-UI CustomEvent so subscribers
29
+ * (e.g. CopilotKit `onCustomEvent`) can receive it.
30
+ */
31
+ export interface McpAppUiEventPayload {
32
+ toolCallId: string;
33
+ resourceUri: string;
34
+ sessionId?: string;
35
+ toolName: string;
36
+ result?: any;
37
+ }
38
+
39
+ /** Tool execution result for continuation */
40
+ interface ToolResult {
41
+ toolCallId: string;
42
+ toolName: string;
43
+ result: string;
44
+ /**
45
+ * Raw result object (if available).
46
+ * Used to preserve metadata (e.g. `_meta`) that is lost in the stringified `result`.
47
+ */
48
+ rawResult?: any;
49
+ messageId: string;
50
+ }
51
+
52
+ /** State for tracking tool calls during a run */
53
+ interface RunState {
54
+ toolCallArgsBuffer: Map<string, string>;
55
+ toolCallNames: Map<string, string>;
56
+ pendingMcpCalls: Set<string>;
57
+ textContent?: string;
58
+ error: boolean;
59
+ }
60
+
61
+ /**
62
+ * Configuration for McpMiddleware
63
+ */
64
+ export interface McpMiddlewareConfig {
65
+ /** Pre-loaded tools with handlers (required) */
66
+ tools: AguiTool[];
67
+ }
68
+
69
+ /**
70
+ * AG-UI Middleware that executes MCP tools server-side.
71
+ */
72
+ export class McpMiddleware extends Middleware {
73
+ private tools: AguiTool[];
74
+ private toolSchemas: Tool[];
75
+
76
+ constructor(config: McpMiddlewareConfig) {
77
+ super();
78
+ this.tools = config.tools;
79
+ this.toolSchemas = this.tools.map((t: AguiTool) => ({
80
+ name: t.name,
81
+ description: t.description,
82
+ parameters: cleanSchema(t.parameters),
83
+ _meta: t._meta, // Include _meta in the tool definition passed to the agent
84
+ }));
85
+ }
86
+
87
+ private isMcpTool(toolName: string): boolean {
88
+ return this.tools.some(t => t.name === toolName);
89
+ }
90
+
91
+ private parseArgs(argsString: string): Record<string, any> {
92
+ if (!argsString?.trim()) return {};
93
+
94
+ try {
95
+ return JSON.parse(argsString);
96
+ } catch {
97
+ // Handle duplicated JSON from streaming issues: {...}{...}
98
+ const trimmed = argsString.trim();
99
+ if (trimmed.includes('}{')) {
100
+ const firstObject = trimmed.slice(0, trimmed.indexOf('}{') + 1);
101
+ try {
102
+ return JSON.parse(firstObject);
103
+ } catch {
104
+ console.error(`[McpMiddleware] Failed to parse JSON:`, firstObject);
105
+ }
106
+ }
107
+ console.error(`[McpMiddleware] Failed to parse args:`, argsString);
108
+ return {};
109
+ }
110
+ }
111
+
112
+ private async executeTool(toolName: string, args: Record<string, any>): Promise<{ resultStr: string, rawResult?: any }> {
113
+ const tool = this.tools.find(t => t.name === toolName);
114
+ if (!tool?.handler) {
115
+ return { resultStr: `Error: Tool ${tool ? 'has no handler' : 'not found'}: ${toolName}` };
116
+ }
117
+
118
+ try {
119
+ console.log(`[McpMiddleware] Executing tool: ${toolName}`, args);
120
+ // Result can be a string (legacy) or an object (MCP Result with content array)
121
+ const result = await tool.handler(args);
122
+
123
+ let resultStr: string;
124
+
125
+ if (typeof result === 'string') {
126
+ resultStr = result;
127
+ } else if (result && typeof result === 'object') {
128
+ // Determine if we should preserve the object structure (e.g. for MCP Tool Results)
129
+ resultStr = JSON.stringify(result);
130
+ } else {
131
+ resultStr = String(result);
132
+ }
133
+
134
+ console.log(`[McpMiddleware] Tool result:`, resultStr.slice(0, 200));
135
+ return { resultStr, rawResult: result };
136
+ } catch (error: any) {
137
+ console.error(`[McpMiddleware] Error executing tool:`, error);
138
+ return { resultStr: `Error: ${error.message || String(error)}` };
139
+ }
140
+ }
141
+
142
+ private generateId(prefix: string): string {
143
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
144
+ }
145
+
146
+ private ensureIds(input: RunAgentInput): void {
147
+ const anyInput = input as any;
148
+ if (!anyInput.threadId) anyInput.threadId = this.generateId('mcp_thread');
149
+ if (!anyInput.runId) anyInput.runId = this.generateId('mcp_run');
150
+ }
151
+
152
+ /** Process tool call events and update state */
153
+ private handleToolCallEvent(event: BaseEvent, state: RunState): void {
154
+ const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls } = state;
155
+
156
+ // Accumulate text content for reconstruction
157
+ if (event.type === EventType.TEXT_MESSAGE_CHUNK) {
158
+ const e = event as any;
159
+ if (e.delta) {
160
+ state.textContent = (state.textContent || '') + e.delta;
161
+ }
162
+ }
163
+
164
+ if (event.type === EventType.TOOL_CALL_START) {
165
+ const e = event as any;
166
+ if (e.toolCallId && e.toolCallName) {
167
+ toolCallNames.set(e.toolCallId, e.toolCallName);
168
+ if (this.isMcpTool(e.toolCallName)) {
169
+ pendingMcpCalls.add(e.toolCallId);
170
+ }
171
+ console.log(`[McpMiddleware] TOOL_CALL_START: ${e.toolCallName} (id: ${e.toolCallId}, isMCP: ${this.isMcpTool(e.toolCallName)})`);
172
+ }
173
+ }
174
+
175
+ if (event.type === EventType.TOOL_CALL_ARGS) {
176
+ const e = event as any;
177
+ if (e.toolCallId && e.delta) {
178
+ const existing = toolCallArgsBuffer.get(e.toolCallId) || '';
179
+ toolCallArgsBuffer.set(e.toolCallId, existing + e.delta);
180
+ }
181
+ }
182
+
183
+ if (event.type === EventType.TOOL_CALL_END) {
184
+ const e = event as ToolCallEndEvent;
185
+ console.log(`[McpMiddleware] TOOL_CALL_END: ${toolCallNames.get(e.toolCallId) ?? 'unknown'} (id: ${e.toolCallId})`);
186
+ }
187
+
188
+ // Workaround: Extract parallel tool calls from MESSAGES_SNAPSHOT
189
+ if (event.type === EventType.MESSAGES_SNAPSHOT) {
190
+ const messages = (event as any).messages || [];
191
+ if (messages.length > 0) {
192
+ const lastMsg = messages[messages.length - 1];
193
+ // Update text content from snapshot if available (often more reliable)
194
+ if (lastMsg.role === 'assistant' && lastMsg.content) {
195
+ state.textContent = lastMsg.content;
196
+ }
197
+
198
+ // Discover tools
199
+ for (let i = messages.length - 1; i >= 0; i--) {
200
+ const msg = messages[i];
201
+ const tools = Array.isArray(msg.toolCalls) ? msg.toolCalls :
202
+ (Array.isArray(msg.tool_calls) ? msg.tool_calls : []);
203
+
204
+ if (msg.role === 'assistant' && tools.length > 0) {
205
+ for (const tc of tools) {
206
+ if (tc.id && tc.function?.name && !toolCallNames.has(tc.id)) {
207
+ toolCallNames.set(tc.id, tc.function.name);
208
+ toolCallArgsBuffer.set(tc.id, tc.function.arguments || '{}');
209
+ if (this.isMcpTool(tc.function.name)) {
210
+ pendingMcpCalls.add(tc.id);
211
+ console.log(`[McpMiddleware] MESSAGES_SNAPSHOT: Discovered ${tc.function.name} (id: ${tc.id})`);
212
+ }
213
+ }
214
+ }
215
+ break;
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ /** Execute pending MCP tools and return results */
223
+ private async executeTools(state: RunState): Promise<ToolResult[]> {
224
+ const { toolCallArgsBuffer, toolCallNames, pendingMcpCalls } = state;
225
+ const results: ToolResult[] = [];
226
+
227
+ const promises = [...pendingMcpCalls].map(async (toolCallId) => {
228
+ const toolName = toolCallNames.get(toolCallId);
229
+ if (!toolName) return;
230
+
231
+ const args = this.parseArgs(toolCallArgsBuffer.get(toolCallId) || '{}');
232
+ console.log(`[McpMiddleware] Executing pending tool: ${toolName}`);
233
+
234
+ const { resultStr, rawResult } = await this.executeTool(toolName, args);
235
+ results.push({
236
+ toolCallId,
237
+ toolName,
238
+ result: resultStr,
239
+ rawResult,
240
+ messageId: this.generateId('mcp_result'),
241
+ });
242
+ pendingMcpCalls.delete(toolCallId);
243
+ });
244
+
245
+ await Promise.all(promises);
246
+ return results;
247
+ }
248
+
249
+ private emitToolResults(observer: Subscriber<BaseEvent>, results: ToolResult[]): void {
250
+ for (const { toolCallId, toolName, result, rawResult, messageId } of results) {
251
+ // UI metadata may appear either on the tool CALL result (rawResult._meta)
252
+ // or only on the tool DEFINITION (listTools result). We support both.
253
+ const toolDef = this.tools.find(t => t.name === toolName);
254
+ const sessionId = toolDef?._meta?.sessionId;
255
+ const resourceUri =
256
+ rawResult?._meta?.ui?.resourceUri ??
257
+ rawResult?._meta?.['ui/resourceUri'] ??
258
+ toolDef?._meta?.ui?.resourceUri ??
259
+ toolDef?._meta?.['ui/resourceUri'];
260
+
261
+ if (resourceUri) {
262
+ const payload: McpAppUiEventPayload = {
263
+ toolCallId,
264
+ resourceUri,
265
+ sessionId,
266
+ toolName,
267
+ result: rawResult ?? result,
268
+ };
269
+
270
+ observer.next({
271
+ type: EventType.CUSTOM,
272
+ name: MCP_APP_UI_EVENT,
273
+ value: payload,
274
+ timestamp: Date.now(),
275
+ role: 'tool',
276
+ } as any);
277
+
278
+ console.log(`[McpMiddleware] Emitting CustomEvent(${MCP_APP_UI_EVENT}) for: ${toolName} (session: ${sessionId})`);
279
+ }
280
+
281
+ observer.next({
282
+ type: EventType.TOOL_CALL_RESULT,
283
+ toolCallId,
284
+ messageId,
285
+ content: result,
286
+ role: 'tool',
287
+ timestamp: Date.now(),
288
+ } as any);
289
+ console.log(`[McpMiddleware] Emitting TOOL_CALL_RESULT for: ${toolName}`);
290
+ }
291
+ }
292
+
293
+ run(input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> {
294
+ return new Observable<BaseEvent>((observer: Subscriber<BaseEvent>) => {
295
+ const state: RunState = {
296
+ toolCallArgsBuffer: new Map(),
297
+ toolCallNames: new Map(),
298
+ pendingMcpCalls: new Set(),
299
+ textContent: '',
300
+ error: false,
301
+ };
302
+
303
+ this.ensureIds(input);
304
+ const anyInput = input as any;
305
+
306
+ console.log(`[McpMiddleware] === NEW RUN ===`);
307
+ console.log(`[McpMiddleware] threadId: ${anyInput.threadId}, runId: ${anyInput.runId}`);
308
+ console.log(`[McpMiddleware] messages: ${input.messages?.length ?? 0}, tools: ${this.tools?.length ?? 0}`);
309
+
310
+ // Inject MCP tools
311
+ if (this.toolSchemas?.length) {
312
+ input.tools = [...(input.tools || []), ...this.toolSchemas];
313
+ console.log(`[McpMiddleware] Injected ${this.toolSchemas.length} tools:`, this.toolSchemas.map((t: Tool) => t.name));
314
+ }
315
+
316
+ const handleRunFinished = async () => {
317
+ if (state.error) return; // Don't continue after error
318
+
319
+ if (state.pendingMcpCalls.size === 0) {
320
+ observer.next({
321
+ type: EventType.RUN_FINISHED,
322
+ threadId: anyInput.threadId,
323
+ runId: anyInput.runId,
324
+ timestamp: Date.now(),
325
+ } as any);
326
+ observer.complete();
327
+ return;
328
+ }
329
+
330
+ console.log(`[McpMiddleware] RUN_FINISHED with ${state.pendingMcpCalls.size} pending calls`);
331
+
332
+ // Reconstruct the Assistant Message that triggered these tools
333
+ const toolCalls = [];
334
+ for (const toolCallId of state.pendingMcpCalls) {
335
+ const name = state.toolCallNames.get(toolCallId);
336
+ const args = state.toolCallArgsBuffer.get(toolCallId) || '{}';
337
+ if (name) {
338
+ toolCalls.push({
339
+ id: toolCallId,
340
+ type: 'function',
341
+ function: { name, arguments: args }
342
+ });
343
+ }
344
+ }
345
+
346
+ // Add the Assistant Message to history FIRST
347
+ if (toolCalls.length > 0 || state.textContent) {
348
+ const assistantMsg = {
349
+ id: this.generateId('msg_ast'),
350
+ role: 'assistant',
351
+ content: state.textContent || null, // Ensure null if empty string for strict LLMs
352
+ tool_calls: toolCalls.length > 0 ? toolCalls : undefined
353
+ };
354
+ input.messages.push(assistantMsg as any);
355
+ console.log(`[McpMiddleware] Added assistant message to history before tools: ${state.textContent?.slice(0, 50)}... [${toolCalls.length} tools]`);
356
+ }
357
+
358
+ // Execute tools and emit results (no RUN_FINISHED yet - continuation follows)
359
+ const results = await this.executeTools(state);
360
+ this.emitToolResults(observer, results);
361
+
362
+ // Prepare continuation
363
+ console.log(`[McpMiddleware] Triggering continuation with ${results.length} results`);
364
+
365
+ // Add tool result messages to history
366
+ for (const { toolCallId, result, messageId } of results) {
367
+ input.messages.push({
368
+ id: messageId,
369
+ role: 'tool',
370
+ tool_call_id: toolCallId,
371
+ content: result,
372
+ } as any);
373
+ }
374
+
375
+ // Reset state for next turn
376
+ state.toolCallArgsBuffer.clear();
377
+ state.toolCallNames.clear();
378
+ state.textContent = ''; // Clear text content for next turn
379
+
380
+ anyInput.runId = this.generateId('mcp_run');
381
+ console.log(`[McpMiddleware] === CONTINUATION RUN === messages: ${input.messages.length}`);
382
+
383
+ // Subscribe to continuation
384
+ next.run(input).subscribe({
385
+ next: (event) => {
386
+ if (state.error) return;
387
+
388
+ this.handleToolCallEvent(event, state);
389
+
390
+ if (event.type === EventType.RUN_ERROR) {
391
+ console.log(`[McpMiddleware] RUN_ERROR received in continuation`);
392
+ state.error = true;
393
+ observer.next(event);
394
+ observer.complete();
395
+ return;
396
+ }
397
+
398
+ if (event.type === EventType.RUN_STARTED) {
399
+ console.log(`[McpMiddleware] Filtering RUN_STARTED from continuation`);
400
+ return;
401
+ }
402
+
403
+ if (event.type === EventType.RUN_FINISHED) {
404
+ if (state.pendingMcpCalls.size > 0) {
405
+ handleRunFinished();
406
+ } else {
407
+ observer.next(event);
408
+ observer.complete();
409
+ }
410
+ return;
411
+ }
412
+ observer.next(event);
413
+ },
414
+ error: (err) => {
415
+ state.error = true;
416
+ observer.error(err);
417
+ },
418
+ complete: () => {
419
+ if (!state.error && state.pendingMcpCalls.size === 0) observer.complete();
420
+ },
421
+ });
422
+ };
423
+
424
+ const subscription = next.run(input).subscribe({
425
+ next: (event) => {
426
+ if (state.error) return;
427
+
428
+ this.handleToolCallEvent(event, state);
429
+
430
+ if (event.type === EventType.RUN_ERROR) {
431
+ console.log(`[McpMiddleware] RUN_ERROR received`);
432
+ state.error = true;
433
+ observer.next(event);
434
+ observer.complete();
435
+ return;
436
+ }
437
+
438
+ if (event.type === EventType.RUN_FINISHED) {
439
+ handleRunFinished();
440
+ return;
441
+ }
442
+ observer.next(event);
443
+ },
444
+ error: (err) => {
445
+ state.error = true;
446
+ observer.error(err);
447
+ },
448
+ complete: () => {
449
+ if (!state.error && state.pendingMcpCalls.size === 0) observer.complete();
450
+ },
451
+ });
452
+
453
+ return () => subscription.unsubscribe();
454
+ });
455
+ }
456
+ }
457
+
458
+ /**
459
+ * Factory function to create MCP middleware.
460
+ */
461
+ export function createMcpMiddleware(options: { tools: AguiTool[] }) {
462
+ const middleware = new McpMiddleware(options);
463
+ return (input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> => {
464
+ return middleware.run(input, next);
465
+ };
466
+ }
467
+
468
+ // Legacy exports
469
+ export { McpMiddleware as McpToolExecutorMiddleware };
470
+ export { createMcpMiddleware as createMcpToolMiddleware };
471
+
472
+ // Re-exports
473
+ export { Middleware, EventType };
474
+ export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent, Tool };