@mcp-ts/sdk 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +132 -50
  2. package/dist/adapters/agui-adapter.d.mts +3 -3
  3. package/dist/adapters/agui-adapter.d.ts +3 -3
  4. package/dist/adapters/agui-middleware.d.mts +3 -3
  5. package/dist/adapters/agui-middleware.d.ts +3 -3
  6. package/dist/adapters/ai-adapter.d.mts +3 -3
  7. package/dist/adapters/ai-adapter.d.ts +3 -3
  8. package/dist/adapters/langchain-adapter.d.mts +3 -3
  9. package/dist/adapters/langchain-adapter.d.ts +3 -3
  10. package/dist/adapters/mastra-adapter.d.mts +3 -3
  11. package/dist/adapters/mastra-adapter.d.ts +3 -3
  12. package/dist/client/index.d.mts +4 -4
  13. package/dist/client/index.d.ts +4 -4
  14. package/dist/client/react.d.mts +9 -8
  15. package/dist/client/react.d.ts +9 -8
  16. package/dist/client/react.js +16 -4
  17. package/dist/client/react.js.map +1 -1
  18. package/dist/client/react.mjs +16 -4
  19. package/dist/client/react.mjs.map +1 -1
  20. package/dist/client/vue.d.mts +5 -5
  21. package/dist/client/vue.d.ts +5 -5
  22. package/dist/client/vue.js +11 -1
  23. package/dist/client/vue.js.map +1 -1
  24. package/dist/client/vue.mjs +11 -1
  25. package/dist/client/vue.mjs.map +1 -1
  26. package/dist/{events-BgeztGYZ.d.mts → events-CK3N--3g.d.mts} +2 -0
  27. package/dist/{events-BgeztGYZ.d.ts → events-CK3N--3g.d.ts} +2 -0
  28. package/dist/index.d.mts +3 -3
  29. package/dist/index.d.ts +3 -3
  30. package/dist/index.js +12 -11
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +12 -11
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/{multi-session-client-CxogNckF.d.mts → multi-session-client-B1DBx5yR.d.mts} +3 -10
  35. package/dist/{multi-session-client-cox_WXUj.d.ts → multi-session-client-DyFzyJUx.d.ts} +3 -10
  36. package/dist/server/index.d.mts +5 -5
  37. package/dist/server/index.d.ts +5 -5
  38. package/dist/server/index.js +12 -11
  39. package/dist/server/index.js.map +1 -1
  40. package/dist/server/index.mjs +12 -11
  41. package/dist/server/index.mjs.map +1 -1
  42. package/dist/shared/index.d.mts +3 -3
  43. package/dist/shared/index.d.ts +3 -3
  44. package/dist/shared/index.js.map +1 -1
  45. package/dist/shared/index.mjs.map +1 -1
  46. package/dist/{types-CLccx9wW.d.mts → types-PjM1W07s.d.mts} +1 -0
  47. package/dist/{types-CLccx9wW.d.ts → types-PjM1W07s.d.ts} +1 -0
  48. package/package.json +1 -1
  49. package/src/client/react/index.ts +16 -16
  50. package/src/client/react/use-mcp-apps.tsx +214 -213
  51. package/src/client/react/use-mcp.ts +15 -2
  52. package/src/client/vue/use-mcp.ts +14 -2
  53. package/src/server/handlers/sse-handler.ts +4 -0
  54. package/src/server/mcp/oauth-client.ts +10 -17
  55. package/src/shared/events.ts +2 -0
  56. package/src/shared/types.ts +1 -0
