@mcp-ts/sdk 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -20,32 +20,11 @@ import {
20
20
  } from '@ag-ui/client';
21
21
  import { type AguiTool, cleanSchema } from './agui-adapter.js';
22
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
23
  /** Tool execution result for continuation */
40
24
  interface ToolResult {
41
25
  toolCallId: string;
42
26
  toolName: string;
43
27
  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
28
  messageId: string;
50
29
  }
51
30
 
@@ -58,43 +37,32 @@ interface RunState {
58
37
  error: boolean;
59
38
  }
60
39
 
61
- /**
62
- * Configuration for McpMiddleware
63
- */
64
- export interface McpMiddlewareConfig {
65
- /** Pre-loaded tools with handlers (required) */
66
- tools: AguiTool[];
67
- }
40
+ /**
41
+ * Configuration for McpMiddleware
42
+ */
43
+ export interface McpMiddlewareConfig {
44
+ /** Pre-loaded tools with handlers (required) */
45
+ tools: AguiTool[];
46
+ }
68
47
 
69
48
  /**
70
49
  * AG-UI Middleware that executes MCP tools server-side.
71
50
  */
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
- /**
88
- * Extract base tool name from prefixed format for event emission
89
- * e.g., "tool_abc123_get-time" -> "get-time"
90
- */
91
- private getBaseToolName(toolName: string): string {
92
- const match = toolName.match(/^tool_[^_]+_(.+)$/);
93
- return match ? match[1] : toolName;
94
- }
51
+ export class McpMiddleware extends Middleware {
52
+ private tools: AguiTool[];
53
+ private toolSchemas: Tool[];
54
+
55
+ constructor(config: McpMiddlewareConfig) {
56
+ super();
57
+ this.tools = config.tools;
58
+ this.toolSchemas = this.tools.map((t: AguiTool) => ({
59
+ name: t.name,
60
+ description: t.description,
61
+ parameters: cleanSchema(t.parameters),
62
+ }));
63
+ }
95
64
 
96
65
  private isMcpTool(toolName: string): boolean {
97
- // Direct comparison - tool names should match as-is
98
66
  return this.tools.some(t => t.name === toolName);
99
67
  }
100
68
 
@@ -119,31 +87,22 @@ export class McpMiddleware extends Middleware {
119
87
  }
120
88
  }
121
89
 
122
- private async executeTool(toolName: string, args: Record<string, any>): Promise<{ resultStr: string, rawResult?: any }> {
90
+ private async executeTool(toolName: string, args: Record<string, any>): Promise<string> {
123
91
  const tool = this.tools.find(t => t.name === toolName);
124
92
  if (!tool?.handler) {
125
- return { resultStr: `Error: Tool ${tool ? 'has no handler' : 'not found'}: ${toolName}` };
93
+ return `Error: Tool ${tool ? 'has no handler' : 'not found'}: ${toolName}`;
126
94
  }
127
95
 
128
- try {
129
- // Result can be a string (legacy) or an object (MCP Result with content array)
130
- const result = await tool.handler(args);
131
-
132
- let resultStr: string;
133
-
134
- if (typeof result === 'string') {
135
- resultStr = result;
136
- } else if (result && typeof result === 'object') {
137
- // Determine if we should preserve the object structure (e.g. for MCP Tool Results)
138
- resultStr = JSON.stringify(result);
139
- } else {
140
- resultStr = String(result);
141
- }
142
-
143
- return { resultStr, rawResult: result };
96
+ try {
97
+ console.log(`[McpMiddleware] Executing tool: ${toolName}`, args);
98
+ const result = await tool.handler(args);
99
+ const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
100
+
101
+ console.log(`[McpMiddleware] Tool result:`, resultStr.slice(0, 200));
102
+ return resultStr;
144
103
  } catch (error: any) {
145
104
  console.error(`[McpMiddleware] Error executing tool:`, error);
146
- return { resultStr: `Error: ${error.message || String(error)}` };
105
+ return `Error: ${error.message || String(error)}`;
147
106
  }
148
107
  }
149
108
 
@@ -176,6 +135,7 @@ export class McpMiddleware extends Middleware {
176
135
  if (this.isMcpTool(e.toolCallName)) {
177
136
  pendingMcpCalls.add(e.toolCallId);
178
137
  }
138
+ console.log(`[McpMiddleware] TOOL_CALL_START: ${e.toolCallName} (id: ${e.toolCallId}, isMCP: ${this.isMcpTool(e.toolCallName)})`);
179
139
  }
180
140
  }
181
141
 
@@ -188,7 +148,8 @@ export class McpMiddleware extends Middleware {
188
148
  }
189
149
 
190
150
  if (event.type === EventType.TOOL_CALL_END) {
191
- // Track tool call end event
151
+ const e = event as ToolCallEndEvent;
152
+ console.log(`[McpMiddleware] TOOL_CALL_END: ${toolCallNames.get(e.toolCallId) ?? 'unknown'} (id: ${e.toolCallId})`);
192
153
  }
193
154
 
194
155
  // Workaround: Extract parallel tool calls from MESSAGES_SNAPSHOT
@@ -214,6 +175,7 @@ export class McpMiddleware extends Middleware {
214
175
  toolCallArgsBuffer.set(tc.id, tc.function.arguments || '{}');
215
176
  if (this.isMcpTool(tc.function.name)) {
216
177
  pendingMcpCalls.add(tc.id);
178
+ console.log(`[McpMiddleware] MESSAGES_SNAPSHOT: Discovered ${tc.function.name} (id: ${tc.id})`);
217
179
  }
218
180
  }
219
181
  }
