@mcp-ts/sdk 1.0.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.
- package/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/adapters/agui-adapter.d.mts +119 -0
- package/dist/adapters/agui-adapter.d.ts +119 -0
- package/dist/adapters/agui-adapter.js +109 -0
- package/dist/adapters/agui-adapter.js.map +1 -0
- package/dist/adapters/agui-adapter.mjs +107 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -0
- package/dist/adapters/agui-middleware.d.mts +171 -0
- package/dist/adapters/agui-middleware.d.ts +171 -0
- package/dist/adapters/agui-middleware.js +429 -0
- package/dist/adapters/agui-middleware.js.map +1 -0
- package/dist/adapters/agui-middleware.mjs +417 -0
- package/dist/adapters/agui-middleware.mjs.map +1 -0
- package/dist/adapters/ai-adapter.d.mts +38 -0
- package/dist/adapters/ai-adapter.d.ts +38 -0
- package/dist/adapters/ai-adapter.js +82 -0
- package/dist/adapters/ai-adapter.js.map +1 -0
- package/dist/adapters/ai-adapter.mjs +80 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -0
- package/dist/adapters/langchain-adapter.d.mts +46 -0
- package/dist/adapters/langchain-adapter.d.ts +46 -0
- package/dist/adapters/langchain-adapter.js +102 -0
- package/dist/adapters/langchain-adapter.js.map +1 -0
- package/dist/adapters/langchain-adapter.mjs +100 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -0
- package/dist/adapters/mastra-adapter.d.mts +49 -0
- package/dist/adapters/mastra-adapter.d.ts +49 -0
- package/dist/adapters/mastra-adapter.js +95 -0
- package/dist/adapters/mastra-adapter.js.map +1 -0
- package/dist/adapters/mastra-adapter.mjs +93 -0
- package/dist/adapters/mastra-adapter.mjs.map +1 -0
- package/dist/client/index.d.mts +119 -0
- package/dist/client/index.d.ts +119 -0
- package/dist/client/index.js +225 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/index.mjs +223 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/client/react.d.mts +151 -0
- package/dist/client/react.d.ts +151 -0
- package/dist/client/react.js +492 -0
- package/dist/client/react.js.map +1 -0
- package/dist/client/react.mjs +489 -0
- package/dist/client/react.mjs.map +1 -0
- package/dist/client/vue.d.mts +157 -0
- package/dist/client/vue.d.ts +157 -0
- package/dist/client/vue.js +474 -0
- package/dist/client/vue.js.map +1 -0
- package/dist/client/vue.mjs +471 -0
- package/dist/client/vue.mjs.map +1 -0
- package/dist/events-BP6WyRNh.d.mts +110 -0
- package/dist/events-BP6WyRNh.d.ts +110 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2784 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2723 -0
- package/dist/index.mjs.map +1 -0
- package/dist/multi-session-client-BOFgPypS.d.ts +389 -0
- package/dist/multi-session-client-DMF3ED2O.d.mts +389 -0
- package/dist/server/index.d.mts +269 -0
- package/dist/server/index.d.ts +269 -0
- package/dist/server/index.js +2444 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/index.mjs +2414 -0
- package/dist/server/index.mjs.map +1 -0
- package/dist/shared/index.d.mts +24 -0
- package/dist/shared/index.d.ts +24 -0
- package/dist/shared/index.js +223 -0
- package/dist/shared/index.js.map +1 -0
- package/dist/shared/index.mjs +190 -0
- package/dist/shared/index.mjs.map +1 -0
- package/dist/types-SbDlA2VX.d.mts +153 -0
- package/dist/types-SbDlA2VX.d.ts +153 -0
- package/dist/utils-0qmYrqoa.d.mts +92 -0
- package/dist/utils-0qmYrqoa.d.ts +92 -0
- package/package.json +165 -0
- package/src/adapters/agui-adapter.ts +210 -0
- package/src/adapters/agui-middleware.ts +512 -0
- package/src/adapters/ai-adapter.ts +115 -0
- package/src/adapters/langchain-adapter.ts +127 -0
- package/src/adapters/mastra-adapter.ts +126 -0
- package/src/client/core/sse-client.ts +340 -0
- package/src/client/index.ts +26 -0
- package/src/client/react/index.ts +10 -0
- package/src/client/react/useMcp.ts +558 -0
- package/src/client/vue/index.ts +10 -0
- package/src/client/vue/useMcp.ts +542 -0
- package/src/index.ts +11 -0
- package/src/server/handlers/nextjs-handler.ts +216 -0
- package/src/server/handlers/sse-handler.ts +699 -0
- package/src/server/index.ts +57 -0
- package/src/server/mcp/multi-session-client.ts +132 -0
- package/src/server/mcp/oauth-client.ts +1168 -0
- package/src/server/mcp/storage-oauth-provider.ts +239 -0
- package/src/server/storage/file-backend.ts +169 -0
- package/src/server/storage/index.ts +115 -0
- package/src/server/storage/memory-backend.ts +132 -0
- package/src/server/storage/redis-backend.ts +210 -0
- package/src/server/storage/redis.ts +160 -0
- package/src/server/storage/types.ts +109 -0
- package/src/shared/constants.ts +29 -0
- package/src/shared/errors.ts +133 -0
- package/src/shared/events.ts +166 -0
- package/src/shared/index.ts +70 -0
- package/src/shared/types.ts +274 -0
- package/src/shared/utils.ts +16 -0
|
@@ -0,0 +1,512 @@
|
|
|
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 };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { MCPClient } from '../server/mcp/oauth-client';
|
|
2
|
+
import { MultiSessionClient } from '../server/mcp/multi-session-client';
|
|
3
|
+
import type { JSONSchema7 } from 'json-schema';
|
|
4
|
+
import type { ToolSet } from 'ai';
|
|
5
|
+
|
|
6
|
+
export interface AIAdapterOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Prefix for tool names to avoid collision with other tools.
|
|
9
|
+
* Defaults to the client's serverId.
|
|
10
|
+
*/
|
|
11
|
+
prefix?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Adapter to use MCP tools with the Vercel AI SDK.
|
|
16
|
+
*/
|
|
17
|
+
export class AIAdapter {
|
|
18
|
+
private jsonSchema: typeof import('ai').jsonSchema | undefined;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private client: MCPClient | MultiSessionClient,
|
|
22
|
+
private options: AIAdapterOptions = {}
|
|
23
|
+
) { }
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Lazy-loads the jsonSchema function from the AI SDK.
|
|
29
|
+
*/
|
|
30
|
+
private async ensureJsonSchema() {
|
|
31
|
+
if (!this.jsonSchema) {
|
|
32
|
+
const { jsonSchema } = await import('ai');
|
|
33
|
+
this.jsonSchema = jsonSchema;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private async transformTools(client: MCPClient): Promise<ToolSet> {
|
|
38
|
+
// Safe check for isConnected method (duck typing for bundler compatibility)
|
|
39
|
+
const isConnected = typeof client.isConnected === 'function'
|
|
40
|
+
? client.isConnected()
|
|
41
|
+
: false;
|
|
42
|
+
|
|
43
|
+
if (!isConnected) {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await client.listTools();
|
|
48
|
+
|
|
49
|
+
// @ts-ignore: ToolSet type inference can be tricky with dynamic imports
|
|
50
|
+
return Object.fromEntries(
|
|
51
|
+
result.tools.map((tool) => {
|
|
52
|
+
// Safe access to getServerId
|
|
53
|
+
const serverId = typeof client.getServerId === 'function'
|
|
54
|
+
? client.getServerId()
|
|
55
|
+
: undefined;
|
|
56
|
+
const prefix = this.options.prefix ?? serverId?.replace(/-/g, '') ?? 'mcp';
|
|
57
|
+
return [
|
|
58
|
+
`tool_${prefix}_${tool.name}`,
|
|
59
|
+
{
|
|
60
|
+
description: tool.description,
|
|
61
|
+
inputSchema: this.jsonSchema!(tool.inputSchema as JSONSchema7),
|
|
62
|
+
execute: async (args: any) => {
|
|
63
|
+
try {
|
|
64
|
+
const response = await client.callTool(tool.name, args);
|
|
65
|
+
return response;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
+
throw new Error(`Tool execution failed: ${errorMessage}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Fetches tools from the client(s) and converts them to AI SDK tools.
|
|
79
|
+
*/
|
|
80
|
+
async getTools(): Promise<ToolSet> {
|
|
81
|
+
await this.ensureJsonSchema();
|
|
82
|
+
|
|
83
|
+
// Use duck typing instead of instanceof to handle module bundling issues
|
|
84
|
+
// MultiSessionClient has getClients(), MCPClient does not
|
|
85
|
+
const isMultiSession = typeof (this.client as any).getClients === 'function';
|
|
86
|
+
const clients = isMultiSession
|
|
87
|
+
? (this.client as MultiSessionClient).getClients()
|
|
88
|
+
: [this.client as MCPClient];
|
|
89
|
+
|
|
90
|
+
const results = await Promise.all(
|
|
91
|
+
clients.map(async (client) => {
|
|
92
|
+
try {
|
|
93
|
+
return await this.transformTools(client);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
// For multi-client, we log and continue.
|
|
96
|
+
// This is safer than throwing.
|
|
97
|
+
const serverId = typeof client.getServerId === 'function'
|
|
98
|
+
? client.getServerId() ?? 'unknown'
|
|
99
|
+
: 'unknown';
|
|
100
|
+
console.error(`[AIAdapter] Failed to fetch tools from ${serverId}:`, error);
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return results.reduce((acc, tools) => ({ ...acc, ...tools }), {});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Convenience static method to fetch tools in a single line.
|
|
111
|
+
*/
|
|
112
|
+
static async getTools(client: MCPClient | MultiSessionClient, options: AIAdapterOptions = {}): Promise<ToolSet> {
|
|
113
|
+
return new AIAdapter(client, options).getTools();
|
|
114
|
+
}
|
|
115
|
+
}
|