@@ -1,213 +1,214 @@
1
- /**
2
- * Simplified MCP Apps Hook - Fixed for no flickering
3
- *
4
- * The key insight: React component identity must be stable.
5
- * We return a stable McpAppRenderer component and separate metadata lookup.
6
- */
7
-
8
- import React, { useState, useEffect, useCallback, useRef, memo } from 'react';
9
- import { useAppHost } from './use-app-host.js';
10
- import type { SSEClient } from '../core/sse-client.js';
11
-
12
- export interface McpClient {
13
- connections: Array<{
14
- sessionId: string;
15
- tools: Array<{
16
- name: string;
17
- mcpApp?: {
18
- resourceUri: string;
19
- };
20
- _meta?: {
21
- ui?: {
22
- resourceUri?: string;
23
- };
24
- 'ui/resourceUri'?: string;
25
- };
26
- }>;
27
- }>;
28
- sseClient?: SSEClient | null;
29
- }
30
-
31
- export interface McpAppMetadata {
32
- toolName: string;
33
- resourceUri: string;
34
- sessionId: string;
35
- }
36
-
37
- interface McpAppRendererProps {
38
- metadata: McpAppMetadata;
39
- input?: Record<string, unknown>;
40
- result?: unknown;
41
- status: 'executing' | 'inProgress' | 'complete' | 'idle';
42
- sseClient?: SSEClient | null;
43
- }
44
-
45
- /**
46
- * Stable renderer component - memoized to prevent flickering
47
- * Uses refs to track data changes and send updates to the iframe
48
- */
49
- const McpAppRenderer = memo(function McpAppRenderer({
50
- metadata,
51
- input,
52
- result,
53
- status,
54
- sseClient,
55
- }: McpAppRendererProps) {
56
- const iframeRef = useRef<HTMLIFrameElement>(null);
57
- const { host, error: hostError } = useAppHost(sseClient as SSEClient, iframeRef);
58
- const [isLaunched, setIsLaunched] = useState(false);
59
- const [error, setError] = useState<Error | null>(null);
60
-
61
- // Track which data has been sent to prevent duplicates
62
- const sentInputRef = useRef(false);
63
- const sentResultRef = useRef(false);
64
- const lastInputRef = useRef(input);
65
- const lastResultRef = useRef(result);
66
- const lastStatusRef = useRef(status);
67
-
68
- // Launch the app when host is ready
69
- useEffect(() => {
70
- if (!host || !metadata.resourceUri || !metadata.sessionId) return;
71
-
72
- host
73
- .launch(metadata.resourceUri, metadata.sessionId)
74
- .then(() => setIsLaunched(true))
75
- .catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
76
- }, [host, metadata.resourceUri, metadata.sessionId]);
77
-
78
- // Send tool input when available or when it changes
79
- useEffect(() => {
80
- if (!host || !isLaunched || !input) return;
81
-
82
- // Send if never sent, or if input changed
83
- if (!sentInputRef.current || JSON.stringify(input) !== JSON.stringify(lastInputRef.current)) {
84
- sentInputRef.current = true;
85
- lastInputRef.current = input;
86
- host.sendToolInput(input);
87
- }
88
- }, [host, isLaunched, input]);
89
-
90
- // Send tool result when complete or when it changes
91
- useEffect(() => {
92
- if (!host || !isLaunched || result === undefined) return;
93
- if (status !== 'complete') return;
94
-
95
- // Send if never sent, or if result changed
96
- if (!sentResultRef.current || JSON.stringify(result) !== JSON.stringify(lastResultRef.current)) {
97
- sentResultRef.current = true;
98
- lastResultRef.current = result;
99
- const formattedResult =
100
- typeof result === 'string'
101
- ? { content: [{ type: 'text', text: result }] }
102
- : result;
103
- host.sendToolResult(formattedResult);
104
- }
105
- }, [host, isLaunched, result, status]);
106
-
107
- // Reset sent flags when tool status resets to executing (new tool call)
108
- useEffect(() => {
109
- if (status === 'executing' && lastStatusRef.current !== 'executing') {
110
- sentInputRef.current = false;
111
- sentResultRef.current = false;
112
- }
113
- lastStatusRef.current = status;
114
- }, [status]);
115
-
116
- // Display errors
117
- const displayError = error || hostError;
118
- if (displayError) {
119
- return (
120
- <div className="p-4 bg-red-900/20 border border-red-700 rounded text-red-200">
121
- Error: {displayError.message || String(displayError)}
122
- </div>
123
- );
124
- }
125
-
126
- return (
127
- <div className="w-full border border-gray-700 rounded overflow-hidden bg-white min-h-96 my-2 relative">
128
- <iframe
129
- ref={iframeRef}
130
- sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
131
- className="w-full h-full min-h-96"
132
- style={{ height: 'auto' }}
133
- title="MCP App"
134
- />
135
- {!isLaunched && (
136
- <div className="absolute inset-0 bg-gray-900/50 flex items-center justify-center pointer-events-none">
137
- <div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
138
- </div>
139
- )}
140
- </div>
141
- );
142
- });
143
-
144
- /**
145
- * Simple hook to get MCP app metadata
146
- *
147
- * @param mcpClient - The MCP client from useMcp() or context
148
- * @returns Object with getAppMetadata function and McpAppRenderer component
149
- *
150
- * @example
151
- * ```tsx
152
- * function ToolRenderer(props) {
153
- * const { getAppMetadata, McpAppRenderer } = useMcpApps(mcpClient);
154
- * const metadata = getAppMetadata(props.name);
155
- *
156
- * if (!metadata) return null;
157
- * return (
158
- * <McpAppRenderer
159
- * metadata={metadata}
160
- * input={props.args}
161
- * result={props.result}
162
- * status={props.status}
163
- * />
164
- * );
165
- * }
166
- * ```
167
- */
168
- export function useMcpApps(mcpClient: McpClient | null) {
169
- /**
170
- * Get MCP app metadata for a tool name
171
- * This is fast and can be called on every render
172
- */
173
- const getAppMetadata = useCallback(
174
- (toolName: string): McpAppMetadata | undefined => {
175
- if (!mcpClient) return undefined;
176
-
177
- const extractedName = extractToolName(toolName);
178
-
179
- for (const conn of mcpClient.connections) {
180
- for (const tool of conn.tools) {
181
- const candidateName = extractToolName(tool.name);
182
- // Check both locations: direct mcpApp or _meta.ui
183
- const resourceUri =
184
- tool.mcpApp?.resourceUri ??
185
- tool._meta?.ui?.resourceUri ??
186
- tool._meta?.['ui/resourceUri'];
187
-
188
- if (resourceUri && candidateName === extractedName) {
189
- return {
190
- toolName: candidateName,
191
- resourceUri,
192
- sessionId: conn.sessionId,
193
- };
194
- }
195
- }
196
- }
197
-
198
- return undefined;
199
- },
200
- [mcpClient]
201
- );
202
-
203
- return { getAppMetadata, McpAppRenderer };
204
- }
205
-
206
- /**
207
- * Extract the base tool name, removing any prefixes
208
- */
209
- function extractToolName(fullName: string): string {
210
- // Handle patterns like "tool_abc123_get-time" -> "get-time"
211
- const match = fullName.match(/(?:tool_[^_]+_)?(.+)$/);
212
- return match?.[1] || fullName;
213
- }
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
+ }
@@ -75,7 +75,7 @@ export interface McpConnection {
75
75
  state: McpConnectionState;
76
76
  tools: ToolInfo[];
77
77
  error?: string;
78
- connectedAt?: Date;
78
+ createdAt?: Date;
79
79
  }
