@mcp-ts/sdk 1.3.10 → 1.4.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/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +3 -189
- package/dist/client/index.d.ts +3 -189
- package/dist/client/index.js +218 -54
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +215 -55
- package/dist/client/index.mjs.map +1 -1
- package/dist/client/react.d.mts +21 -14
- package/dist/client/react.d.ts +21 -14
- package/dist/client/react.js +402 -83
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +400 -85
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +3 -2
- package/dist/client/vue.d.ts +3 -2
- package/dist/client/vue.js +239 -63
- package/dist/client/vue.js.map +1 -1
- package/dist/client/vue.mjs +236 -64
- package/dist/client/vue.mjs.map +1 -1
- package/dist/index-CQr9q0bF.d.mts +295 -0
- package/dist/index-nE_7Io0I.d.ts +295 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +237 -58
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +230 -59
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.js +15 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +15 -4
- package/dist/server/index.mjs.map +1 -1
- package/package.json +13 -11
- package/src/adapters/langchain-adapter.ts +1 -1
- package/src/client/core/app-host.ts +252 -65
- package/src/client/core/constants.ts +30 -0
- package/src/client/index.ts +6 -1
- package/src/client/react/index.ts +1 -0
- package/src/client/react/use-app-host.ts +8 -15
- package/src/client/react/use-mcp-apps.tsx +221 -26
- package/src/client/react/use-mcp.ts +23 -12
- package/src/client/utils/app-host-utils.ts +62 -0
- package/src/client/vue/use-mcp.ts +23 -12
- package/src/server/mcp/oauth-client.ts +31 -8
|
@@ -11,10 +11,14 @@ import React, {
|
|
|
11
11
|
useRef,
|
|
12
12
|
memo,
|
|
13
13
|
useMemo,
|
|
14
|
+
forwardRef,
|
|
15
|
+
useImperativeHandle,
|
|
14
16
|
type MutableRefObject,
|
|
15
17
|
} from 'react';
|
|
16
|
-
import { useAppHost } from './use-app-host.js';
|
|
18
|
+
import { useAppHost, type UseAppHostOptions } from './use-app-host.js';
|
|
17
19
|
import type { SSEClient } from '../core/sse-client.js';
|
|
20
|
+
import { APP_HOST_DEFAULTS } from '../core/constants.js';
|
|
21
|
+
import type { SandboxConfig } from '../core/app-host.js';
|
|
18
22
|
|
|
19
23
|
export interface McpClient {
|
|
20
24
|
connections: Array<{
|
|
@@ -41,14 +45,26 @@ export interface McpAppMetadata {
|
|
|
41
45
|
sessionId: string;
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Imperative handle for {@link useMcpApps}'s `McpAppRenderer` (via `ref`),
|
|
50
|
+
* aligned with `@mcp-ui/client`'s `AppRendererHandle.teardownResource`.
|
|
51
|
+
*/
|
|
52
|
+
export interface McpAppRendererHandle {
|
|
53
|
+
teardownResource: (params?: Record<string, unknown>) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/** Props for {@link useMcpApps}'s `McpAppRenderer` (client is supplied via the hook). */
|
|
45
|
-
export interface McpAppRendererProps {
|
|
57
|
+
export interface McpAppRendererProps extends Pick<UseAppHostOptions, 'sandbox' | 'hostContext' | 'onCallTool' | 'onReadResource' | 'onFallbackRequest' | 'onMessage' | 'onOpenLink' | 'onLoggingMessage' | 'onSizeChanged' | 'onError'> {
|
|
46
58
|
name: string;
|
|
59
|
+
toolResourceUri?: string;
|
|
60
|
+
html?: string;
|
|
47
61
|
input?: Record<string, unknown>;
|
|
48
62
|
result?: unknown;
|
|
49
|
-
status
|
|
50
|
-
|
|
63
|
+
status?: 'executing' | 'inProgress' | 'complete' | 'idle';
|
|
64
|
+
toolInputPartial?: any;
|
|
65
|
+
toolCancelled?: boolean;
|
|
51
66
|
className?: string;
|
|
67
|
+
loader?: React.ReactNode;
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
type McpAppViewProps = McpAppRendererProps & {
|
|
@@ -60,22 +76,119 @@ type McpAppViewProps = McpAppRendererProps & {
|
|
|
60
76
|
};
|
|
61
77
|
|
|
62
78
|
/** Renders one MCP App in a sandboxed iframe; reads the latest client from `clientRef` each render. */
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
79
|
+
const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(function McpAppView(
|
|
80
|
+
{
|
|
81
|
+
clientRef,
|
|
82
|
+
name,
|
|
83
|
+
toolResourceUri,
|
|
84
|
+
html,
|
|
85
|
+
input,
|
|
86
|
+
result,
|
|
87
|
+
status = 'idle',
|
|
88
|
+
toolInputPartial,
|
|
89
|
+
toolCancelled,
|
|
90
|
+
sandbox,
|
|
91
|
+
hostContext,
|
|
92
|
+
onCallTool,
|
|
93
|
+
onReadResource,
|
|
94
|
+
onFallbackRequest,
|
|
95
|
+
onMessage,
|
|
96
|
+
onOpenLink,
|
|
97
|
+
onLoggingMessage,
|
|
98
|
+
onSizeChanged,
|
|
99
|
+
onError: onHostError,
|
|
100
|
+
className,
|
|
101
|
+
loader,
|
|
102
|
+
},
|
|
103
|
+
ref,
|
|
104
|
+
) {
|
|
71
105
|
const mcpClient = clientRef.current;
|
|
72
106
|
const metadata = getMcpAppMetadata(mcpClient, name);
|
|
73
107
|
const sseClient = mcpClient?.sseClient ?? null;
|
|
74
|
-
const resourceUri = metadata?.resourceUri;
|
|
108
|
+
const resourceUri = toolResourceUri || metadata?.resourceUri;
|
|
75
109
|
const appSessionId = metadata?.sessionId;
|
|
76
110
|
|
|
77
111
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
78
|
-
const
|
|
112
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
113
|
+
// Tracks the last height (px) reported by the guest before/after fullscreen so we
|
|
114
|
+
// can restore it when the native fullscreen API exits and the guest fires a stale
|
|
115
|
+
// resize event that would otherwise collapse the iframe to 0.
|
|
116
|
+
const preFullscreenHeightRef = useRef<number | null>(null);
|
|
117
|
+
const displayModeRef = useRef<'inline' | 'fullscreen'>('inline');
|
|
118
|
+
const [displayMode, setDisplayMode] = useState<'inline' | 'fullscreen'>('inline');
|
|
119
|
+
|
|
120
|
+
const setDisplayModeWithRef = (mode: 'inline' | 'fullscreen') => {
|
|
121
|
+
displayModeRef.current = mode;
|
|
122
|
+
setDisplayMode(mode);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const { host, error: hostError } = useAppHost(sseClient as any, iframeRef, {
|
|
126
|
+
sandbox,
|
|
127
|
+
hostContext,
|
|
128
|
+
onCallTool,
|
|
129
|
+
onReadResource,
|
|
130
|
+
onFallbackRequest,
|
|
131
|
+
onMessage,
|
|
132
|
+
onOpenLink,
|
|
133
|
+
onLoggingMessage,
|
|
134
|
+
// Intercept onSizeChanged: when exiting fullscreen, ignore guest resize events
|
|
135
|
+
// that arrive with the shrunken viewport dimensions, and restore the pre-fullscreen height.
|
|
136
|
+
onSizeChanged: (params) => {
|
|
137
|
+
if (displayModeRef.current === 'inline' && preFullscreenHeightRef.current !== null) {
|
|
138
|
+
// Guest fired a resize right after fullscreen exit – restore the saved height
|
|
139
|
+
const savedHeight = preFullscreenHeightRef.current;
|
|
140
|
+
preFullscreenHeightRef.current = null;
|
|
141
|
+
if (iframeRef.current) {
|
|
142
|
+
iframeRef.current.style.height = `${savedHeight}px`;
|
|
143
|
+
}
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
onSizeChanged?.(params);
|
|
147
|
+
},
|
|
148
|
+
onError: onHostError,
|
|
149
|
+
onRequestDisplayMode: async (params) => {
|
|
150
|
+
if (params.mode === 'fullscreen') {
|
|
151
|
+
// Snapshot current iframe height so we can restore on exit
|
|
152
|
+
if (iframeRef.current) {
|
|
153
|
+
const h = iframeRef.current.getBoundingClientRect().height;
|
|
154
|
+
if (h > 0) preFullscreenHeightRef.current = h;
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
if (containerRef.current?.requestFullscreen) {
|
|
158
|
+
await containerRef.current.requestFullscreen();
|
|
159
|
+
} else if ((containerRef.current as any)?.webkitRequestFullscreen) {
|
|
160
|
+
await (containerRef.current as any).webkitRequestFullscreen();
|
|
161
|
+
}
|
|
162
|
+
setDisplayModeWithRef('fullscreen');
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.warn('[McpAppHost] requestFullscreen failed:', err);
|
|
165
|
+
preFullscreenHeightRef.current = null;
|
|
166
|
+
return { mode: 'inline' };
|
|
167
|
+
}
|
|
168
|
+
} else if (params.mode === 'inline') {
|
|
169
|
+
// Eagerly restore height — don't wait for a guest onsizechange that may never arrive
|
|
170
|
+
restoreHeightAfterFullscreen();
|
|
171
|
+
try {
|
|
172
|
+
if (document.fullscreenElement) {
|
|
173
|
+
await document.exitFullscreen();
|
|
174
|
+
}
|
|
175
|
+
} catch (err) {}
|
|
176
|
+
setDisplayModeWithRef('inline');
|
|
177
|
+
}
|
|
178
|
+
return { mode: params.mode };
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
useImperativeHandle(
|
|
183
|
+
ref,
|
|
184
|
+
() => ({
|
|
185
|
+
teardownResource: (params?: Record<string, unknown>) => {
|
|
186
|
+
host?.teardownResource(params ?? {});
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
[host],
|
|
190
|
+
);
|
|
191
|
+
|
|
79
192
|
const [isLaunched, setIsLaunched] = useState(false);
|
|
80
193
|
const [error, setError] = useState<Error | null>(null);
|
|
81
194
|
|
|
@@ -90,14 +203,38 @@ const McpAppView = memo(function McpAppView({
|
|
|
90
203
|
setError(null);
|
|
91
204
|
}, [resourceUri, appSessionId]);
|
|
92
205
|
|
|
206
|
+
// Eagerly restore the iframe's pre-fullscreen height at every exit point.
|
|
207
|
+
// The guest app may NOT fire onSizeChanged after exiting fullscreen, so we cannot
|
|
208
|
+
// rely on the onSizeChanged interceptor to restore the height.
|
|
209
|
+
const restoreHeightAfterFullscreen = () => {
|
|
210
|
+
const savedHeight = preFullscreenHeightRef.current;
|
|
211
|
+
if (savedHeight && iframeRef.current) {
|
|
212
|
+
iframeRef.current.style.height = `${savedHeight}px`;
|
|
213
|
+
}
|
|
214
|
+
preFullscreenHeightRef.current = null;
|
|
215
|
+
};
|
|
216
|
+
|
|
93
217
|
useEffect(() => {
|
|
94
|
-
|
|
218
|
+
const onFullscreenChange = () => {
|
|
219
|
+
const isFullscreen = !!document.fullscreenElement;
|
|
220
|
+
// Use ref to avoid stale closure (ESC key exit path)
|
|
221
|
+
if (!isFullscreen && displayModeRef.current === 'fullscreen') {
|
|
222
|
+
restoreHeightAfterFullscreen();
|
|
223
|
+
setDisplayModeWithRef('inline');
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
227
|
+
return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
228
|
+
}, []); // stable – reads from refs only, no stale closure over state
|
|
229
|
+
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
if (!host || (!resourceUri && !html)) return;
|
|
95
232
|
|
|
96
233
|
host
|
|
97
|
-
.launch(resourceUri, appSessionId)
|
|
234
|
+
.launch({ uri: resourceUri, html }, appSessionId)
|
|
98
235
|
.then(() => setIsLaunched(true))
|
|
99
236
|
.catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
100
|
-
}, [host, resourceUri, appSessionId]);
|
|
237
|
+
}, [host, resourceUri, html, appSessionId]);
|
|
101
238
|
|
|
102
239
|
useEffect(() => {
|
|
103
240
|
if (!host || !isLaunched || !resourceUri || !appSessionId || !input) return;
|
|
@@ -132,7 +269,30 @@ const McpAppView = memo(function McpAppView({
|
|
|
132
269
|
lastStatusRef.current = status;
|
|
133
270
|
}, [status]);
|
|
134
271
|
|
|
135
|
-
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!host) return;
|
|
274
|
+
// Merge user-provided hostContext with our internal displayMode, then notify the guest.
|
|
275
|
+
// This causes Excalidraw (and other MCP apps) to switch between inline/fullscreen UI mode.
|
|
276
|
+
const mergedCtx = {
|
|
277
|
+
theme: APP_HOST_DEFAULTS.THEME,
|
|
278
|
+
platform: APP_HOST_DEFAULTS.PLATFORM,
|
|
279
|
+
containerDimensions: { maxHeight: APP_HOST_DEFAULTS.MAX_HEIGHT },
|
|
280
|
+
availableDisplayModes: ['inline', 'fullscreen'],
|
|
281
|
+
...(hostContext || {}),
|
|
282
|
+
displayMode, // always override with our authoritative state
|
|
283
|
+
};
|
|
284
|
+
host.setHostContext(mergedCtx);
|
|
285
|
+
}, [host, hostContext, displayMode]);
|
|
286
|
+
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (host && toolInputPartial) host.sendToolInputPartial(toolInputPartial);
|
|
289
|
+
}, [host, toolInputPartial]);
|
|
290
|
+
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (host && toolCancelled) host.sendToolCancelled("User cancelled");
|
|
293
|
+
}, [host, toolCancelled]);
|
|
294
|
+
|
|
295
|
+
if (!metadata && !html && !toolResourceUri) {
|
|
136
296
|
return null;
|
|
137
297
|
}
|
|
138
298
|
|
|
@@ -145,24 +305,55 @@ const McpAppView = memo(function McpAppView({
|
|
|
145
305
|
);
|
|
146
306
|
}
|
|
147
307
|
|
|
308
|
+
const opacityClass = isLaunched ? 'opacity-100' : 'opacity-0';
|
|
309
|
+
let containerClass = `w-full border border-gray-700 rounded bg-transparent my-2 relative ${className || ''}`;
|
|
310
|
+
let iframeClass = `w-full transition-opacity duration-300 ${opacityClass}`;
|
|
311
|
+
|
|
312
|
+
// When native fullscreen is active, the container naturally expands via the browser API.
|
|
313
|
+
// We only need to satisfy flex layout so the iframe fills 100% of the fullscreen viewport.
|
|
314
|
+
if (displayMode === 'fullscreen') {
|
|
315
|
+
containerClass = `w-full h-full bg-black m-0 p-0 flex flex-col relative`;
|
|
316
|
+
iframeClass = `w-full flex-1 transition-opacity duration-300 ${opacityClass}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
148
319
|
return (
|
|
149
|
-
<div
|
|
320
|
+
<div ref={containerRef} className={containerClass}>
|
|
321
|
+
{displayMode === 'fullscreen' && (
|
|
322
|
+
<div className="absolute top-0 right-0 p-2 z-[100000] w-full bg-gradient-to-b from-black/80 to-transparent flex justify-end">
|
|
323
|
+
<button
|
|
324
|
+
title="Exit Fullscreen"
|
|
325
|
+
onClick={() => {
|
|
326
|
+
// Eagerly restore height before the browser animation completes
|
|
327
|
+
restoreHeightAfterFullscreen();
|
|
328
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
329
|
+
setDisplayModeWithRef('inline');
|
|
330
|
+
}}
|
|
331
|
+
className="px-4 py-2 bg-gray-800 hover:bg-gray-700 text-white rounded-md shadow flex items-center gap-2 border border-gray-600 transition-colors"
|
|
332
|
+
>
|
|
333
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/></svg>
|
|
334
|
+
<span className="text-sm font-medium">Exit</span>
|
|
335
|
+
</button>
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
150
338
|
<iframe
|
|
151
339
|
ref={iframeRef}
|
|
152
340
|
sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
|
|
153
|
-
|
|
154
|
-
|
|
341
|
+
allow="fullscreen"
|
|
342
|
+
className={iframeClass}
|
|
155
343
|
title="MCP App"
|
|
156
344
|
/>
|
|
157
|
-
{!isLaunched && (
|
|
158
|
-
<div className="absolute inset-0 bg-
|
|
159
|
-
|
|
345
|
+
{!isLaunched && loader && (
|
|
346
|
+
<div className="absolute inset-0 bg-transparent flex items-center justify-center pointer-events-none z-10">
|
|
347
|
+
{loader}
|
|
160
348
|
</div>
|
|
161
349
|
)}
|
|
162
350
|
</div>
|
|
163
351
|
);
|
|
164
352
|
});
|
|
165
353
|
|
|
354
|
+
const McpAppView = memo(McpAppViewInner);
|
|
355
|
+
McpAppView.displayName = 'McpAppView';
|
|
356
|
+
|
|
166
357
|
/**
|
|
167
358
|
* Helpers scoped to one `mcpClient`. Pass the client here once; `McpAppRenderer` only needs per-tool props (`name`, `input`, `result`, `status`).
|
|
168
359
|
*
|
|
@@ -179,9 +370,13 @@ export function useMcpApps(mcpClient: McpClient | null) {
|
|
|
179
370
|
);
|
|
180
371
|
|
|
181
372
|
const McpAppRenderer = useMemo(() => {
|
|
182
|
-
const
|
|
183
|
-
|
|
373
|
+
const Inner = forwardRef<McpAppRendererHandle, McpAppRendererProps>(function McpAppRenderer(
|
|
374
|
+
props,
|
|
375
|
+
ref,
|
|
376
|
+
) {
|
|
377
|
+
return <McpAppView ref={ref} clientRef={clientRef} {...props} />;
|
|
184
378
|
});
|
|
379
|
+
const Renderer = memo(Inner);
|
|
185
380
|
Renderer.displayName = 'McpAppRenderer';
|
|
186
381
|
return Renderer;
|
|
187
382
|
}, []);
|
|
@@ -343,21 +343,32 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
343
343
|
}
|
|
344
344
|
|
|
345
345
|
case 'auth_required': {
|
|
346
|
-
|
|
347
|
-
if (
|
|
348
|
-
onLog?.('
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
346
|
+
const url = (event.authUrl || '').trim();
|
|
347
|
+
if (!url) {
|
|
348
|
+
onLog?.('error', 'OAuth required but authorization URL is missing', { sessionId: event.sessionId });
|
|
349
|
+
return prev.map((c: McpConnection) =>
|
|
350
|
+
c.sessionId === event.sessionId
|
|
351
|
+
? {
|
|
352
|
+
...c,
|
|
353
|
+
state: 'FAILED',
|
|
354
|
+
error: 'OAuth authorization URL not available',
|
|
355
|
+
authUrl: undefined,
|
|
356
|
+
}
|
|
357
|
+
: c
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
onLog?.('info', `OAuth required - redirecting to ${url}`, { authUrl: url });
|
|
361
|
+
|
|
362
|
+
// Suppress redirects/popups for auto-restore on page load.
|
|
363
|
+
if (!suppressAuthRedirectSessionsRef.current.has(event.sessionId)) {
|
|
364
|
+
if (onRedirect) {
|
|
365
|
+
onRedirect(url);
|
|
366
|
+
} else if (typeof window !== 'undefined') {
|
|
367
|
+
window.location.href = url;
|
|
357
368
|
}
|
|
358
369
|
}
|
|
359
370
|
return prev.map((c: McpConnection) =>
|
|
360
|
-
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl:
|
|
371
|
+
c.sessionId === event.sessionId ? { ...c, state: 'AUTHENTICATING', authUrl: url } : c
|
|
361
372
|
);
|
|
362
373
|
}
|
|
363
374
|
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { APP_HOST_DEFAULTS, SANDBOX_PROXY_READY_METHOD } from '../core/constants.js';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SANDBOX_TIMEOUT_MS = APP_HOST_DEFAULTS.SANDBOX_TIMEOUT_MS;
|
|
4
|
+
|
|
5
|
+
export async function setupSandboxProxyIframe(
|
|
6
|
+
iframe: HTMLIFrameElement,
|
|
7
|
+
sandboxProxyUrl: URL
|
|
8
|
+
): Promise<{
|
|
9
|
+
onReady: Promise<void>;
|
|
10
|
+
}> {
|
|
11
|
+
iframe.style.width = '100%';
|
|
12
|
+
iframe.style.height = '100%';
|
|
13
|
+
iframe.style.border = 'none';
|
|
14
|
+
iframe.style.backgroundColor = 'transparent';
|
|
15
|
+
iframe.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads');
|
|
16
|
+
|
|
17
|
+
const onReady = new Promise<void>((resolve, reject) => {
|
|
18
|
+
let settled = false;
|
|
19
|
+
|
|
20
|
+
const cleanup = () => {
|
|
21
|
+
window.removeEventListener('message', messageListener);
|
|
22
|
+
iframe.removeEventListener('error', errorListener);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const timeoutId = setTimeout(() => {
|
|
26
|
+
if (!settled) {
|
|
27
|
+
settled = true;
|
|
28
|
+
cleanup();
|
|
29
|
+
reject(new Error('Timed out waiting for sandbox proxy iframe to be ready'));
|
|
30
|
+
}
|
|
31
|
+
}, DEFAULT_SANDBOX_TIMEOUT_MS);
|
|
32
|
+
|
|
33
|
+
const messageListener = (event: MessageEvent) => {
|
|
34
|
+
if (event.source === iframe.contentWindow) {
|
|
35
|
+
if (event.data?.method === SANDBOX_PROXY_READY_METHOD) {
|
|
36
|
+
if (!settled) {
|
|
37
|
+
settled = true;
|
|
38
|
+
clearTimeout(timeoutId);
|
|
39
|
+
cleanup();
|
|
40
|
+
resolve();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const errorListener = () => {
|
|
47
|
+
if (!settled) {
|
|
48
|
+
settled = true;
|
|
49
|
+
clearTimeout(timeoutId);
|
|
50
|
+
cleanup();
|
|
51
|
+
reject(new Error('Failed to load sandbox proxy iframe'));
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
window.addEventListener('message', messageListener);
|
|
56
|
+
iframe.addEventListener('error', errorListener);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
iframe.src = sandboxProxyUrl.href;
|
|
60
|
+
|
|
61
|
+
return { onReady };
|
|
62
|
+
}
|
|
@@ -285,22 +285,33 @@ export function useMcp(options: UseMcpOptions): McpClient {
|
|
|
285
285
|
}
|
|
286
286
|
|
|
287
287
|
case 'auth_required': {
|
|
288
|
-
|
|
289
|
-
if (
|
|
290
|
-
onLog?.('
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
288
|
+
const url = (event.authUrl || '').trim();
|
|
289
|
+
if (!url) {
|
|
290
|
+
onLog?.('error', 'OAuth required but authorization URL is missing', { sessionId: event.sessionId });
|
|
291
|
+
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
292
|
+
if (index !== -1) {
|
|
293
|
+
connections.value[index] = {
|
|
294
|
+
...connections.value[index],
|
|
295
|
+
state: 'FAILED',
|
|
296
|
+
error: 'OAuth authorization URL not available',
|
|
297
|
+
authUrl: undefined,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
onLog?.('info', `OAuth required - redirecting to ${url}`, { authUrl: url });
|
|
303
|
+
|
|
304
|
+
// Suppress redirects/popups for background auto-restore on page load.
|
|
305
|
+
if (!suppressAuthRedirectSessions.value.has(event.sessionId)) {
|
|
306
|
+
if (onRedirect) {
|
|
307
|
+
onRedirect(url);
|
|
308
|
+
} else if (typeof window !== 'undefined') {
|
|
309
|
+
window.location.href = url;
|
|
299
310
|
}
|
|
300
311
|
}
|
|
301
312
|
const index = connections.value.findIndex((c) => c.sessionId === event.sessionId);
|
|
302
313
|
if (index !== -1) {
|
|
303
|
-
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl:
|
|
314
|
+
connections.value[index] = { ...connections.value[index], state: 'AUTHENTICATING', authUrl: url };
|
|
304
315
|
}
|
|
305
316
|
break;
|
|
306
317
|
}
|
|
@@ -51,12 +51,12 @@ export type TransportType = 'sse' | 'streamable_http';
|
|
|
51
51
|
*/
|
|
52
52
|
import type { ClientCapabilities } from '@modelcontextprotocol/sdk/types.js';
|
|
53
53
|
|
|
54
|
-
interface McpAppClientCapabilities extends ClientCapabilities {
|
|
54
|
+
interface McpAppClientCapabilities extends Omit<ClientCapabilities, 'extensions'> {
|
|
55
55
|
extensions?: {
|
|
56
56
|
'io.modelcontextprotocol/ui'?: {
|
|
57
57
|
mimeTypes: string[];
|
|
58
58
|
};
|
|
59
|
-
[key: string]:
|
|
59
|
+
[key: string]: any;
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -516,17 +516,40 @@ export class MCPClient {
|
|
|
516
516
|
error instanceof SDKUnauthorizedError ||
|
|
517
517
|
(error instanceof Error && error.message.toLowerCase().includes('unauthorized'))
|
|
518
518
|
) {
|
|
519
|
+
/** Set when the SDK calls redirectToAuthorization on the OAuth provider */
|
|
520
|
+
let authUrl = '';
|
|
521
|
+
if (this.oauthProvider) {
|
|
522
|
+
authUrl = (this.oauthProvider.authUrl || '').trim();
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* 401 without a usable URL means metadata/DCR failed or the server never started
|
|
527
|
+
* an interactive OAuth flow — not recoverable as "pending OAuth".
|
|
528
|
+
*/
|
|
529
|
+
if (!authUrl) {
|
|
530
|
+
const detail =
|
|
531
|
+
error instanceof Error && error.message.trim().length > 0
|
|
532
|
+
? error.message.trim()
|
|
533
|
+
: 'Unauthorized';
|
|
534
|
+
const message =
|
|
535
|
+
detail.toLowerCase() === 'unauthorized'
|
|
536
|
+
? 'OAuth authorization URL not available'
|
|
537
|
+
: `OAuth authorization URL not available: ${detail}`;
|
|
538
|
+
this.emitError(message, 'auth');
|
|
539
|
+
this.emitStateChange('FAILED');
|
|
540
|
+
try {
|
|
541
|
+
await storage.removeSession(this.identity, this.sessionId);
|
|
542
|
+
} catch {
|
|
543
|
+
// best-effort cleanup
|
|
544
|
+
}
|
|
545
|
+
throw new Error(message);
|
|
546
|
+
}
|
|
547
|
+
|
|
519
548
|
this.emitStateChange('AUTHENTICATING');
|
|
520
549
|
// Save session with 10min TTL for OAuth pending state
|
|
521
550
|
console.log(`[MCPClient] Saving session ${this.sessionId} with 10min TTL (OAuth pending)`);
|
|
522
551
|
await this.saveSession(Math.floor(STATE_EXPIRATION_MS / 1000), false);
|
|
523
552
|
|
|
524
|
-
/** Get OAuth authorization URL if available */
|
|
525
|
-
let authUrl = '';
|
|
526
|
-
if (this.oauthProvider) {
|
|
527
|
-
authUrl = this.oauthProvider.authUrl || '';
|
|
528
|
-
}
|
|
529
|
-
|
|
530
553
|
if (this.serverId) {
|
|
531
554
|
this._onConnectionEvent.fire({
|
|
532
555
|
type: 'auth_required',
|