@mcp-ts/sdk 1.3.10 → 1.5.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/README.md +20 -27
- package/dist/adapters/agui-adapter.d.mts +16 -0
- package/dist/adapters/agui-adapter.d.ts +16 -0
- package/dist/adapters/agui-adapter.js +185 -0
- package/dist/adapters/agui-adapter.js.map +1 -1
- package/dist/adapters/agui-adapter.mjs +185 -0
- package/dist/adapters/agui-adapter.mjs.map +1 -1
- package/dist/adapters/agui-middleware.d.mts +2 -0
- package/dist/adapters/agui-middleware.d.ts +2 -0
- package/dist/adapters/agui-middleware.js.map +1 -1
- package/dist/adapters/agui-middleware.mjs.map +1 -1
- package/dist/adapters/ai-adapter.d.mts +21 -0
- package/dist/adapters/ai-adapter.d.ts +21 -0
- package/dist/adapters/ai-adapter.js +175 -0
- package/dist/adapters/ai-adapter.js.map +1 -1
- package/dist/adapters/ai-adapter.mjs +175 -0
- package/dist/adapters/ai-adapter.mjs.map +1 -1
- package/dist/adapters/langchain-adapter.d.mts +16 -0
- package/dist/adapters/langchain-adapter.d.ts +16 -0
- package/dist/adapters/langchain-adapter.js +179 -0
- package/dist/adapters/langchain-adapter.js.map +1 -1
- package/dist/adapters/langchain-adapter.mjs +179 -0
- package/dist/adapters/langchain-adapter.mjs.map +1 -1
- package/dist/client/index.d.mts +4 -190
- package/dist/client/index.d.ts +4 -190
- 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 +31 -17
- package/dist/client/react.d.ts +31 -17
- package/dist/client/react.js +447 -103
- package/dist/client/react.js.map +1 -1
- package/dist/client/react.mjs +443 -105
- package/dist/client/react.mjs.map +1 -1
- package/dist/client/vue.d.mts +5 -4
- package/dist/client/vue.d.ts +5 -4
- 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-DcYfpY3H.d.mts +295 -0
- package/dist/index-GfC_eNEv.d.ts +295 -0
- package/dist/index.d.mts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1120 -59
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1097 -60
- package/dist/index.mjs.map +1 -1
- package/dist/server/index.d.mts +2 -2
- package/dist/server/index.d.ts +2 -2
- package/dist/server/index.js +18 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +18 -5
- package/dist/server/index.mjs.map +1 -1
- package/dist/shared/index.d.mts +86 -4
- package/dist/shared/index.d.ts +86 -4
- package/dist/shared/index.js +874 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/index.mjs +865 -1
- package/dist/shared/index.mjs.map +1 -1
- package/dist/tool-router-Bo8qZbsD.d.ts +325 -0
- package/dist/tool-router-XnWVxPzv.d.mts +325 -0
- package/dist/{types-CW6lghof.d.mts → types-CfCoIsWI.d.mts} +27 -1
- package/dist/{types-CW6lghof.d.ts → types-CfCoIsWI.d.ts} +27 -1
- package/package.json +15 -12
- package/src/adapters/agui-adapter.ts +79 -0
- package/src/adapters/ai-adapter.ts +75 -0
- package/src/adapters/langchain-adapter.ts +75 -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 +3 -0
- package/src/client/react/use-app-host.ts +8 -15
- package/src/client/react/use-mcp-apps.tsx +262 -49
- 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/index.ts +2 -0
- package/src/server/mcp/oauth-client.ts +34 -9
- package/src/shared/index.ts +36 -0
- package/src/shared/meta-tools.ts +387 -0
- package/src/shared/schema-compressor.ts +124 -0
- package/src/shared/tool-index.ts +499 -0
- package/src/shared/tool-router.ts +469 -0
- package/src/shared/types.ts +30 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default configuration values for the App Host.
|
|
3
|
+
*
|
|
4
|
+
* `SANDBOX_*_READY_METHOD` match `@modelcontextprotocol/ext-apps` (see
|
|
5
|
+
* https://github.com/modelcontextprotocol/ext-apps/blob/main/src/types.ts ).
|
|
6
|
+
* Duplicated here because the package root `app.d.ts` often omits these value exports under
|
|
7
|
+
* `moduleResolution: "NodeNext"`.
|
|
8
|
+
*/
|
|
9
|
+
export const SANDBOX_PROXY_READY_METHOD = 'ui/notifications/sandbox-proxy-ready' as const;
|
|
10
|
+
export const SANDBOX_RESOURCE_READY_METHOD = 'ui/notifications/sandbox-resource-ready' as const;
|
|
11
|
+
|
|
12
|
+
export const APP_HOST_DEFAULTS = {
|
|
13
|
+
/** Default timeout for waiting for the sandbox proxy to be ready (ms). */
|
|
14
|
+
SANDBOX_TIMEOUT_MS: 10000,
|
|
15
|
+
|
|
16
|
+
/** Default host info reported to guest apps. */
|
|
17
|
+
HOST_INFO: { name: 'mcp-ts-host', version: '1.0.0' },
|
|
18
|
+
|
|
19
|
+
/** Supported MCP App URI schemes. */
|
|
20
|
+
URI_SCHEMES: ['ui://', 'mcp-app://'] as const,
|
|
21
|
+
|
|
22
|
+
/** Default theme for the host context. */
|
|
23
|
+
THEME: 'dark',
|
|
24
|
+
|
|
25
|
+
/** Default platform for the host context. */
|
|
26
|
+
PLATFORM: 'web',
|
|
27
|
+
|
|
28
|
+
/** Default max height for the iframe container (px). */
|
|
29
|
+
MAX_HEIGHT: 6000,
|
|
30
|
+
} as const;
|
package/src/client/index.ts
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
/** SSE client for real-time connections */
|
|
7
7
|
export { SSEClient, type SSEClientOptions } from './core/sse-client';
|
|
8
|
-
export { AppHost } from './core/app-host';
|
|
8
|
+
export { AppHost, DEFAULT_MCP_APP_CSP } from './core/app-host';
|
|
9
|
+
export {
|
|
10
|
+
APP_HOST_DEFAULTS,
|
|
11
|
+
SANDBOX_PROXY_READY_METHOD,
|
|
12
|
+
SANDBOX_RESOURCE_READY_METHOD,
|
|
13
|
+
} from './core/constants.js';
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
|
|
@@ -12,7 +12,10 @@ export { useAppHost } from './use-app-host.js';
|
|
|
12
12
|
// Simplified MCP Apps Hook - the main API
|
|
13
13
|
export {
|
|
14
14
|
useMcpApps,
|
|
15
|
+
McpAppRenderer,
|
|
16
|
+
getMcpAppMetadata,
|
|
15
17
|
type McpAppRendererProps,
|
|
18
|
+
type McpAppRendererHandle,
|
|
16
19
|
type McpAppMetadata,
|
|
17
20
|
} from './use-mcp-apps.js';
|
|
18
21
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
import { AppHost } from '../core/app-host';
|
|
2
|
+
import type { AppHostClient } from '../core/types';
|
|
3
|
+
import { AppHost, type AppHostOptions } from '../core/app-host';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Hook to host an MCP App in a React component
|
|
@@ -16,19 +16,17 @@ import { AppHost } from '../core/app-host';
|
|
|
16
16
|
* @param options - Optional configuration
|
|
17
17
|
* @returns Object containing the AppHost instance (or null) and error state
|
|
18
18
|
*/
|
|
19
|
+
export type UseAppHostOptions = AppHostOptions;
|
|
20
|
+
|
|
19
21
|
export function useAppHost(
|
|
20
|
-
client:
|
|
22
|
+
client: AppHostClient | null,
|
|
21
23
|
iframeRef: React.RefObject<HTMLIFrameElement>,
|
|
22
|
-
options?:
|
|
23
|
-
/** Callback when the App sends a message (e.g. to chat) */
|
|
24
|
-
onMessage?: (params: { role: string; content: unknown }) => void;
|
|
25
|
-
}
|
|
24
|
+
options?: UseAppHostOptions
|
|
26
25
|
) {
|
|
27
26
|
const [host, setHost] = useState<AppHost | null>(null);
|
|
28
27
|
const [error, setError] = useState<Error | null>(null);
|
|
29
28
|
const initializingRef = useRef(false);
|
|
30
29
|
|
|
31
|
-
// Store latest callback in ref to avoid re-initializing AppHost on callback change
|
|
32
30
|
const onMessageRef = useRef(options?.onMessage);
|
|
33
31
|
useEffect(() => {
|
|
34
32
|
onMessageRef.current = options?.onMessage;
|
|
@@ -42,13 +40,8 @@ export function useAppHost(
|
|
|
42
40
|
|
|
43
41
|
const initHost = async () => {
|
|
44
42
|
try {
|
|
45
|
-
// Initialize AppHost with security enforcement
|
|
46
|
-
const appHost = new AppHost(client, iframeRef.current
|
|
47
|
-
|
|
48
|
-
// Register message handler
|
|
49
|
-
appHost.onAppMessage = (params) => {
|
|
50
|
-
onMessageRef.current?.(params);
|
|
51
|
-
};
|
|
43
|
+
// Initialize AppHost with security enforcement and options
|
|
44
|
+
const appHost = new AppHost(client, iframeRef.current!, options);
|
|
52
45
|
|
|
53
46
|
// Set host immediately so launch can be called
|
|
54
47
|
// (launch will wait for bridge if needed)
|
|
@@ -11,10 +11,16 @@ import React, {
|
|
|
11
11
|
useRef,
|
|
12
12
|
memo,
|
|
13
13
|
useMemo,
|
|
14
|
+
forwardRef,
|
|
15
|
+
useImperativeHandle,
|
|
14
16
|
type MutableRefObject,
|
|
15
17
|
} from 'react';
|
|
18
|
+
import type { UseAppHostOptions } from './use-app-host.js';
|
|
16
19
|
import { useAppHost } from './use-app-host.js';
|
|
20
|
+
import { resolveMetaToolProxy } from '../../shared/meta-tools.js';
|
|
17
21
|
import type { SSEClient } from '../core/sse-client.js';
|
|
22
|
+
import { APP_HOST_DEFAULTS } from '../core/constants.js';
|
|
23
|
+
import type { SandboxConfig } from '../core/app-host.js';
|
|
18
24
|
|
|
19
25
|
export interface McpClient {
|
|
20
26
|
connections: Array<{
|
|
@@ -41,14 +47,27 @@ export interface McpAppMetadata {
|
|
|
41
47
|
sessionId: string;
|
|
42
48
|
}
|
|
43
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Imperative handle for {@link useMcpApps}'s `McpAppRenderer` (via `ref`),
|
|
52
|
+
* aligned with `@mcp-ui/client`'s `AppRendererHandle.teardownResource`.
|
|
53
|
+
*/
|
|
54
|
+
export interface McpAppRendererHandle {
|
|
55
|
+
teardownResource: (params?: Record<string, unknown>) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
/** Props for {@link useMcpApps}'s `McpAppRenderer` (client is supplied via the hook). */
|
|
45
|
-
export interface McpAppRendererProps {
|
|
59
|
+
export interface McpAppRendererProps extends Pick<UseAppHostOptions, 'sandbox' | 'hostContext' | 'onCallTool' | 'onReadResource' | 'onFallbackRequest' | 'onMessage' | 'onOpenLink' | 'onLoggingMessage' | 'onSizeChanged' | 'onError'> {
|
|
46
60
|
name: string;
|
|
47
|
-
|
|
61
|
+
client?: McpClient | null;
|
|
62
|
+
toolResourceUri?: string;
|
|
63
|
+
html?: string;
|
|
64
|
+
input?: Record<string, unknown> | null;
|
|
48
65
|
result?: unknown;
|
|
49
|
-
status
|
|
50
|
-
|
|
66
|
+
status?: 'executing' | 'inProgress' | 'complete' | 'idle';
|
|
67
|
+
toolInputPartial?: any;
|
|
68
|
+
toolCancelled?: boolean;
|
|
51
69
|
className?: string;
|
|
70
|
+
loader?: React.ReactNode;
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
type McpAppViewProps = McpAppRendererProps & {
|
|
@@ -60,28 +79,127 @@ type McpAppViewProps = McpAppRendererProps & {
|
|
|
60
79
|
};
|
|
61
80
|
|
|
62
81
|
/** 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
|
-
|
|
82
|
+
const McpAppViewInner = forwardRef<McpAppRendererHandle, McpAppViewProps>(function McpAppView(
|
|
83
|
+
{
|
|
84
|
+
clientRef,
|
|
85
|
+
name,
|
|
86
|
+
toolResourceUri,
|
|
87
|
+
html,
|
|
88
|
+
input,
|
|
89
|
+
result,
|
|
90
|
+
status = 'idle',
|
|
91
|
+
toolInputPartial,
|
|
92
|
+
toolCancelled,
|
|
93
|
+
sandbox,
|
|
94
|
+
hostContext,
|
|
95
|
+
onCallTool,
|
|
96
|
+
onReadResource,
|
|
97
|
+
onFallbackRequest,
|
|
98
|
+
onMessage,
|
|
99
|
+
onOpenLink,
|
|
100
|
+
onLoggingMessage,
|
|
101
|
+
onSizeChanged,
|
|
102
|
+
onError: onHostError,
|
|
103
|
+
className,
|
|
104
|
+
loader,
|
|
105
|
+
},
|
|
106
|
+
ref,
|
|
107
|
+
) {
|
|
108
|
+
|
|
71
109
|
const mcpClient = clientRef.current;
|
|
72
|
-
const
|
|
110
|
+
const { toolName: resolvedToolName, args: resolvedInput } = resolveMetaToolProxy(name, input);
|
|
111
|
+
const metadata = getMcpAppMetadata(mcpClient, resolvedToolName, resolvedInput);
|
|
73
112
|
const sseClient = mcpClient?.sseClient ?? null;
|
|
74
|
-
const resourceUri = metadata?.resourceUri;
|
|
113
|
+
const resourceUri = toolResourceUri || metadata?.resourceUri;
|
|
75
114
|
const appSessionId = metadata?.sessionId;
|
|
76
115
|
|
|
77
116
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
78
|
-
const
|
|
117
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
118
|
+
// Tracks the last height (px) reported by the guest before/after fullscreen so we
|
|
119
|
+
// can restore it when the native fullscreen API exits and the guest fires a stale
|
|
120
|
+
// resize event that would otherwise collapse the iframe to 0.
|
|
121
|
+
const preFullscreenHeightRef = useRef<number | null>(null);
|
|
122
|
+
const displayModeRef = useRef<'inline' | 'fullscreen'>('inline');
|
|
123
|
+
const [displayMode, setDisplayMode] = useState<'inline' | 'fullscreen'>('inline');
|
|
124
|
+
|
|
125
|
+
const setDisplayModeWithRef = (mode: 'inline' | 'fullscreen') => {
|
|
126
|
+
displayModeRef.current = mode;
|
|
127
|
+
setDisplayMode(mode);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const { host, error: hostError } = useAppHost(sseClient as any, iframeRef, {
|
|
131
|
+
sandbox,
|
|
132
|
+
hostContext,
|
|
133
|
+
onCallTool,
|
|
134
|
+
onReadResource,
|
|
135
|
+
onFallbackRequest,
|
|
136
|
+
onMessage,
|
|
137
|
+
onOpenLink,
|
|
138
|
+
onLoggingMessage,
|
|
139
|
+
// Intercept onSizeChanged: when exiting fullscreen, ignore guest resize events
|
|
140
|
+
// that arrive with the shrunken viewport dimensions, and restore the pre-fullscreen height.
|
|
141
|
+
onSizeChanged: (params) => {
|
|
142
|
+
if (displayModeRef.current === 'inline' && preFullscreenHeightRef.current !== null) {
|
|
143
|
+
// Guest fired a resize right after fullscreen exit – restore the saved height
|
|
144
|
+
const savedHeight = preFullscreenHeightRef.current;
|
|
145
|
+
preFullscreenHeightRef.current = null;
|
|
146
|
+
if (iframeRef.current) {
|
|
147
|
+
iframeRef.current.style.height = `${savedHeight}px`;
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
onSizeChanged?.(params);
|
|
152
|
+
},
|
|
153
|
+
onError: onHostError,
|
|
154
|
+
onRequestDisplayMode: async (params) => {
|
|
155
|
+
if (params.mode === 'fullscreen') {
|
|
156
|
+
// Snapshot current iframe height so we can restore on exit
|
|
157
|
+
if (iframeRef.current) {
|
|
158
|
+
const h = iframeRef.current.getBoundingClientRect().height;
|
|
159
|
+
if (h > 0) preFullscreenHeightRef.current = h;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
if (containerRef.current?.requestFullscreen) {
|
|
163
|
+
await containerRef.current.requestFullscreen();
|
|
164
|
+
} else if ((containerRef.current as any)?.webkitRequestFullscreen) {
|
|
165
|
+
await (containerRef.current as any).webkitRequestFullscreen();
|
|
166
|
+
}
|
|
167
|
+
setDisplayModeWithRef('fullscreen');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
console.warn('[McpAppHost] requestFullscreen failed:', err);
|
|
170
|
+
preFullscreenHeightRef.current = null;
|
|
171
|
+
return { mode: 'inline' };
|
|
172
|
+
}
|
|
173
|
+
} else if (params.mode === 'inline') {
|
|
174
|
+
// Eagerly restore height — don't wait for a guest onsizechange that may never arrive
|
|
175
|
+
restoreHeightAfterFullscreen();
|
|
176
|
+
try {
|
|
177
|
+
if (document.fullscreenElement) {
|
|
178
|
+
await document.exitFullscreen();
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {}
|
|
181
|
+
setDisplayModeWithRef('inline');
|
|
182
|
+
}
|
|
183
|
+
return { mode: params.mode };
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
useImperativeHandle(
|
|
188
|
+
ref,
|
|
189
|
+
() => ({
|
|
190
|
+
teardownResource: (params?: Record<string, unknown>) => {
|
|
191
|
+
host?.teardownResource(params ?? {});
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
[host],
|
|
195
|
+
);
|
|
196
|
+
|
|
79
197
|
const [isLaunched, setIsLaunched] = useState(false);
|
|
80
198
|
const [error, setError] = useState<Error | null>(null);
|
|
81
199
|
|
|
82
200
|
const sentInputRef = useRef(false);
|
|
83
201
|
const sentResultRef = useRef(false);
|
|
84
|
-
const lastInputRef = useRef(
|
|
202
|
+
const lastInputRef = useRef(resolvedInput);
|
|
85
203
|
const lastResultRef = useRef(result);
|
|
86
204
|
const lastStatusRef = useRef(status);
|
|
87
205
|
|
|
@@ -90,24 +208,49 @@ const McpAppView = memo(function McpAppView({
|
|
|
90
208
|
setError(null);
|
|
91
209
|
}, [resourceUri, appSessionId]);
|
|
92
210
|
|
|
211
|
+
// Eagerly restore the iframe's pre-fullscreen height at every exit point.
|
|
212
|
+
// The guest app may NOT fire onSizeChanged after exiting fullscreen, so we cannot
|
|
213
|
+
// rely on the onSizeChanged interceptor to restore the height.
|
|
214
|
+
const restoreHeightAfterFullscreen = () => {
|
|
215
|
+
const savedHeight = preFullscreenHeightRef.current;
|
|
216
|
+
if (savedHeight && iframeRef.current) {
|
|
217
|
+
iframeRef.current.style.height = `${savedHeight}px`;
|
|
218
|
+
}
|
|
219
|
+
preFullscreenHeightRef.current = null;
|
|
220
|
+
};
|
|
221
|
+
|
|
93
222
|
useEffect(() => {
|
|
94
|
-
|
|
223
|
+
const onFullscreenChange = () => {
|
|
224
|
+
const isFullscreen = !!document.fullscreenElement;
|
|
225
|
+
// Use ref to avoid stale closure (ESC key exit path)
|
|
226
|
+
if (!isFullscreen && displayModeRef.current === 'fullscreen') {
|
|
227
|
+
restoreHeightAfterFullscreen();
|
|
228
|
+
setDisplayModeWithRef('inline');
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
document.addEventListener('fullscreenchange', onFullscreenChange);
|
|
232
|
+
return () => document.removeEventListener('fullscreenchange', onFullscreenChange);
|
|
233
|
+
}, []); // stable – reads from refs only, no stale closure over state
|
|
234
|
+
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
if (!host || (!resourceUri && !html)) return;
|
|
95
237
|
|
|
96
238
|
host
|
|
97
|
-
.launch(resourceUri, appSessionId)
|
|
239
|
+
.launch({ uri: resourceUri, html }, appSessionId)
|
|
98
240
|
.then(() => setIsLaunched(true))
|
|
99
241
|
.catch((err) => setError(err instanceof Error ? err : new Error(String(err))));
|
|
100
|
-
}, [host, resourceUri, appSessionId]);
|
|
242
|
+
}, [host, resourceUri, html, appSessionId]);
|
|
101
243
|
|
|
244
|
+
// Send tool inputs
|
|
102
245
|
useEffect(() => {
|
|
103
|
-
if (!host || !isLaunched || !resourceUri || !appSessionId || !
|
|
246
|
+
if (!host || !isLaunched || !resourceUri || !appSessionId || !resolvedInput) return;
|
|
104
247
|
|
|
105
|
-
if (!sentInputRef.current || JSON.stringify(
|
|
248
|
+
if (!sentInputRef.current || JSON.stringify(resolvedInput) !== JSON.stringify(lastInputRef.current)) {
|
|
106
249
|
sentInputRef.current = true;
|
|
107
|
-
lastInputRef.current =
|
|
108
|
-
host.sendToolInput(
|
|
250
|
+
lastInputRef.current = resolvedInput;
|
|
251
|
+
host.sendToolInput(resolvedInput);
|
|
109
252
|
}
|
|
110
|
-
}, [host, isLaunched,
|
|
253
|
+
}, [host, isLaunched, resolvedInput, resourceUri, appSessionId, resolvedToolName]);
|
|
111
254
|
|
|
112
255
|
useEffect(() => {
|
|
113
256
|
if (!host || !isLaunched || !resourceUri || !appSessionId || result === undefined) return;
|
|
@@ -122,7 +265,7 @@ const McpAppView = memo(function McpAppView({
|
|
|
122
265
|
: result;
|
|
123
266
|
host.sendToolResult(formattedResult);
|
|
124
267
|
}
|
|
125
|
-
}, [host, isLaunched, result, status, resourceUri, appSessionId,
|
|
268
|
+
}, [host, isLaunched, result, status, resourceUri, appSessionId, resolvedToolName]);
|
|
126
269
|
|
|
127
270
|
useEffect(() => {
|
|
128
271
|
if (status === 'executing' && lastStatusRef.current !== 'executing') {
|
|
@@ -132,7 +275,30 @@ const McpAppView = memo(function McpAppView({
|
|
|
132
275
|
lastStatusRef.current = status;
|
|
133
276
|
}, [status]);
|
|
134
277
|
|
|
135
|
-
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!host) return;
|
|
280
|
+
// Merge user-provided hostContext with our internal displayMode, then notify the guest.
|
|
281
|
+
// This causes Excalidraw (and other MCP apps) to switch between inline/fullscreen UI mode.
|
|
282
|
+
const mergedCtx = {
|
|
283
|
+
theme: APP_HOST_DEFAULTS.THEME,
|
|
284
|
+
platform: APP_HOST_DEFAULTS.PLATFORM,
|
|
285
|
+
containerDimensions: { maxHeight: APP_HOST_DEFAULTS.MAX_HEIGHT },
|
|
286
|
+
availableDisplayModes: ['inline', 'fullscreen'],
|
|
287
|
+
...(hostContext || {}),
|
|
288
|
+
displayMode, // always override with our authoritative state
|
|
289
|
+
};
|
|
290
|
+
host.setHostContext(mergedCtx);
|
|
291
|
+
}, [host, hostContext, displayMode]);
|
|
292
|
+
|
|
293
|
+
useEffect(() => {
|
|
294
|
+
if (host && toolInputPartial) host.sendToolInputPartial(toolInputPartial);
|
|
295
|
+
}, [host, toolInputPartial]);
|
|
296
|
+
|
|
297
|
+
useEffect(() => {
|
|
298
|
+
if (host && toolCancelled) host.sendToolCancelled("User cancelled");
|
|
299
|
+
}, [host, toolCancelled]);
|
|
300
|
+
|
|
301
|
+
if (!metadata && !html && !toolResourceUri) {
|
|
136
302
|
return null;
|
|
137
303
|
}
|
|
138
304
|
|
|
@@ -145,48 +311,93 @@ const McpAppView = memo(function McpAppView({
|
|
|
145
311
|
);
|
|
146
312
|
}
|
|
147
313
|
|
|
314
|
+
const opacityClass = isLaunched ? 'opacity-100' : 'opacity-0';
|
|
315
|
+
let containerClass = `w-full border border-gray-700 rounded bg-transparent my-2 relative ${className || ''}`;
|
|
316
|
+
let iframeClass = `w-full transition-opacity duration-300 ${opacityClass}`;
|
|
317
|
+
|
|
318
|
+
// When native fullscreen is active, the container naturally expands via the browser API.
|
|
319
|
+
// We only need to satisfy flex layout so the iframe fills 100% of the fullscreen viewport.
|
|
320
|
+
if (displayMode === 'fullscreen') {
|
|
321
|
+
containerClass = `w-full h-full bg-black m-0 p-0 flex flex-col relative`;
|
|
322
|
+
iframeClass = `w-full flex-1 transition-opacity duration-300 ${opacityClass}`;
|
|
323
|
+
}
|
|
324
|
+
|
|
148
325
|
return (
|
|
149
|
-
<div
|
|
326
|
+
<div ref={containerRef} className={containerClass}>
|
|
327
|
+
{displayMode === 'fullscreen' && (
|
|
328
|
+
<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">
|
|
329
|
+
<button
|
|
330
|
+
title="Exit Fullscreen"
|
|
331
|
+
onClick={() => {
|
|
332
|
+
// Eagerly restore height before the browser animation completes
|
|
333
|
+
restoreHeightAfterFullscreen();
|
|
334
|
+
if (document.fullscreenElement) document.exitFullscreen();
|
|
335
|
+
setDisplayModeWithRef('inline');
|
|
336
|
+
}}
|
|
337
|
+
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"
|
|
338
|
+
>
|
|
339
|
+
<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>
|
|
340
|
+
<span className="text-sm font-medium">Exit</span>
|
|
341
|
+
</button>
|
|
342
|
+
</div>
|
|
343
|
+
)}
|
|
150
344
|
<iframe
|
|
151
345
|
ref={iframeRef}
|
|
152
346
|
sandbox="allow-scripts allow-same-origin allow-forms allow-modals allow-popups allow-downloads"
|
|
153
|
-
|
|
154
|
-
|
|
347
|
+
allow="fullscreen"
|
|
348
|
+
className={iframeClass}
|
|
155
349
|
title="MCP App"
|
|
156
350
|
/>
|
|
157
|
-
{!isLaunched && (
|
|
158
|
-
<div className="absolute inset-0 bg-
|
|
159
|
-
|
|
351
|
+
{!isLaunched && loader && (
|
|
352
|
+
<div className="absolute inset-0 bg-transparent flex items-center justify-center pointer-events-none z-10">
|
|
353
|
+
{loader}
|
|
160
354
|
</div>
|
|
161
355
|
)}
|
|
162
356
|
</div>
|
|
163
357
|
);
|
|
164
358
|
});
|
|
165
359
|
|
|
360
|
+
const McpAppView = memo(McpAppViewInner);
|
|
361
|
+
McpAppView.displayName = 'McpAppView';
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Renders an interactive MCP application inside a sandboxed iframe.
|
|
365
|
+
*/
|
|
366
|
+
export const McpAppRenderer = memo(
|
|
367
|
+
forwardRef<McpAppRendererHandle, McpAppRendererProps>(function McpAppRenderer(
|
|
368
|
+
{ client, ...props },
|
|
369
|
+
ref
|
|
370
|
+
) {
|
|
371
|
+
const clientRef = useRef(client || null);
|
|
372
|
+
clientRef.current = client || null;
|
|
373
|
+
|
|
374
|
+
return <McpAppView ref={ref} clientRef={clientRef} {...props} />;
|
|
375
|
+
})
|
|
376
|
+
);
|
|
377
|
+
|
|
166
378
|
/**
|
|
167
379
|
* Helpers scoped to one `mcpClient`. Pass the client here once; `McpAppRenderer` only needs per-tool props (`name`, `input`, `result`, `status`).
|
|
168
380
|
*
|
|
169
381
|
* @param mcpClient - From `useMcp()` or context (for example `useMcpContext()`).
|
|
382
|
+
* @deprecated Use the standalone `<McpAppRenderer>` component and `getMcpAppMetadata` utility directly.
|
|
170
383
|
*/
|
|
171
384
|
export function useMcpApps(mcpClient: McpClient | null) {
|
|
172
|
-
// Stable `McpAppRenderer` type: parent re-renders and `connections` updates must not remount the iframe.
|
|
173
|
-
const clientRef = useRef(mcpClient);
|
|
174
|
-
clientRef.current = mcpClient;
|
|
175
|
-
|
|
176
385
|
const getAppMetadata = useCallback(
|
|
177
|
-
(toolName: string) => getMcpAppMetadata(
|
|
178
|
-
[]
|
|
386
|
+
(toolName: string) => getMcpAppMetadata(mcpClient, toolName),
|
|
387
|
+
[mcpClient]
|
|
179
388
|
);
|
|
180
389
|
|
|
181
|
-
const
|
|
182
|
-
const Renderer =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
390
|
+
const BoundMcpAppRenderer = useMemo(() => {
|
|
391
|
+
const Renderer = forwardRef<McpAppRendererHandle, Omit<McpAppRendererProps, 'client'>>(
|
|
392
|
+
function BoundMcpAppRenderer(props, ref) {
|
|
393
|
+
return <McpAppRenderer ref={ref} client={mcpClient} {...props} />;
|
|
394
|
+
}
|
|
395
|
+
);
|
|
396
|
+
Renderer.displayName = 'BoundMcpAppRenderer';
|
|
397
|
+
return memo(Renderer);
|
|
398
|
+
}, [mcpClient]);
|
|
188
399
|
|
|
189
|
-
return { getAppMetadata, McpAppRenderer };
|
|
400
|
+
return { getAppMetadata, McpAppRenderer: BoundMcpAppRenderer };
|
|
190
401
|
}
|
|
191
402
|
|
|
192
403
|
function extractToolName(fullName: string): string {
|
|
@@ -194,13 +405,15 @@ function extractToolName(fullName: string): string {
|
|
|
194
405
|
return match?.[1] || fullName;
|
|
195
406
|
}
|
|
196
407
|
|
|
197
|
-
function getMcpAppMetadata(
|
|
408
|
+
export function getMcpAppMetadata(
|
|
198
409
|
mcpClient: McpClient | null,
|
|
199
|
-
toolName: string
|
|
410
|
+
toolName: string,
|
|
411
|
+
input?: Record<string, unknown> | null
|
|
200
412
|
): McpAppMetadata | undefined {
|
|
201
413
|
if (!mcpClient) return undefined;
|
|
202
414
|
|
|
203
|
-
const
|
|
415
|
+
const { toolName: proxyToolName } = resolveMetaToolProxy(toolName, input);
|
|
416
|
+
const extractedName = extractToolName(proxyToolName);
|
|
204
417
|
|
|
205
418
|
for (const conn of mcpClient.connections) {
|
|
206
419
|
for (const tool of conn.tools) {
|
|
@@ -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
|
+
}
|