@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.
@@ -1,275 +0,0 @@
1
- /**
2
- * AG-UI Subscriber for MCP Apps
3
- *
4
- * Provides a standalone subscriber pattern for listening to AG-UI agent events
5
- * without depending on CopilotKit components. This allows MCP apps to be rendered
6
- * based on tool call events from any AG-UI agent.
7
- *
8
- * @requires @ag-ui/client - Peer dependency for AG-UI types
9
- */
10
-
11
- import type { AgentSubscriber, AbstractAgent } from '@ag-ui/client';
12
-
13
- /**
14
- * MCP App UI event payload emitted from middleware
15
- */
16
- export interface McpAppEvent {
17
- /** Tool call ID that triggered this UI */
18
- toolCallId: string;
19
- /** Resource URI for the MCP app */
20
- resourceUri: string;
21
- /** Session ID for the MCP connection */
22
- sessionId?: string;
23
- /** Name of the tool that was called */
24
- toolName: string;
25
- /** Tool execution result (if available) */
26
- result?: any;
27
- /** Tool input arguments */
28
- input?: Record<string, unknown>;
29
- /** Tool execution status */
30
- status?: 'executing' | 'inProgress' | 'complete';
31
- }
32
-
33
- /**
34
- * Event handler for MCP app events
35
- */
36
- export type McpAppEventHandler = (event: McpAppEvent) => void;
37
-
38
- /**
39
- * Event handler for tool call events
40
- */
41
- export interface ToolCallEventData {
42
- toolCallId: string;
43
- toolName: string;
44
- args?: Record<string, unknown>;
45
- result?: any;
46
- status: 'start' | 'args' | 'end' | 'result';
47
- }
48
-
49
- export type ToolCallEventHandler = (event: ToolCallEventData) => void;
50
-
51
- /**
52
- * Configuration for MCP app subscriber
53
- */
54
- export interface McpAppSubscriberConfig {
55
- /** Handler for MCP app UI events */
56
- onMcpApp?: McpAppEventHandler;
57
- /** Handler for general tool call events */
58
- onToolCall?: ToolCallEventHandler;
59
- /** Custom event name to listen for (default: 'mcp-apps-ui') */
60
- eventName?: string;
61
- }
62
-
63
- /**
64
- * Creates an AG-UI AgentSubscriber that listens for MCP app events
65
- *
66
- * This subscriber can be attached to any AG-UI agent instance using agent.subscribe()
67
- * and will call the provided handlers when MCP app events are detected.
68
- *
69
- * @param config - Configuration with event handlers
70
- * @returns AgentSubscriber that can be passed to agent.subscribe()
71
- *
72
- * @example
73
- * ```typescript
74
- * import { createMcpAppSubscriber } from '@mcp-ts/sdk/client/react';
75
- * import { HttpAgent } from '@ag-ui/client';
76
- *
77
- * const agent = new HttpAgent({ url: '/api/agent' });
78
- *
79
- * const subscriber = createMcpAppSubscriber({
80
- * onMcpApp: (event) => {
81
- * console.log('MCP App:', event.resourceUri);
82
- * // Render MCP app UI
83
- * },
84
- * onToolCall: (event) => {
85
- * console.log('Tool Call:', event.toolName, event.status);
86
- * }
87
- * });
88
- *
89
- * const { unsubscribe } = agent.subscribe(subscriber);
90
- * ```
91
- */
92
- export function createMcpAppSubscriber(
93
- config: McpAppSubscriberConfig
94
- ): AgentSubscriber {
95
- const eventName = config.eventName || 'mcp-apps-ui';
96
-
97
- const subscriber: AgentSubscriber = {
98
- // Listen for custom MCP app events from middleware
99
- onCustomEvent: ({ event }) => {
100
- if (event.name === eventName && config.onMcpApp) {
101
- const payload = event.value as McpAppEvent;
102
- config.onMcpApp(payload);
103
- }
104
- },
105
-
106
- // Listen for tool call lifecycle events
107
- onToolCallStartEvent: (params) => {
108
- if (config.onToolCall && params.event.toolCallName) {
109
- config.onToolCall({
110
- toolCallId: params.event.toolCallId || '',
111
- toolName: params.event.toolCallName,
112
- status: 'start',
113
- });
114
- }
115
- },
116
-
117
- onToolCallArgsEvent: (params) => {
118
- if (config.onToolCall) {
119
- // partialToolCallArgs contains the parsed args
120
- const args = params.partialToolCallArgs;
121
-
122
- config.onToolCall({
123
- toolCallId: params.event.toolCallId || '',
124
- toolName: params.toolCallName || '', // toolCallName is in params, not event
125
- args,
126
- status: 'args',
127
- });
128
- }
129
- },
130
-
131
- onToolCallEndEvent: (params) => {
132
- if (config.onToolCall && params.event.toolCallId) {
133
- config.onToolCall({
134
- toolCallId: params.event.toolCallId,
135
- toolName: params.toolCallName || '', // toolCallName is in params, not event
136
- status: 'end',
137
- });
138
- }
139
- },
140
-
141
- onToolCallResultEvent: (params) => {
142
- if (config.onToolCall && params.event.toolCallId) {
143
- config.onToolCall({
144
- toolCallId: params.event.toolCallId,
145
- toolName: '', // Not available in result event
146
- result: params.event.content, // content contains the result
147
- status: 'result',
148
- });
149
- }
150
- },
151
- };
152
-
153
- return subscriber;
154
- }
155
-
156
- /**
157
- * Subscribes to MCP app events from an AG-UI agent
158
- *
159
- * Convenience function that creates a subscriber and attaches it to an agent.
160
- * Returns an unsubscribe function to clean up the subscription.
161
- *
162
- * @param agent - AG-UI agent instance
163
- * @param config - Configuration with event handlers
164
- * @returns Unsubscribe function
165
- *
166
- * @example
167
- * ```typescript
168
- * const unsubscribe = subscribeMcpAppEvents(agent, {
169
- * onMcpApp: (event) => {
170
- * renderMcpApp(event.resourceUri, event.sessionId);
171
- * }
172
- * });
173
- *
174
- * // Later, to cleanup:
175
- * unsubscribe();
176
- * ```
177
- */
178
- export function subscribeMcpAppEvents(
179
- agent: AbstractAgent,
180
- config: McpAppSubscriberConfig
181
- ): () => void {
182
- const subscriber = createMcpAppSubscriber(config);
183
- const { unsubscribe } = agent.subscribe(subscriber);
184
- return unsubscribe;
185
- }
186
-
187
- /**
188
- * Manager for MCP app events with built-in state management
189
- *
190
- * Provides a higher-level API for managing MCP app events with automatic
191
- * state tracking. Useful for React contexts or state management systems.
192
- */
193
- export class McpAppEventManager {
194
- private events: Map<string, McpAppEvent> = new Map();
195
- private toolCalls: Map<string, ToolCallEventData> = new Map();
196
- private listeners: Set<() => void> = new Set();
197
- private unsubscribe?: () => void;
198
- private cachedSnapshot: Record<string, McpAppEvent> = {};
199
-
200
- /**
201
- * Attach to an AG-UI agent
202
- */
203
- attach(agent: AbstractAgent): void {
204
- if (this.unsubscribe) {
205
- this.unsubscribe();
206
- }
207
-
208
- this.unsubscribe = subscribeMcpAppEvents(agent, {
209
- onMcpApp: (event) => {
210
- this.events.set(event.toolName, event);
211
- this.cachedSnapshot = Object.fromEntries(this.events);
212
- this.notify();
213
- },
214
- onToolCall: (event) => {
215
- if (event.toolCallId) {
216
- this.toolCalls.set(event.toolCallId, event);
217
- this.notify();
218
- }
219
- },
220
- });
221
- }
222
-
223
- /**
224
- * Detach from the current agent
225
- */
226
- detach(): void {
227
- if (this.unsubscribe) {
228
- this.unsubscribe();
229
- this.unsubscribe = undefined;
230
- }
231
- }
232
-
233
- /**
234
- * Get MCP app event for a specific tool
235
- */
236
- getEvent(toolName: string): McpAppEvent | undefined {
237
- return this.events.get(toolName);
238
- }
239
-
240
- /**
241
- * Get all MCP app events (cached for useSyncExternalStore)
242
- */
243
- getAllEvents(): Record<string, McpAppEvent> {
244
- return this.cachedSnapshot;
245
- }
246
-
247
- /**
248
- * Get tool call event by ID
249
- */
250
- getToolCall(toolCallId: string): ToolCallEventData | undefined {
251
- return this.toolCalls.get(toolCallId);
252
- }
253
-
254
- /**
255
- * Subscribe to event changes
256
- */
257
- subscribe(listener: () => void): () => void {
258
- this.listeners.add(listener);
259
- return () => this.listeners.delete(listener);
260
- }
261
-
262
- /**
263
- * Clear all events
264
- */
265
- clear(): void {
266
- this.events.clear();
267
- this.toolCalls.clear();
268
- this.cachedSnapshot = {};
269
- this.notify();
270
- }
271
-
272
- private notify(): void {
273
- this.listeners.forEach(listener => listener());
274
- }
275
- }
@@ -1,270 +0,0 @@
1
- /**
2
- * React Hook for AG-UI Subscriber Pattern
3
- *
4
- * Provides React hooks for subscribing to AG-UI agent events without
5
- * depending on CopilotKit components. Works with any AG-UI agent instance.
6
- */
7
-
8
- import { useEffect, useState, useSyncExternalStore, useMemo } from 'react';
9
- import type { AbstractAgent } from '@ag-ui/client';
10
- import {
11
- createMcpAppSubscriber,
12
- McpAppEventManager,
13
- type McpAppEvent,
14
- type McpAppSubscriberConfig,
15
- type ToolCallEventData,
16
- } from './agui-subscriber.js';
17
-
18
- /**
19
- * React hook to subscribe to MCP app events from an AG-UI agent
20
- *
21
- * This hook manages the subscription lifecycle and provides event state.
22
- * It's completely independent of CopilotKit and works with any AG-UI agent.
23
- *
24
- * @param agent - AG-UI agent instance (can be from HttpAgent, LangGraphAgent, etc.)
25
- * @param config - Configuration with event handlers
26
- *
27
- * @example
28
- * ```typescript
29
- * import { useAguiSubscriber } from '@mcp-ts/sdk/client/react';
30
- * import { HttpAgent } from '@ag-ui/client';
31
- *
32
- * function MyComponent() {
33
- * const [agent] = useState(() => new HttpAgent({ url: '/api/agent' }));
34
- *
35
- * useAguiSubscriber(agent, {
36
- * onMcpApp: (event) => {
37
- * console.log('MCP App:', event.resourceUri);
38
- * },
39
- * onToolCall: (event) => {
40
- * console.log('Tool:', event.toolName, event.status);
41
- * }
42
- * });
43
- *
44
- * // Your UI code...
45
- * }
46
- * ```
47
- */
48
- export function useAguiSubscriber(
49
- agent: AbstractAgent | null | undefined,
50
- config: McpAppSubscriberConfig
51
- ): void {
52
- useEffect(() => {
53
- if (!agent) return;
54
-
55
- const subscriber = createMcpAppSubscriber(config);
56
- const { unsubscribe } = agent.subscribe(subscriber);
57
-
58
- return () => unsubscribe();
59
- }, [agent, config]);
60
- }
61
-
62
- /**
63
- * React hook for managing MCP apps
64
- *
65
- * Returns MCP apps that need to be rendered. Automatically combines:
66
- * 1. Tool metadata (instant, synchronous)
67
- * 2. AG-UI agent events (async, after tool execution)
68
- *
69
- * Prioritizes tool metadata for instant loading.
70
- *
71
- * @param agent - AG-UI agent instance (optional)
72
- * @param mcpClient - MCP client with tool metadata (optional)
73
- * @returns Object with MCP apps and helper functions
74
- *
75
- * @example
76
- * ```typescript
77
- * import { useMcpApps } from '@mcp-ts/sdk/client/react';
78
- *
79
- * function ToolRenderer() {
80
- * const { agent } = useAgent({ agentId: "myAgent" });
81
- * const { mcpClient } = useMcpContext();
82
- * const { apps } = useMcpApps(agent, mcpClient);
83
- *
84
- * return (
85
- * <>
86
- * {Object.entries(apps).map(([toolName, app]) => (
87
- * <McpAppUI key={toolName} {...app} />
88
- * ))}
89
- * </>
90
- * );
91
- * }
92
- * ```
93
- */
94
- export function useMcpApps(
95
- agent?: AbstractAgent | null,
96
- mcpClient?: { connections: Array<{ tools: any[]; sessionId: string }> } | null
97
- ): {
98
- /** All MCP apps indexed by tool name */
99
- apps: Record<string, McpAppEvent>;
100
- /** Get app for a specific tool */
101
- getApp: (toolName: string) => McpAppEvent | undefined;
102
- /** Clear all apps */
103
- clear: () => void;
104
- } {
105
- // Create manager instance once
106
- const [manager] = useState(() => new McpAppEventManager());
107
-
108
- // Attach/detach manager when agent changes
109
- useEffect(() => {
110
- if (!agent) {
111
- manager.detach();
112
- return;
113
- }
114
-
115
- manager.attach(agent);
116
- return () => manager.detach();
117
- }, [agent, manager]);
118
-
119
- // Subscribe to manager state changes using useSyncExternalStore
120
- const agentApps = useSyncExternalStore(
121
- (callback) => manager.subscribe(callback),
122
- () => manager.getAllEvents(),
123
- () => ({}) // Server-side snapshot
124
- );
125
-
126
- // Combine tool metadata with agent events
127
- const apps: Record<string, McpAppEvent> = useMemo(() => {
128
- const combined: Record<string, McpAppEvent> = {};
129
-
130
- // First, add tool metadata (instant, synchronous - prioritized!)
131
- if (mcpClient) {
132
- for (const conn of mcpClient.connections) {
133
- for (const tool of conn.tools) {
134
- const meta = (tool as any)._meta;
135
- if (!meta?.ui) continue;
136
-
137
- const ui = meta.ui;
138
- if (typeof ui !== 'object' || !ui) continue;
139
- if (ui.visibility && !ui.visibility.includes('app')) continue;
140
-
141
- const resourceUri =
142
- typeof ui.resourceUri === 'string'
143
- ? ui.resourceUri
144
- : typeof ui.uri === 'string'
145
- ? ui.uri
146
- : undefined;
147
-
148
- if (resourceUri) {
149
- combined[tool.name] = {
150
- toolCallId: '',
151
- resourceUri,
152
- sessionId: conn.sessionId,
153
- toolName: tool.name,
154
- };
155
- }
156
- }
157
- }
158
- }
159
-
160
- // Then, merge in agent events (may have additional data like toolCallId, result)
161
- for (const [toolName, event] of Object.entries(agentApps) as [string, McpAppEvent][]) {
162
- if (combined[toolName]) {
163
- // Merge: keep metadata's resourceUri/sessionId, add event's toolCallId/result
164
- combined[toolName] = {
165
- ...combined[toolName],
166
- toolCallId: event.toolCallId || combined[toolName].toolCallId,
167
- result: event.result,
168
- input: event.input,
169
- status: event.status,
170
- };
171
- } else {
172
- // No metadata, just use the event
173
- combined[toolName] = event;
174
- }
175
- }
176
-
177
- // Return a Proxy that handles both base and prefixed tool names transparently
178
- // This allows users to lookup apps with either format:
179
- // - apps["get-time"] (base name)
180
- // - apps["tool_abc123_get-time"] (prefixed name from CopilotKit)
181
- return new Proxy(combined, {
182
- get(target, prop: string | symbol) {
183
- if (typeof prop !== 'string') return undefined;
184
-
185
- // Try exact match first (base name)
186
- if (prop in target) return target[prop];
187
-
188
- // Extract base name from prefixed format: "tool_xxx_baseName" -> "baseName"
189
- const match = prop.match(/^tool_[^_]+_(.+)$/);
190
- if (match && match[1] in target) {
191
- return target[match[1]];
192
- }
193
-
194
- return undefined;
195
- },
196
- // Support Object.entries, Object.keys, etc. by returning base names
197
- ownKeys(target) {
198
- return Reflect.ownKeys(target);
199
- },
200
- getOwnPropertyDescriptor(target, prop) {
201
- return Reflect.getOwnPropertyDescriptor(target, prop);
202
- }
203
- });
204
- }, [agentApps, mcpClient]);
205
-
206
- return {
207
- apps,
208
- // getApp handles both base and prefixed names transparently via the Proxy
209
- getApp: (toolName: string) => apps[toolName] as McpAppEvent | undefined,
210
- clear: () => manager.clear(),
211
- };
212
- }
213
-
214
- /**
215
- * React hook for tracking tool call lifecycle events
216
- *
217
- * Provides access to detailed tool call events (start, args, end, result)
218
- * for debugging or custom UI rendering.
219
- *
220
- * @param agent - AG-UI agent instance
221
- * @returns Object with tool call events and helper functions
222
- *
223
- * @example
224
- * ```typescript
225
- * import { useToolCallEvents } from '@mcp-ts/sdk/client/react';
226
- *
227
- * function ToolCallDebugger() {
228
- * const [agent] = useState(() => new HttpAgent({ url: '/api/agent' }));
229
- * const { toolCalls } = useToolCallEvents(agent);
230
- *
231
- * return (
232
- * <div>
233
- * {Object.entries(toolCalls).map(([id, event]) => (
234
- * <div key={id}>
235
- * {event.toolName} - {event.status}
236
- * </div>
237
- * ))}
238
- * </div>
239
- * );
240
- * }
241
- * ```
242
- */
243
- export function useToolCallEvents(agent: AbstractAgent | null | undefined): {
244
- /** All tool call events indexed by tool call ID */
245
- toolCalls: Record<string, ToolCallEventData>;
246
- } {
247
- const [toolCalls, setToolCalls] = useState<Record<string, ToolCallEventData>>({});
248
-
249
- useEffect(() => {
250
- if (!agent) {
251
- setToolCalls({});
252
- return;
253
- }
254
-
255
- const subscriber = createMcpAppSubscriber({
256
- onToolCall: (event) => {
257
- setToolCalls((prev) => ({
258
- ...prev,
259
- [event.toolCallId]: event,
260
- }));
261
- },
262
- });
263
-
264
- const { unsubscribe } = agent.subscribe(subscriber);
265
-
266
- return () => unsubscribe();
267
- }, [agent]);
268
-
269
- return { toolCalls };
270
- }
@@ -1,164 +0,0 @@
1
- /**
2
- * useMcpAppIframe Hook
3
- * Manages iframe lifecycle, app host communication, and tool data flow
4
- */
5
-
6
- import { useEffect, useRef, useState } from 'react';
7
- import type { SSEClient } from '../core/sse-client.js';
8
- import { useAppHost } from './use-app-host.js';
9
-
10
- export interface McpAppIframeProps {
11
- /**
12
- * The resource URI of the MCP app to load
13
- */
14
- resourceUri: string;
15
-
16
- /**
17
- * The session ID for the MCP connection
18
- */
19
- sessionId: string;
20
-
21
- /**
22
- * Tool input arguments to send to the app
23
- */
24
- toolInput?: Record<string, unknown>;
25
-
26
- /**
27
- * Tool execution result to send to the app
28
- */
29
- toolResult?: unknown;
30
-
31
- /**
32
- * Current status of the tool execution
33
- */
34
- toolStatus?: 'executing' | 'inProgress' | 'complete';
35
-
36
- /**
37
- * SSE client instance for MCP operations
38
- */
39
- sseClient: SSEClient;
40
- }
41
-
42
- interface McpAppIframeResult {
43
- /**
44
- * Ref to attach to the iframe element
45
- */
46
- iframeRef: React.RefObject<HTMLIFrameElement>;
47
-
48
- /**
49
- * Whether the app has been successfully launched
50
- */
51
- isLaunched: boolean;
52
-
53
- /**
54
- * Error that occurred during initialization or execution
55
- */
56
- error: Error | null;
57
- }
58
-
59
- /**
60
- * Hook to manage MCP app iframe lifecycle and communication
61
- *
62
- * Handles:
63
- * - Iframe setup and host initialization
64
- * - App launching with resource preloading
65
- * - Tool input and result communication
66
- * - Error tracking
67
- *
68
- * Returns refs and state for UI rendering - styling is left to the user.
69
- *
70
- * @param props - Configuration for the iframe
71
- * @returns Iframe ref, launch state, and error state
72
- *
73
- * @example
74
- * const { iframeRef, isLaunched, error } = useMcpAppIframe({
75
- * resourceUri: "https://example.com/app",
76
- * sessionId: "session-123",
77
- * toolInput: myInput,
78
- * toolResult: myResult,
79
- * toolStatus: "complete",
80
- * sseClient: sseClient,
81
- * });
82
- *
83
- * return (
84
- * <div className="my-custom-container">
85
- * <iframe ref={iframeRef} className="my-iframe-style" />
86
- * {!isLaunched && <p>Loading...</p>}
87
- * {error && <p>Error: {error.message}</p>}
88
- * </div>
89
- * );
90
- */
91
- export function useMcpAppIframe({
92
- resourceUri,
93
- sessionId,
94
- toolInput,
95
- toolResult,
96
- toolStatus,
97
- sseClient,
98
- }: McpAppIframeProps): McpAppIframeResult {
99
- const iframeRef = useRef<HTMLIFrameElement>(null!);
100
- const { host, error: hostError } = useAppHost(sseClient, iframeRef);
101
-
102
- const [isLaunched, setIsLaunched] = useState(false);
103
- const [error, setError] = useState<Error | null>(null);
104
-
105
- // Track attempt flags to ensure operations run only once
106
- const launchAttemptedRef = useRef(false);
107
- const toolInputSentRef = useRef(false);
108
- const toolResultSentRef = useRef(false);
109
-
110
- // Report host initialization errors
111
- useEffect(() => {
112
- if (hostError) {
113
- setError(hostError);
114
- }
115
- }, [hostError]);
116
-
117
- // Launch the app when host is ready
118
- // The resource should be preloaded, so this resolves instantly
119
- useEffect(() => {
120
- if (!host || !resourceUri || !sessionId || launchAttemptedRef.current) return;
121
-
122
- launchAttemptedRef.current = true;
123
-
124
- host
125
- .launch(resourceUri, sessionId)
126
- .then(() => {
127
- setIsLaunched(true);
128
- })
129
- .catch((err) => {
130
- const error = err instanceof Error ? err : new Error(String(err));
131
- setError(error);
132
- });
133
- }, [host, resourceUri, sessionId]);
134
-
135
- // Send tool input to the app when available and launched
136
- useEffect(() => {
137
- if (!host || !isLaunched || !toolInput || toolInputSentRef.current) return;
138
-
139
- toolInputSentRef.current = true;
140
- host.sendToolInput(toolInput);
141
- }, [host, isLaunched, toolInput]);
142
-
143
- // Send tool result to the app when available and complete
144
- useEffect(() => {
145
- if (!host || !isLaunched || toolResult === undefined || toolResultSentRef.current) return;
146
- if (toolStatus !== 'complete') return;
147
-
148
- toolResultSentRef.current = true;
149
-
150
- // Format result - wrap string results in content array for MCP compatibility
151
- const formattedResult =
152
- typeof toolResult === 'string'
153
- ? { content: [{ type: 'text', text: toolResult }] }
154
- : toolResult;
155
-
156
- host.sendToolResult(formattedResult);
157
- }, [host, isLaunched, toolResult, toolStatus]);
158
-
159
- return {
160
- iframeRef,
161
- isLaunched,
162
- error,
163
- };
164
- }