@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.
- package/README.md +44 -40
- package/dist/adapters/agui-middleware.d.mts +2 -21
- package/dist/adapters/agui-middleware.d.ts +2 -21
- package/dist/adapters/agui-middleware.js +30 -50
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs +31 -50
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/client/react.d.mts +51 -331
- package/dist/client/react.d.ts +51 -331
- package/dist/client/react.js +76 -281
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +78 -277
- package/dist/client/react.mjs.map +1 -1
- package/package.json +1 -1
- package/src/adapters/agui-middleware.ts +60 -109
- package/src/client/react/index.ts +16 -30
- package/src/client/react/use-mcp-apps.tsx +214 -0
- package/src/client/react/agui-subscriber.ts +0 -275
- package/src/client/react/use-agui-subscriber.ts +0 -270
- package/src/client/react/use-mcp-app-iframe.ts +0 -164
|
@@ -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
|
-
|
|
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<
|
|
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
|
|
93
|
+
return `Error: Tool ${tool ? 'has no handler' : 'not found'}: ${toolName}`;
|
|
126
94
|
}
|
|
127
95
|
|
|
128
|
-
try {
|
|
129
|
-
|
|
130
|
-
const result = await tool.handler(args);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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
|
|
3
|
-
* React
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
//
|
|
7
|
-
export { useMcp, type UseMcpOptions, type McpClient, type McpConnection } from './use-mcp.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
+
}
|