80
80
 
81
81
  export interface McpClient {
@@ -271,16 +271,28 @@ export function useMcp(options: UseMcpOptions): McpClient {
271
271
  const existing = prev.find((c: McpConnection) => c.sessionId === event.sessionId);
272
272
  if (existing) {
273
273
  return prev.map((c: McpConnection) =>
274
- c.sessionId === event.sessionId ? { ...c, state: event.state } : c
274
+ c.sessionId === event.sessionId ? {
275
+ ...c,
276
+ state: event.state,
277
+ // update createdAt if present in event, otherwise keep existing
278
+ createdAt: event.createdAt ? new Date(event.createdAt) : c.createdAt
279
+ } : c
275
280
  );
276
281
  } else {
282
+ // Fix: Don't add back disconnected sessions that were just removed
283
+ if (event.state === 'DISCONNECTED') {
284
+ return prev;
285
+ }
286
+
277
287
  return [
278
288
  ...prev,
279
289
  {
280
290
  sessionId: event.sessionId,
281
291
  serverId: event.serverId,
282
292
  serverName: event.serverName,
293
+ serverUrl: event.serverUrl,
283
294
  state: event.state,
295
+ createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
284
296
  tools: [],
285
297
  },
286
298
  ];
@@ -352,6 +364,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
352
364
  serverUrl: s.serverUrl,
353
365
  transport: s.transport,
354
366
  state: 'VALIDATING' as McpConnectionState,
367
+ createdAt: new Date(s.createdAt),
355
368
  tools: [],
356
369
  }))