@@ -234,12 +196,13 @@ export class McpMiddleware extends Middleware {
234
196
  if (!toolName) return;
235
197
 
236
198
  const args = this.parseArgs(toolCallArgsBuffer.get(toolCallId) || '{}');
237
- const { resultStr, rawResult } = await this.executeTool(toolName, args);
199
+ console.log(`[McpMiddleware] Executing pending tool: ${toolName}`);
200
+
201
+ const result = await this.executeTool(toolName, args);
238
202
  results.push({
239
203
  toolCallId,
240
204
  toolName,
241
- result: resultStr,
242
- rawResult,
205
+ result,
243
206
  messageId: this.generateId('mcp_result'),
244
207
  });
245
208
  pendingMcpCalls.delete(toolCallId);
@@ -249,39 +212,9 @@ export class McpMiddleware extends Middleware {
249
212
  return results;
250
213
  }
251
214
 
215
+ /** Emit tool results (without RUN_FINISHED - that's emitted when truly done) */
252
216
  private emitToolResults(observer: Subscriber<BaseEvent>, results: ToolResult[]): void {
253
- for (const { toolCallId, toolName, result, rawResult, messageId } of results) {
254
- // UI metadata may appear either on the tool CALL result (rawResult._meta)
255
- // or only on the tool DEFINITION (listTools result). We support both.
256
- const toolDef = this.tools.find(t => t.name === toolName);
257
- const sessionId = toolDef?._meta?.sessionId;
258
- const resourceUri =
259
- rawResult?._meta?.ui?.resourceUri ??
260
- rawResult?._meta?.['ui/resourceUri'] ??
261
- toolDef?._meta?.ui?.resourceUri ??
262
- toolDef?._meta?.['ui/resourceUri'];
263
-
264
- if (resourceUri) {
265
- // Extract base name for event emission to match metadata
266
- const baseToolName = this.getBaseToolName(toolName);
267
-
268
- const payload: McpAppUiEventPayload = {
269
- toolCallId,
270
- resourceUri,
271
- sessionId,
272
- toolName: baseToolName, // Use base name to match metadata
273
- result: rawResult ?? result,
274
- };
275
-
276
- observer.next({
277
- type: EventType.CUSTOM,
278
- name: MCP_APP_UI_EVENT,
279
- value: payload,
280
- timestamp: Date.now(),
281
- role: 'tool',
282
- } as any);
283
- }
284
-
217
+ for (const { toolCallId, toolName, result, messageId } of results) {
285
218
  observer.next({
286
219
  type: EventType.TOOL_CALL_RESULT,
287
220
  toolCallId,
@@ -290,6 +223,7 @@ export class McpMiddleware extends Middleware {
290
223
  role: 'tool',
291
224
  timestamp: Date.now(),
292
225
  } as any);
226
+ console.log(`[McpMiddleware] Emitting TOOL_CALL_RESULT for: ${toolName}`);
293
227
  }
294
228
  }
295
229
 
@@ -306,9 +240,14 @@ export class McpMiddleware extends Middleware {
306
240
  this.ensureIds(input);
307
241
  const anyInput = input as any;
308
242
 
243
+ console.log(`[McpMiddleware] === NEW RUN ===`);
244
+ console.log(`[McpMiddleware] threadId: ${anyInput.threadId}, runId: ${anyInput.runId}`);
245
+ console.log(`[McpMiddleware] messages: ${input.messages?.length ?? 0}, tools: ${this.tools?.length ?? 0}`);
246
+
309
247
  // Inject MCP tools
310
248
  if (this.toolSchemas?.length) {
311
249
  input.tools = [...(input.tools || []), ...this.toolSchemas];
250
+ console.log(`[McpMiddleware] Injected ${this.toolSchemas.length} tools:`, this.toolSchemas.map((t: Tool) => t.name));
312
251
  }
313
252
 
314
253
  const handleRunFinished = async () => {
@@ -325,6 +264,8 @@ export class McpMiddleware extends Middleware {
325
264
  return;
326
265
  }
327
266
 
267
+ console.log(`[McpMiddleware] RUN_FINISHED with ${state.pendingMcpCalls.size} pending calls`);
268
+
328
269
  // Reconstruct the Assistant Message that triggered these tools
329
270
  const toolCalls = [];
330
271
  for (const toolCallId of state.pendingMcpCalls) {
@@ -348,12 +289,16 @@ export class McpMiddleware extends Middleware {
348
289
  tool_calls: toolCalls.length > 0 ? toolCalls : undefined
349
290
  };
350
291
  input.messages.push(assistantMsg as any);
292
+ console.log(`[McpMiddleware] Added assistant message to history before tools: ${state.textContent?.slice(0, 50)}... [${toolCalls.length} tools]`);
351
293
  }
352
294
 
353
295
  // Execute tools and emit results (no RUN_FINISHED yet - continuation follows)
354
296
  const results = await this.executeTools(state);
355
297
  this.emitToolResults(observer, results);
356
298
 
299
+ // Prepare continuation
300
+ console.log(`[McpMiddleware] Triggering continuation with ${results.length} results`);
301
+
357
302
  // Add tool result messages to history
358
303
  for (const { toolCallId, result, messageId } of results) {
359
304
  input.messages.push({
@@ -370,6 +315,7 @@ export class McpMiddleware extends Middleware {
370
315
  state.textContent = ''; // Clear text content for next turn
371
316
 
372
317
  anyInput.runId = this.generateId('mcp_run');
318
+ console.log(`[McpMiddleware] === CONTINUATION RUN === messages: ${input.messages.length}`);
373
319
 
374
320
  // Subscribe to continuation
375
321
  next.run(input).subscribe({
@@ -379,6 +325,7 @@ export class McpMiddleware extends Middleware {
379
325
  this.handleToolCallEvent(event, state);
380
326
 
381
327
  if (event.type === EventType.RUN_ERROR) {
328
+ console.log(`[McpMiddleware] RUN_ERROR received in continuation`);
382
329
  state.error = true;
383
330
  observer.next(event);
384
331
  observer.complete();
@@ -386,6 +333,7 @@ export class McpMiddleware extends Middleware {
386
333
  }
387
334
 
388
335
  if (event.type === EventType.RUN_STARTED) {
336
+ console.log(`[McpMiddleware] Filtering RUN_STARTED from continuation`);
389
337
  return;
390
338
  }
391
339
 
@@ -417,6 +365,7 @@ export class McpMiddleware extends Middleware {
417
365
  this.handleToolCallEvent(event, state);
418
366
 
419
367
  if (event.type === EventType.RUN_ERROR) {
368
+ console.log(`[McpMiddleware] RUN_ERROR received`);
420
369
  state.error = true;
421
370
  observer.next(event);
422
371
  observer.complete();
@@ -446,7 +395,9 @@ export class McpMiddleware extends Middleware {
446
395
  /**
447
396
  * Factory function to create MCP middleware.
448
397
  */
449
- export function createMcpMiddleware(options: { tools: AguiTool[] }) {
398
+ export function createMcpMiddleware(
399
+ options: { tools: AguiTool[] }
400
+ ) {
450
401
  const middleware = new McpMiddleware(options);
451
402
  return (input: RunAgentInput, next: AbstractAgent): Observable<BaseEvent> => {
452
403
  return middleware.run(input, next);
@@ -459,4 +410,4 @@ export { createMcpMiddleware as createMcpToolMiddleware };
459
410
 
460
411
  // Re-exports
461
412
  export { Middleware, EventType };
462
- export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent, Tool };
413
+ export type { RunAgentInput, BaseEvent, AbstractAgent, ToolCallEndEvent, Tool };
@@ -1,30 +1,16 @@
1
- /**
2
- * MCP Redis Client Package - React
3
- * React client-side exports for MCP connection management
4
- */
5
-
6
- // React Hooks
7
- export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
8
- export { useAppHost } from './use-app-host.js';
9
- export { useMcpAppIframe, type McpAppIframeProps } from './use-mcp-app-iframe.js';
10
-
11
- // AG-UI Subscriber Pattern (Framework-agnostic)
12
- export {
13
- useAguiSubscriber,
14
- useMcpApps,
15
- useToolCallEvents,
16
- } from './use-agui-subscriber.js';
17
-
18
- export {
19
- createMcpAppSubscriber,
20
- subscribeMcpAppEvents,
21
- McpAppEventManager,
22
- type McpAppEvent,
23
- type McpAppEventHandler,
24
- type ToolCallEventData,
25
- type ToolCallEventHandler,
26
- type McpAppSubscriberConfig,
27
- } from './agui-subscriber.js';
28
-
29
- // Re-export shared types and client from main entry
30
- export * from '../index.js';
1
+ /**
2
+ * MCP SDK - React Client
3
+ * Simple React hooks for MCP app rendering
4
+ */
5
+
6
+ // Core MCP Hook
7
+ export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
8
+
9
+ // App Host (internal use)
10
+ export { useAppHost } from './use-app-host.js';
11
+
12
+ // Simplified MCP Apps Hook - the main API
13
+ export { useMcpApps } from './use-mcp-apps.js';
14
+
15
+ // Re-export shared types and client from main entry
16
+ export * from '../index.js';
@@ -0,0 +1,214 @@
1
+ /**
2
+ * MCP Apps Hook
3
+ *
4
+ * Provides utilities for rendering interactive UI components from MCP servers.
5
+ */
6
+
7
+ import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
8
+ import { useAppHost } from './use-app-host.js';
9
+ import type { SSEClient } from '../core/sse-client.js';
10
+
11
+ export interface McpClient {
12
+ connections: Array<{
13
+ sessionId: string;
14
+ tools: Array<{
15
+ name: string;
16
+ mcpApp?: {
17
+ resourceUri: string;
18
+ };
19
+ _meta?: {
20
+ ui?: {
21
+ resourceUri?: string;
22
+ };
23
+ 'ui/resourceUri'?: string;
24
+ };
25
+ }>;
26
+ }>;
27
+ sseClient?: SSEClient | null;
28
+ }
29
+
30
+ export interface McpAppMetadata {
31
+ toolName: string;
32
+ resourceUri: string;
33
+ sessionId: string;
34
+ }
35
+
36
+ interface McpAppRendererProps {
37
+ metadata: McpAppMetadata;
38
+ input?: Record<string, unknown>;
39
+ result?: unknown;
40
+ status: 'executing' | 'inProgress' | 'complete' | 'idle';
41
+ sseClient?: SSEClient | null;
42
+ /** Custom CSS class for the container */
43
+ className?: string;
44
+ }
45
+
46
+ /**
47
+ * Internal component that renders the MCP app in a sandboxed iframe
48
+ */
49
+ const McpAppRenderer = memo(function McpAppRenderer({
50
+ metadata,
51
+ input,
52
+ result,
53
+ status,
54
+ sseClient,
55
+ className,
56
+ }: McpAppRendererProps) {
57
+ const iframeRef = useRef<HTMLIFrameElement>(null);
58
+ const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
59
+ const [isLaunched, setIsLaunched] = useState(false);
60
+ const [error, setError] = useState<Error | null>(null);
61
+
62
+ // Track which data has been sent to prevent duplicates
63
+ const sentInputRef = useRef(false);
64
+ const sentResultRef = useRef(false);
65
+ const lastInputRef = useRef(input);
66
+ const lastResultRef = useRef(result);
67
+ const lastStatusRef = useRef(status);
68
+
69
+ // Launch the app when host is ready
70
+ useEffect(() => {
71
+ if (!host || !metadata.resourceUri || !metadata.sessionId) return;
72
+
73
+ host
74
+ .launch(metadata.resourceUri, metadata.sessionId)
75
+ .then(() => setIsLaunched(true))
76
+ .catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
77
+ }, [host, metadata.resourceUri, metadata.sessionId]);
78
+
79
+ // Send tool input when available or when it changes
80
+ useEffect(() => {
81
+ if (!host || !isLaunched || !input) return;
82
+
83
+ // Send if never sent, or if input changed
84
+ if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
85
+ sentInputRef.current = true;
86
+ lastInputRef.current = input;
87
+ host.sendToolInput(input);
88
+ }
89
+ }, [host, isLaunched, input]);
90
+
91
+ // Send tool result when complete or when it changes
92
+ useEffect(() => {
93
+ if (!host || !isLaunched || result === undefined) return;
94
+ if (status !== 'complete') return;
95
+
96
+ // Send if never sent, or if result changed
97
+ if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
98
+ sentResultRef.current = true;
99
+ lastResultRef.current = result;
100
+ const formattedResult =
101
+ typeof result === 'string'
102
+ ? { content: [{ type: 'text', text: result }] }
103
+ : result;
104
+ host.sendToolResult(formattedResult);
105
+ }
106
+ }, [host, isLaunched, result, status]);
107
+
108
+ // Reset sent flags when tool status resets to executing (new tool call)
109
+ useEffect(() => {
110
+ if (status === 'executing' && lastStatusRef.current !== 'executing') {
111
+ sentInputRef.current = false;
112
+ sentResultRef.current = false;
113
+ }
114
+ lastStatusRef.current = status;
115
+ }, [status]);
116
+
117
+ // Display errors
118
+ const displayError = error || hostError;
119
+ if (displayError) {
120
+ return (
121
+ <div className={`p-4 bg-red-900/20 border border-red-700 rounded text-red-200 ${className || ''}`}>
122
+ Error: {displayError.message || String(displayError)}
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
128
+ <div className={`w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative ${className || ''}`}>
129
+ <iframe
130
+ ref={iframeRef}
131
+ sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
132
+ className="w-full h-full min-h-96"
133
+ style={{ height: 'auto' }}
134
+ title="MCP App"
135
+ />
136
+ {!isLaunched && (
137
+ <div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
138
+ <div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
139
+ </div>
140
+ )}
141
+ </div>
142
+ );
143
+ });
144
+
145
+ /**
146
+ * Simple hook to get MCP app metadata
147
+ *
148
+ * @param mcpClient - The MCP client from useMcp() or context
149
+ * @returns Object with getAppMetadata function and McpAppRenderer component
150
+ *
151
+ * @example
152
+ * ```tsx
153
+ * function ToolRenderer(props) {
154
+ * const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
155
+ * const metadata = getAppMetadata(props.name);
156
+ *
157
+ * if (!metadata) return null;
158
+ * return (
159
+ * <McpAppRenderer
160
+ * metadata={metadata}
161
+ * input={props.args}
162
+ * result={props.result}
163
+ * status={props.status}
164
+ * />
165
+ * );
166
+ * }
167
+ * ```
168
+ */
169
+ export function useMcpApps(mcpClient: McpClient | null) {
170
+ /**
171
+ * Get MCP app metadata for a tool name
172
+ * This is fast and can be called on every render
173
+ */
174
+ const getAppMetadata = useCallback(
175
+ (toolName: string): McpAppMetadata | undefined => {
176
+ if (!mcpClient) return undefined;
177
+
178
+ const extractedName = extractToolName(toolName);
179
+
180
+ for (const conn of mcpClient.connections) {
181
+ for (const tool of conn.tools) {
182
+ const candidateName = extractToolName(tool.name);
183
+ // Check both locations: direct mcpApp or _meta.ui
184
+ const resourceUri =
185
+ tool.mcpApp?.resourceUri ??
186
+ tool._meta?.ui?.resourceUri ??
187
+ tool._meta?.['ui/resourceUri'];
188
+
189
+ if (resourceUri && candidateName === extractedName) {
190
+ return {
191
+ toolName: candidateName,
192
+ resourceUri,
193
+ sessionId: conn.sessionId,
194
+ };
195
+ }
196
+ }
197
+ }
198
+
199
+ return undefined;
200
+ },
201
+ [mcpClient]
202
+ );
203
+
204
+ return { getAppMetadata, McpAppRenderer };
205
+ }
206
+
207
+ /**
208
+ * Extract the base tool name, removing any prefixes
209
+ */
210
+ function extractToolName(fullName: string): string {
211
+ // Handle patterns like "tool_abc123_get-time" -> "get-time"
212
+ const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
213
+ return match?.[1] || fullName;
214
+ }