@agent-native/core 0.72.3 → 0.72.4
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/package.json +6 -6
- package/corpus/core/src/a2a/client.ts +7 -3
- package/corpus/core/src/client/api-path.ts +28 -0
- package/corpus/core/src/client/embed-auth.ts +26 -0
- package/corpus/core/src/client/mcp-app-host.ts +6 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
- package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
- package/corpus/core/src/embedding/agent.ts +6 -1
- package/corpus/core/src/embedding/bridge.ts +34 -1
- package/corpus/core/src/mcp/build-server.ts +38 -10
- package/corpus/core/src/mcp/embed-app.ts +120 -45
- package/corpus/core/src/mcp/index.ts +5 -0
- package/corpus/core/src/mcp/org-directory.ts +70 -11
- package/corpus/core/src/mcp/server.ts +14 -0
- package/corpus/core/src/mcp-client/config.ts +12 -0
- package/corpus/core/src/mcp-client/index.ts +2 -0
- package/corpus/core/src/mcp-client/manager.ts +268 -9
- package/corpus/core/src/mcp-client/remote-store.ts +154 -0
- package/corpus/core/src/mcp-client/routes.ts +2 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
- package/corpus/core/src/vite/client.ts +53 -0
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
- package/corpus/templates/assets/AGENTS.md +7 -5
- package/corpus/templates/assets/actions/generate-asset.ts +161 -0
- package/corpus/templates/assets/actions/list-assets.ts +4 -2
- package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
- package/corpus/templates/assets/app/routes/library.tsx +243 -40
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
- package/corpus/templates/design/AGENTS.md +10 -5
- package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
- package/corpus/templates/design/actions/insert-asset.ts +219 -0
- package/corpus/templates/design/actions/view-screen.ts +3 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
- package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
- package/dist/a2a/client.d.ts +1 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +5 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/client/api-path.d.ts.map +1 -1
- package/dist/client/api-path.js +27 -0
- package/dist/client/api-path.js.map +1 -1
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +27 -0
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +5 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +3 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/demo/fetch-interceptor.js +1 -1
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/embedding/agent.d.ts +1 -1
- package/dist/embedding/agent.d.ts.map +1 -1
- package/dist/embedding/agent.js +1 -1
- package/dist/embedding/agent.js.map +1 -1
- package/dist/embedding/bridge.d.ts.map +1 -1
- package/dist/embedding/bridge.js +27 -1
- package/dist/embedding/bridge.js.map +1 -1
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +32 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +120 -45
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/org-directory.d.ts +1 -0
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +59 -11
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/config.d.ts +12 -0
- package/dist/mcp-client/config.d.ts.map +1 -1
- package/dist/mcp-client/config.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/manager.d.ts +2 -0
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +201 -9
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/remote-store.d.ts +27 -0
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +105 -0
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +1 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
- package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +8 -8
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +8 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +3 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +40 -3
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +44 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +6 -6
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.72.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 17b696f: Avoid credentialed cross-origin demo status checks from opaque embedded MCP app frames.
|
|
8
|
+
- 17b696f: Mint a fresh MCP App embed session when cached host state contains an expired one-time embed ticket, and keep native MCP host shells alive so they can recover on refresh.
|
|
9
|
+
- 17b696f: Handle asset picker selections from opaque MCP App parent frames and wait for native MCP host initialization before forwarding selected asset messages.
|
|
10
|
+
- 17b696f: Inject per-request identity tokens for trusted first-party org-scoped remote MCP servers, preserving static headers for third-party remotes.
|
|
11
|
+
- 17b696f: Bound MCP client connection and tools/list handshakes so stale org MCP servers do not stall dev-server startup after reloads.
|
|
12
|
+
|
|
3
13
|
## 0.72.3
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.72.
|
|
3
|
+
"version": "0.72.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=22"
|
|
@@ -70,6 +70,11 @@
|
|
|
70
70
|
"./client/observability": "./dist/client/observability/index.js",
|
|
71
71
|
"./client/onboarding": "./dist/client/onboarding/index.js",
|
|
72
72
|
"./client/settings/useBuilderStatus": "./dist/client/settings/useBuilderStatus.js",
|
|
73
|
+
"./embedding": "./dist/embedding/index.js",
|
|
74
|
+
"./embedding/agent": "./dist/embedding/agent.js",
|
|
75
|
+
"./embedding/bridge": "./dist/embedding/bridge.js",
|
|
76
|
+
"./embedding/protocol": "./dist/embedding/protocol.js",
|
|
77
|
+
"./embedding/react": "./dist/embedding/react.js",
|
|
73
78
|
"./onboarding": "./dist/onboarding/index.js",
|
|
74
79
|
"./shared": "./dist/shared/index.js",
|
|
75
80
|
"./scripts": "./dist/scripts/index.js",
|
|
@@ -117,11 +122,6 @@
|
|
|
117
122
|
"./router": "./dist/router/index.js",
|
|
118
123
|
"./collab": "./dist/collab/index.js",
|
|
119
124
|
"./a2a": "./dist/a2a/index.js",
|
|
120
|
-
"./embedding": "./dist/embedding/index.js",
|
|
121
|
-
"./embedding/react": "./dist/embedding/react.js",
|
|
122
|
-
"./embedding/bridge": "./dist/embedding/bridge.js",
|
|
123
|
-
"./embedding/agent": "./dist/embedding/agent.js",
|
|
124
|
-
"./embedding/protocol": "./dist/embedding/protocol.js",
|
|
125
125
|
"./mcp": "./dist/mcp/index.js",
|
|
126
126
|
"./mcp-client": "./dist/mcp-client/index.js",
|
|
127
127
|
"./tracking": "./dist/tracking/index.js",
|
|
@@ -43,6 +43,7 @@ export async function signA2AToken(
|
|
|
43
43
|
options?: {
|
|
44
44
|
expiresIn?: string | number;
|
|
45
45
|
preferGlobalSecret?: boolean;
|
|
46
|
+
audience?: string | string[];
|
|
46
47
|
/**
|
|
47
48
|
* Extra JWT claims to merge alongside `sub` / `org_domain`. Used by the
|
|
48
49
|
* MCP connect flow to add a revocable `jti` and a `scope: "mcp-connect"`
|
|
@@ -67,7 +68,7 @@ export async function signA2AToken(
|
|
|
67
68
|
process.env.BETTER_AUTH_URL ||
|
|
68
69
|
"http://localhost:3000";
|
|
69
70
|
|
|
70
|
-
|
|
71
|
+
const jwt = new jose.SignJWT({
|
|
71
72
|
...(options?.extraClaims ?? {}),
|
|
72
73
|
// `sub` / `org_domain` are spread AFTER extraClaims so a caller-supplied
|
|
73
74
|
// map can never override the verified identity claims.
|
|
@@ -77,8 +78,11 @@ export async function signA2AToken(
|
|
|
77
78
|
.setProtectedHeader({ alg: "HS256" })
|
|
78
79
|
.setIssuer(appUrl)
|
|
79
80
|
.setIssuedAt()
|
|
80
|
-
.setExpirationTime(options?.expiresIn ?? "15m")
|
|
81
|
-
|
|
81
|
+
.setExpirationTime(options?.expiresIn ?? "15m");
|
|
82
|
+
|
|
83
|
+
if (options?.audience) jwt.setAudience(options.audience);
|
|
84
|
+
|
|
85
|
+
return jwt.sign(new TextEncoder().encode(secret));
|
|
82
86
|
}
|
|
83
87
|
|
|
84
88
|
export class A2AClient {
|
|
@@ -59,8 +59,36 @@ function workspacePathBasePath(): string {
|
|
|
59
59
|
return normalizeBasePath(segment);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function externalEmbedTargetBasePath(): string {
|
|
63
|
+
if (typeof window === "undefined") return "";
|
|
64
|
+
const target = (
|
|
65
|
+
window as Window & {
|
|
66
|
+
__AGENT_NATIVE_EXTERNAL_EMBED?: { target?: unknown };
|
|
67
|
+
}
|
|
68
|
+
).__AGENT_NATIVE_EXTERNAL_EMBED?.target;
|
|
69
|
+
if (typeof target !== "string" || !target.startsWith("/")) return "";
|
|
70
|
+
try {
|
|
71
|
+
const url = new URL(target, "http://agent-native.invalid");
|
|
72
|
+
const markerIndex = url.pathname.indexOf(FRAMEWORK_ROUTE_PREFIX);
|
|
73
|
+
if (markerIndex > 0) {
|
|
74
|
+
return normalizeBasePath(url.pathname.slice(0, markerIndex));
|
|
75
|
+
}
|
|
76
|
+
if (isWorkspaceRuntime()) {
|
|
77
|
+
const segment = url.pathname.split("/").find(Boolean);
|
|
78
|
+
if (segment && segment !== "_agent-native" && segment !== "api") {
|
|
79
|
+
return normalizeBasePath(segment);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
return "";
|
|
84
|
+
}
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
|
|
62
88
|
export function appBasePath(): string {
|
|
63
89
|
ensureEmbedAuthFetchInterceptor();
|
|
90
|
+
const externalEmbed = externalEmbedTargetBasePath();
|
|
91
|
+
if (externalEmbed) return externalEmbed;
|
|
64
92
|
const configured = configuredBasePath();
|
|
65
93
|
const derived = pathDerivedBasePath();
|
|
66
94
|
if (!configured) return derived;
|
|
@@ -262,7 +262,33 @@ export function _resetEmbedAuthForTests(): void {
|
|
|
262
262
|
embedAuthFailure = null;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/**
|
|
266
|
+
* True when this document runs in an opaque-origin (`origin === "null"`)
|
|
267
|
+
* browsing context — e.g. a `sandbox="allow-scripts"` iframe without
|
|
268
|
+
* `allow-same-origin`, which is how MCP App embeds always load (the outer host
|
|
269
|
+
* iframe's sandbox propagates to nested frames).
|
|
270
|
+
*
|
|
271
|
+
* It matters for auth: the embed session cookie is keyed to the real app origin
|
|
272
|
+
* and is NOT delivered to an opaque context, so a full document reload here
|
|
273
|
+
* arrives with neither cookie nor — once stripped — URL token, and the server
|
|
274
|
+
* auth guard serves the sign-in page. In that case the URL token is the only
|
|
275
|
+
* credential that survives a reload, so it must stay in the URL.
|
|
276
|
+
*/
|
|
277
|
+
function isOpaqueOriginFrame(win: Window): boolean {
|
|
278
|
+
try {
|
|
279
|
+
return win.location.origin === "null";
|
|
280
|
+
} catch {
|
|
281
|
+
// A thrown access is itself a signal of an opaque/cross-origin context.
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
265
286
|
function stripTokenFromUrl(win: Window): void {
|
|
287
|
+
// Keep the token in the URL for opaque-origin frames — see
|
|
288
|
+
// isOpaqueOriginFrame. Stripping it there breaks re-auth on any document
|
|
289
|
+
// reload. Referrer-Policy is set to no-referrer on embed responses, so the
|
|
290
|
+
// retained token does not leak via the Referer header.
|
|
291
|
+
if (isOpaqueOriginFrame(win)) return;
|
|
266
292
|
try {
|
|
267
293
|
const url = currentUrl(win);
|
|
268
294
|
if (!url) return;
|
|
@@ -438,6 +438,10 @@ function postJsonRpcNotification(method: string, params?: unknown): void {
|
|
|
438
438
|
}
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
function waitForHostLifecycleTurn(): Promise<void> {
|
|
442
|
+
return new Promise((resolve) => window.setTimeout(resolve, 0));
|
|
443
|
+
}
|
|
444
|
+
|
|
441
445
|
function postJsonRpcRequest(
|
|
442
446
|
method: string,
|
|
443
447
|
params?: unknown,
|
|
@@ -484,6 +488,8 @@ async function ensureDirectMcpAppInitialized(): Promise<boolean> {
|
|
|
484
488
|
});
|
|
485
489
|
updateSnapshotFromInitialize(result);
|
|
486
490
|
postJsonRpcNotification("ui/notifications/initialized", {});
|
|
491
|
+
await waitForHostLifecycleTurn();
|
|
492
|
+
await waitForHostLifecycleTurn();
|
|
487
493
|
return true;
|
|
488
494
|
})().catch(() => {
|
|
489
495
|
// Reset so the next call retries the handshake. Otherwise one timed-out
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useLayoutEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "react";
|
|
2
9
|
import {
|
|
3
10
|
AppBridge,
|
|
4
11
|
PostMessageTransport,
|
|
@@ -8,8 +15,14 @@ import {
|
|
|
8
15
|
} from "@modelcontextprotocol/ext-apps/app-bridge";
|
|
9
16
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
10
17
|
import { IconAlertTriangle, IconLoader2 } from "@tabler/icons-react";
|
|
18
|
+
import {
|
|
19
|
+
AGENT_NATIVE_EMBED_MESSAGE_TYPES,
|
|
20
|
+
AGENT_NATIVE_EMBED_PROTOCOL,
|
|
21
|
+
AGENT_NATIVE_EMBED_VERSION,
|
|
22
|
+
} from "../../embedding/protocol.js";
|
|
11
23
|
import type { AgentMcpAppPayload } from "../../mcp-client/app-result.js";
|
|
12
24
|
import { agentNativePath } from "../api-path.js";
|
|
25
|
+
import { sendToAgentChat, type AgentChatRequestMode } from "../agent-chat.js";
|
|
13
26
|
import { cn } from "../utils.js";
|
|
14
27
|
|
|
15
28
|
export const DEFAULT_MCP_APP_IFRAME_HEIGHT = 650;
|
|
@@ -17,6 +30,8 @@ export const MCP_APP_INITIALIZE_TIMEOUT_MS = 8000;
|
|
|
17
30
|
const MIN_IFRAME_HEIGHT = 220;
|
|
18
31
|
const VIEWPORT_MARGIN = 16;
|
|
19
32
|
const SANDBOX_FLAGS = "allow-scripts allow-forms allow-popups";
|
|
33
|
+
const useBrowserLayoutEffect =
|
|
34
|
+
typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
20
35
|
|
|
21
36
|
export interface McpAppRendererProps {
|
|
22
37
|
app: AgentMcpAppPayload;
|
|
@@ -29,10 +44,24 @@ type ResourceUiMeta = {
|
|
|
29
44
|
prefersBorder?: boolean;
|
|
30
45
|
};
|
|
31
46
|
|
|
47
|
+
type McpContentPart = {
|
|
48
|
+
type?: unknown;
|
|
49
|
+
text?: unknown;
|
|
50
|
+
data?: unknown;
|
|
51
|
+
mimeType?: unknown;
|
|
52
|
+
url?: unknown;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type McpAppModelContext = {
|
|
56
|
+
content?: unknown;
|
|
57
|
+
structuredContent?: unknown;
|
|
58
|
+
};
|
|
59
|
+
|
|
32
60
|
export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
33
61
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
34
62
|
const desiredHeightRef = useRef(DEFAULT_MCP_APP_IFRAME_HEIGHT);
|
|
35
|
-
const
|
|
63
|
+
const modelContextRef = useRef<McpAppModelContext | null>(null);
|
|
64
|
+
const readyRef = useRef(false);
|
|
36
65
|
const [height, setHeight] = useState(DEFAULT_MCP_APP_IFRAME_HEIGHT);
|
|
37
66
|
const [error, setError] = useState<string | null>(null);
|
|
38
67
|
const [ready, setReady] = useState(false);
|
|
@@ -49,6 +78,23 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
49
78
|
);
|
|
50
79
|
const externalOpenUrl = useMemo(() => openUrlFromMcpApp(app), [app]);
|
|
51
80
|
|
|
81
|
+
// Keep the latest payload/permissions/csp reachable from the bridge effect
|
|
82
|
+
// without making them effect dependencies. The embedded resource identity is
|
|
83
|
+
// fully captured by `srcDoc`. The bridge effect must NOT re-run when a benign
|
|
84
|
+
// parent re-render hands us a new `app` object reference with identical
|
|
85
|
+
// content (common during chat streaming/polling): re-running tears down a
|
|
86
|
+
// live, already-initialized MCP App (teardownResource) and re-arms the
|
|
87
|
+
// initialize watchdog against a fresh host AppBridge that the embed shell
|
|
88
|
+
// will never re-handshake (its connect promise is memoized), surfacing a
|
|
89
|
+
// false "MCP App did not finish initializing." error after the app is
|
|
90
|
+
// visibly working.
|
|
91
|
+
const appRef = useRef(app);
|
|
92
|
+
const supportedPermissionsRef = useRef(supportedPermissions);
|
|
93
|
+
const uiCspRef = useRef(uiMeta.csp);
|
|
94
|
+
appRef.current = app;
|
|
95
|
+
supportedPermissionsRef.current = supportedPermissions;
|
|
96
|
+
uiCspRef.current = uiMeta.csp;
|
|
97
|
+
|
|
52
98
|
useEffect(() => {
|
|
53
99
|
desiredHeightRef.current = DEFAULT_MCP_APP_IFRAME_HEIGHT;
|
|
54
100
|
setHeight(
|
|
@@ -57,11 +103,18 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
57
103
|
availableMcpAppHeight(iframeRef.current),
|
|
58
104
|
),
|
|
59
105
|
);
|
|
60
|
-
|
|
106
|
+
readyRef.current = false;
|
|
61
107
|
setReady(false);
|
|
62
108
|
setError(null);
|
|
109
|
+
modelContextRef.current = null;
|
|
63
110
|
}, [srcDoc]);
|
|
64
111
|
|
|
112
|
+
const markReady = useCallback(() => {
|
|
113
|
+
readyRef.current = true;
|
|
114
|
+
setReady(true);
|
|
115
|
+
setError(null);
|
|
116
|
+
}, []);
|
|
117
|
+
|
|
65
118
|
const applyHeight = useCallback((desiredHeight?: number) => {
|
|
66
119
|
if (
|
|
67
120
|
typeof desiredHeight === "number" &&
|
|
@@ -114,11 +167,26 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
114
167
|
|
|
115
168
|
useEffect(() => {
|
|
116
169
|
const iframe = iframeRef.current;
|
|
117
|
-
if (!iframe?.contentWindow || !srcDoc
|
|
170
|
+
if (!iframe?.contentWindow || !srcDoc) return;
|
|
171
|
+
const frameWindow = iframe.contentWindow;
|
|
172
|
+
const listener = (event: MessageEvent) => {
|
|
173
|
+
if (event.source !== frameWindow) return;
|
|
174
|
+
if (isMcpAppReadyMessage(event.data)) {
|
|
175
|
+
markReady();
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
window.addEventListener("message", listener);
|
|
179
|
+
return () => window.removeEventListener("message", listener);
|
|
180
|
+
}, [markReady, srcDoc]);
|
|
181
|
+
|
|
182
|
+
useBrowserLayoutEffect(() => {
|
|
183
|
+
const iframe = iframeRef.current;
|
|
184
|
+
if (!iframe?.contentWindow || !srcDoc) return;
|
|
118
185
|
|
|
186
|
+
const currentApp = appRef.current;
|
|
119
187
|
let closed = false;
|
|
120
188
|
const initializeTimer = window.setTimeout(() => {
|
|
121
|
-
if (closed) return;
|
|
189
|
+
if (closed || readyRef.current) return;
|
|
122
190
|
setReady(false);
|
|
123
191
|
setError("MCP App did not finish initializing.");
|
|
124
192
|
}, MCP_APP_INITIALIZE_TIMEOUT_MS);
|
|
@@ -131,13 +199,13 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
131
199
|
serverResources: {},
|
|
132
200
|
logging: {},
|
|
133
201
|
sandbox: {
|
|
134
|
-
permissions:
|
|
135
|
-
csp:
|
|
202
|
+
permissions: supportedPermissionsRef.current,
|
|
203
|
+
csp: uiCspRef.current ?? {},
|
|
136
204
|
},
|
|
137
205
|
},
|
|
138
206
|
{
|
|
139
207
|
hostContext: buildHostContext(
|
|
140
|
-
|
|
208
|
+
currentApp,
|
|
141
209
|
availableMcpAppHeight(iframe),
|
|
142
210
|
) as any,
|
|
143
211
|
},
|
|
@@ -152,12 +220,9 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
152
220
|
bridge.addEventListener("initialized", () => {
|
|
153
221
|
if (closed) return;
|
|
154
222
|
clearTimeout(initializeTimer);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
setError(null);
|
|
159
|
-
void bridge.sendToolInput({ arguments: app.toolInput });
|
|
160
|
-
void bridge.sendToolResult(app.toolResult as CallToolResult);
|
|
223
|
+
markReady();
|
|
224
|
+
void bridge.sendToolInput({ arguments: appRef.current.toolInput });
|
|
225
|
+
void bridge.sendToolResult(appRef.current.toolResult as CallToolResult);
|
|
161
226
|
});
|
|
162
227
|
bridge.addEventListener("loggingmessage", ({ level, data }) => {
|
|
163
228
|
if (level === "error" || level === "critical" || level === "alert") {
|
|
@@ -170,13 +235,16 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
170
235
|
return {};
|
|
171
236
|
};
|
|
172
237
|
bridge.oncalltool = async ({ name, arguments: toolArguments }) => {
|
|
173
|
-
const toolName = normalizeSameServerToolName(
|
|
238
|
+
const toolName = normalizeSameServerToolName(
|
|
239
|
+
appRef.current.serverId,
|
|
240
|
+
name,
|
|
241
|
+
);
|
|
174
242
|
if (!toolName) {
|
|
175
243
|
return errorToolResult("Cross-server MCP App tool calls are blocked.");
|
|
176
244
|
}
|
|
177
245
|
try {
|
|
178
246
|
return await postMcpAppEndpoint<CallToolResult>("call-tool", {
|
|
179
|
-
serverId:
|
|
247
|
+
serverId: appRef.current.serverId,
|
|
180
248
|
toolName,
|
|
181
249
|
arguments:
|
|
182
250
|
toolArguments && typeof toolArguments === "object"
|
|
@@ -188,22 +256,40 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
188
256
|
}
|
|
189
257
|
};
|
|
190
258
|
(bridge as any).onlisttools = async () =>
|
|
191
|
-
postMcpAppEndpoint("list-tools", { serverId:
|
|
259
|
+
postMcpAppEndpoint("list-tools", { serverId: appRef.current.serverId });
|
|
192
260
|
bridge.onreadresource = async ({ uri }) =>
|
|
193
|
-
postMcpAppEndpoint("read-resource", {
|
|
261
|
+
postMcpAppEndpoint("read-resource", {
|
|
262
|
+
serverId: appRef.current.serverId,
|
|
263
|
+
uri,
|
|
264
|
+
});
|
|
194
265
|
bridge.onlistresources = async () => ({ resources: [] });
|
|
195
266
|
bridge.onlistresourcetemplates = async () => ({ resourceTemplates: [] });
|
|
196
267
|
bridge.ondownloadfile = async () => ({ isError: true });
|
|
197
|
-
bridge.onmessage = async () =>
|
|
198
|
-
|
|
268
|
+
bridge.onmessage = async (params) => {
|
|
269
|
+
const message = messageTextFromMcpUiMessage(params);
|
|
270
|
+
if (!message.trim()) return { isError: true };
|
|
271
|
+
const mode = requestModeFromMcpUiMessage(params);
|
|
272
|
+
sendToAgentChat({
|
|
273
|
+
message,
|
|
274
|
+
context: contextTextFromMcpModelContext(modelContextRef.current),
|
|
275
|
+
images: imageDataUrlsFromMcpContent(params.content),
|
|
276
|
+
submit: true,
|
|
277
|
+
openSidebar: true,
|
|
278
|
+
...(mode ? { mode } : {}),
|
|
279
|
+
});
|
|
280
|
+
return {};
|
|
281
|
+
};
|
|
282
|
+
bridge.onupdatemodelcontext = async (params) => {
|
|
283
|
+
modelContextRef.current = params as McpAppModelContext;
|
|
284
|
+
return {};
|
|
285
|
+
};
|
|
199
286
|
|
|
200
287
|
const transport = new PostMessageTransport(
|
|
201
288
|
iframe.contentWindow,
|
|
202
289
|
iframe.contentWindow,
|
|
203
290
|
);
|
|
204
|
-
setReady(false);
|
|
205
291
|
setError(null);
|
|
206
|
-
bridge.connect(transport).catch((err: any) => {
|
|
292
|
+
void bridge.connect(transport).catch((err: any) => {
|
|
207
293
|
if (!closed) {
|
|
208
294
|
clearTimeout(initializeTimer);
|
|
209
295
|
setError(err?.message ?? "Failed to initialize MCP App.");
|
|
@@ -212,8 +298,8 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
212
298
|
|
|
213
299
|
return () => {
|
|
214
300
|
closed = true;
|
|
301
|
+
modelContextRef.current = null;
|
|
215
302
|
clearTimeout(initializeTimer);
|
|
216
|
-
setReady(false);
|
|
217
303
|
void bridge
|
|
218
304
|
.teardownResource({}, { timeout: 500 })
|
|
219
305
|
.catch(() => undefined)
|
|
@@ -221,14 +307,10 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
221
307
|
void (bridge as any).close?.().catch?.(() => undefined);
|
|
222
308
|
});
|
|
223
309
|
};
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
srcDoc,
|
|
229
|
-
supportedPermissions,
|
|
230
|
-
uiMeta.csp,
|
|
231
|
-
]);
|
|
310
|
+
// The embedded resource identity is captured by `srcDoc`; `app`,
|
|
311
|
+
// `supportedPermissions`, and `uiMeta.csp` are read via refs so a
|
|
312
|
+
// new-but-equal `app` object reference does not tear down a live bridge.
|
|
313
|
+
}, [applyHeight, markReady, srcDoc]);
|
|
232
314
|
|
|
233
315
|
if (!resourceHtml) {
|
|
234
316
|
return (
|
|
@@ -277,12 +359,83 @@ export function McpAppRenderer({ app, className }: McpAppRendererProps) {
|
|
|
277
359
|
sandbox={SANDBOX_FLAGS}
|
|
278
360
|
allow={buildAllowAttribute(supportedPermissions)}
|
|
279
361
|
style={{ height }}
|
|
280
|
-
onLoad={() => setLoadedSrcDoc(srcDoc)}
|
|
281
362
|
/>
|
|
282
363
|
</div>
|
|
283
364
|
);
|
|
284
365
|
}
|
|
285
366
|
|
|
367
|
+
function messageTextFromMcpUiMessage(params: { content?: unknown }): string {
|
|
368
|
+
return textPartsFromMcpContent(params.content).join("\n\n").trim();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function requestModeFromMcpUiMessage(
|
|
372
|
+
params: unknown,
|
|
373
|
+
): AgentChatRequestMode | undefined {
|
|
374
|
+
const record =
|
|
375
|
+
params && typeof params === "object" && !Array.isArray(params)
|
|
376
|
+
? (params as { mode?: unknown; requestMode?: unknown })
|
|
377
|
+
: {};
|
|
378
|
+
const mode = record.requestMode ?? record.mode;
|
|
379
|
+
return mode === "act" || mode === "plan" ? mode : undefined;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function contextTextFromMcpModelContext(
|
|
383
|
+
context: McpAppModelContext | null,
|
|
384
|
+
): string | undefined {
|
|
385
|
+
if (!context) return undefined;
|
|
386
|
+
const parts = textPartsFromMcpContent(context.content);
|
|
387
|
+
if (context.structuredContent !== undefined) {
|
|
388
|
+
parts.push(JSON.stringify(context.structuredContent, null, 2));
|
|
389
|
+
}
|
|
390
|
+
const text = parts.join("\n\n").trim();
|
|
391
|
+
return text || undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function textPartsFromMcpContent(content: unknown): string[] {
|
|
395
|
+
if (!Array.isArray(content)) return [];
|
|
396
|
+
return content
|
|
397
|
+
.map((part): string | null => {
|
|
398
|
+
const record = contentPartRecord(part);
|
|
399
|
+
if (
|
|
400
|
+
!record ||
|
|
401
|
+
record.type !== "text" ||
|
|
402
|
+
typeof record.text !== "string"
|
|
403
|
+
) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const text = record.text.trim();
|
|
407
|
+
return text || null;
|
|
408
|
+
})
|
|
409
|
+
.filter((text): text is string => Boolean(text));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function imageDataUrlsFromMcpContent(content: unknown): string[] | undefined {
|
|
413
|
+
if (!Array.isArray(content)) return undefined;
|
|
414
|
+
const images = content
|
|
415
|
+
.map((part): string | null => {
|
|
416
|
+
const record = contentPartRecord(part);
|
|
417
|
+
if (!record || record.type !== "image") return null;
|
|
418
|
+
if (typeof record.url === "string" && record.url.trim()) {
|
|
419
|
+
return record.url;
|
|
420
|
+
}
|
|
421
|
+
if (typeof record.data !== "string" || !record.data.trim()) return null;
|
|
422
|
+
const mimeType =
|
|
423
|
+
typeof record.mimeType === "string" &&
|
|
424
|
+
record.mimeType.startsWith("image/")
|
|
425
|
+
? record.mimeType
|
|
426
|
+
: "image/png";
|
|
427
|
+
return `data:${mimeType};base64,${record.data}`;
|
|
428
|
+
})
|
|
429
|
+
.filter((image): image is string => Boolean(image));
|
|
430
|
+
return images.length ? images : undefined;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function contentPartRecord(part: unknown): McpContentPart | null {
|
|
434
|
+
return part && typeof part === "object" && !Array.isArray(part)
|
|
435
|
+
? (part as McpContentPart)
|
|
436
|
+
: null;
|
|
437
|
+
}
|
|
438
|
+
|
|
286
439
|
function resourceUiMeta(app: AgentMcpAppPayload): ResourceUiMeta {
|
|
287
440
|
const meta = app.resource?._meta;
|
|
288
441
|
const ui =
|
|
@@ -341,8 +494,31 @@ export function supportedMcpAppPermissions(
|
|
|
341
494
|
return permissions?.clipboardWrite ? { clipboardWrite: {} } : {};
|
|
342
495
|
}
|
|
343
496
|
|
|
497
|
+
export function isMcpAppReadyMessage(data: unknown): boolean {
|
|
498
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
499
|
+
const record = data as {
|
|
500
|
+
protocol?: unknown;
|
|
501
|
+
version?: unknown;
|
|
502
|
+
type?: unknown;
|
|
503
|
+
};
|
|
504
|
+
if (
|
|
505
|
+
record.protocol === AGENT_NATIVE_EMBED_PROTOCOL &&
|
|
506
|
+
record.version === AGENT_NATIVE_EMBED_VERSION &&
|
|
507
|
+
record.type === AGENT_NATIVE_EMBED_MESSAGE_TYPES.READY
|
|
508
|
+
) {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
const type = record.type;
|
|
512
|
+
return (
|
|
513
|
+
type === "agentNative.embeddedAppReady" ||
|
|
514
|
+
type === "agentNative.frameOrigin"
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
|
|
344
518
|
export function buildMcpAppCsp(csp: McpUiResourceCsp | undefined): string {
|
|
345
|
-
const connect =
|
|
519
|
+
const connect = withLocalWebSocketSources(
|
|
520
|
+
sanitizeCspSources(csp?.connectDomains),
|
|
521
|
+
);
|
|
346
522
|
const resources = sanitizeCspSources(csp?.resourceDomains);
|
|
347
523
|
const frames = sanitizeCspSources(csp?.frameDomains);
|
|
348
524
|
const base = sanitizeCspSources(csp?.baseUriDomains);
|
|
@@ -369,6 +545,24 @@ function sanitizeCspSources(values: string[] | undefined): string[] {
|
|
|
369
545
|
return [...new Set(out)];
|
|
370
546
|
}
|
|
371
547
|
|
|
548
|
+
function withLocalWebSocketSources(sources: string[]): string[] {
|
|
549
|
+
const out = [...sources];
|
|
550
|
+
for (const source of sources) {
|
|
551
|
+
try {
|
|
552
|
+
const url = new URL(source);
|
|
553
|
+
if (
|
|
554
|
+
url.protocol === "http:" &&
|
|
555
|
+
["localhost", "127.0.0.1", "::1"].includes(url.hostname)
|
|
556
|
+
) {
|
|
557
|
+
out.push(`ws://${url.host}`);
|
|
558
|
+
}
|
|
559
|
+
} catch {
|
|
560
|
+
// Ignore non-URL CSP source expressions.
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return [...new Set(out)];
|
|
564
|
+
}
|
|
565
|
+
|
|
372
566
|
function sanitizeCspSource(value: string): string | null {
|
|
373
567
|
const source = value.trim();
|
|
374
568
|
if (!source || source.includes("'") || /[\s;]/.test(source)) return null;
|
|
@@ -33,7 +33,9 @@ export function DemoModeSection() {
|
|
|
33
33
|
const { data } = useQuery({
|
|
34
34
|
queryKey: ["agent-native", "demo-mode"],
|
|
35
35
|
queryFn: async () => {
|
|
36
|
-
const res = await fetch(DEMO_STATUS_URL, {
|
|
36
|
+
const res = await fetch(DEMO_STATUS_URL, {
|
|
37
|
+
credentials: "same-origin",
|
|
38
|
+
});
|
|
37
39
|
if (!res.ok) return null;
|
|
38
40
|
return (await res.json()) as DemoStatus | null;
|
|
39
41
|
},
|
|
@@ -91,7 +91,7 @@ function methodOf(input: RequestInfo | URL, init?: RequestInit): string {
|
|
|
91
91
|
async function refreshDemoFlag(): Promise<void> {
|
|
92
92
|
const f = originalFetch ?? fetch;
|
|
93
93
|
try {
|
|
94
|
-
const res = await f(STATUS_PATH, { credentials: "
|
|
94
|
+
const res = await f(STATUS_PATH, { credentials: "same-origin" });
|
|
95
95
|
if (!res.ok) return;
|
|
96
96
|
const json = (await res.json()) as {
|
|
97
97
|
enabled?: boolean;
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MCP_APP_CHAT_BRIDGE_QUERY_PARAM,
|
|
3
|
+
EMBED_MODE_QUERY_PARAM,
|
|
4
|
+
} from "../shared/embed-auth.js";
|
|
1
5
|
import {
|
|
2
6
|
AGENT_NATIVE_EMBED_MESSAGE_TYPES,
|
|
3
7
|
createAgentNativeEmbedEnvelope,
|
|
@@ -61,10 +65,38 @@ type PendingRequest = {
|
|
|
61
65
|
|
|
62
66
|
function resolveParentOrigin(win: Window, explicit?: string): string | null {
|
|
63
67
|
if (explicit) return explicit;
|
|
68
|
+
if (shouldUseOpaqueParentTarget(win)) return "*";
|
|
64
69
|
const referrerOrigin = embeddedAppOrigin(win.document?.referrer ?? "");
|
|
65
70
|
return referrerOrigin;
|
|
66
71
|
}
|
|
67
72
|
|
|
73
|
+
function shouldUseOpaqueParentTarget(win: Window): boolean {
|
|
74
|
+
try {
|
|
75
|
+
const url = new URL(win.location.href);
|
|
76
|
+
const chatBridge = url.searchParams.get(MCP_APP_CHAT_BRIDGE_QUERY_PARAM);
|
|
77
|
+
const embedMode = url.searchParams.get(EMBED_MODE_QUERY_PARAM);
|
|
78
|
+
return (
|
|
79
|
+
chatBridge === "1" ||
|
|
80
|
+
chatBridge === "true" ||
|
|
81
|
+
embedMode === "1" ||
|
|
82
|
+
embedMode === "true"
|
|
83
|
+
);
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultAllowedOrigins(
|
|
90
|
+
parentOrigin: string | null,
|
|
91
|
+
referrerOrigin: string | null,
|
|
92
|
+
): string[] {
|
|
93
|
+
if (!parentOrigin) return [];
|
|
94
|
+
if (parentOrigin === "*") {
|
|
95
|
+
return ["null", ...(referrerOrigin ? [referrerOrigin] : [])];
|
|
96
|
+
}
|
|
97
|
+
return [parentOrigin];
|
|
98
|
+
}
|
|
99
|
+
|
|
68
100
|
function postToParent(
|
|
69
101
|
win: Window,
|
|
70
102
|
parent: Window | null,
|
|
@@ -83,9 +115,10 @@ export function createEmbeddedAppBridge(
|
|
|
83
115
|
): EmbeddedAppBridge {
|
|
84
116
|
const win = options.currentWindow ?? window;
|
|
85
117
|
const parentOrigin = resolveParentOrigin(win, options.parentOrigin);
|
|
118
|
+
const referrerOrigin = embeddedAppOrigin(win.document?.referrer ?? "");
|
|
86
119
|
const allowedOrigins =
|
|
87
120
|
options.allowedOrigins ??
|
|
88
|
-
(parentOrigin
|
|
121
|
+
defaultAllowedOrigins(parentOrigin, referrerOrigin);
|
|
89
122
|
const pending = new Map<string, PendingRequest>();
|
|
90
123
|
let destroyed = false;
|
|
91
124
|
|