357
370
  );
@@ -69,7 +69,7 @@ export interface McpConnection {
69
69
  state: McpConnectionState;
70
70
  tools: ToolInfo[];
71
71
  error?: string;
72
- connectedAt?: Date;
72
+ createdAt?: Date;
73
73
  }
74
74
 
75
75
  export interface McpClient {
@@ -213,13 +213,24 @@ export function useMcp(options: UseMcpOptions): McpClient {
213
213
  const existing = connections.value.find((c) => c.sessionId === event.sessionId);
214
214
  if (existing) {
215
215
  const index = connections.value.indexOf(existing);
216
- connections.value[index] = { ...existing, state: event.state };
216
+ connections.value[index] = {
217
+ ...existing,
218
+ state: event.state,
219
+ // update createdAt if present in event, otherwise keep existing
220
+ createdAt: event.createdAt ? new Date(event.createdAt) : existing.createdAt
221
+ };
217
222
  } else {
223
+ // Fix: Don't add back disconnected sessions that were just removed
224
+ if (event.state === 'DISCONNECTED') {
225
+ break;
226
+ }
227
+
218
228
  connections.value = [...connections.value, {
219
229
  sessionId: event.sessionId,
220
230
  serverId: event.serverId,
221
231
  serverName: event.serverName,
222
232
  state: event.state,
233
+ createdAt: event.createdAt ? new Date(event.createdAt) : undefined,
223
234
  tools: [],
224
235
  }];
225
236
  }
@@ -288,6 +299,7 @@ export function useMcp(options: UseMcpOptions): McpClient {
288
299
  serverUrl: s.serverUrl,
289
300
  transport: s.transport,
290
301
  state: 'VALIDATING' as McpConnectionState,
302
+ createdAt: new Date(s.createdAt),
291
303
  tools: [],
292
304
  }));
293
305
  }
@@ -225,6 +225,7 @@ export class SSEConnectionManager {
225
225
  serverName: s.serverName,
226
226
  serverUrl: s.serverUrl,
227
227
  transport: s.transportType,
228
+ createdAt: s.createdAt,
228
229
  })),
229
230
  };
230
231
  }
@@ -260,6 +261,7 @@ export class SSEConnectionManager {
260
261
  sessionId,
261
262
  serverId,
262
263
  serverName,
264
+ serverUrl,
263
265
  state: 'CONNECTING',
264
266
  previousState: 'DISCONNECTED',
265
267
  timestamp: Date.now(),
@@ -433,6 +435,7 @@ export class SSEConnectionManager {
433
435
  sessionId,
434
436
  serverId: session.serverId ?? 'unknown',
435
437
  serverName: session.serverName ?? 'Unknown',
438
+ serverUrl: session.serverUrl,
436
439
  state: 'VALIDATING',
437
440
  previousState: 'DISCONNECTED',
438
441
  timestamp: Date.now(),
@@ -495,6 +498,7 @@ export class SSEConnectionManager {
495
498
  sessionId,
496
499
  serverId: session.serverId ?? 'unknown',
497
500
  serverName: session.serverName ?? 'Unknown',
501
+ serverUrl: session.serverUrl,
498
502
  state: 'AUTHENTICATING',
499
503
  previousState: 'DISCONNECTED',
500
504
  timestamp: Date.now(),