@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.
Files changed (136) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +10 -0
  3. package/corpus/core/package.json +6 -6
  4. package/corpus/core/src/a2a/client.ts +7 -3
  5. package/corpus/core/src/client/api-path.ts +28 -0
  6. package/corpus/core/src/client/embed-auth.ts +26 -0
  7. package/corpus/core/src/client/mcp-app-host.ts +6 -0
  8. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
  9. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  10. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
  11. package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
  12. package/corpus/core/src/embedding/agent.ts +6 -1
  13. package/corpus/core/src/embedding/bridge.ts +34 -1
  14. package/corpus/core/src/mcp/build-server.ts +38 -10
  15. package/corpus/core/src/mcp/embed-app.ts +120 -45
  16. package/corpus/core/src/mcp/index.ts +5 -0
  17. package/corpus/core/src/mcp/org-directory.ts +70 -11
  18. package/corpus/core/src/mcp/server.ts +14 -0
  19. package/corpus/core/src/mcp-client/config.ts +12 -0
  20. package/corpus/core/src/mcp-client/index.ts +2 -0
  21. package/corpus/core/src/mcp-client/manager.ts +268 -9
  22. package/corpus/core/src/mcp-client/remote-store.ts +154 -0
  23. package/corpus/core/src/mcp-client/routes.ts +2 -0
  24. package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
  25. package/corpus/core/src/server/index.ts +1 -0
  26. package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
  27. package/corpus/core/src/vite/client.ts +53 -0
  28. package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
  29. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
  30. package/corpus/templates/assets/AGENTS.md +7 -5
  31. package/corpus/templates/assets/actions/generate-asset.ts +161 -0
  32. package/corpus/templates/assets/actions/list-assets.ts +4 -2
  33. package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
  34. package/corpus/templates/assets/app/routes/library.tsx +243 -40
  35. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
  36. package/corpus/templates/design/AGENTS.md +10 -5
  37. package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
  38. package/corpus/templates/design/actions/insert-asset.ts +219 -0
  39. package/corpus/templates/design/actions/view-screen.ts +3 -1
  40. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  41. package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
  42. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
  43. package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
  44. package/dist/a2a/client.d.ts +1 -0
  45. package/dist/a2a/client.d.ts.map +1 -1
  46. package/dist/a2a/client.js +5 -3
  47. package/dist/a2a/client.js.map +1 -1
  48. package/dist/client/api-path.d.ts.map +1 -1
  49. package/dist/client/api-path.js +27 -0
  50. package/dist/client/api-path.js.map +1 -1
  51. package/dist/client/embed-auth.d.ts.map +1 -1
  52. package/dist/client/embed-auth.js +27 -0
  53. package/dist/client/embed-auth.js.map +1 -1
  54. package/dist/client/mcp-app-host.d.ts.map +1 -1
  55. package/dist/client/mcp-app-host.js +5 -0
  56. package/dist/client/mcp-app-host.js.map +1 -1
  57. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  58. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  59. package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
  60. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  61. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  62. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  63. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  64. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  65. package/dist/client/settings/DemoModeSection.js +3 -1
  66. package/dist/client/settings/DemoModeSection.js.map +1 -1
  67. package/dist/demo/fetch-interceptor.js +1 -1
  68. package/dist/demo/fetch-interceptor.js.map +1 -1
  69. package/dist/embedding/agent.d.ts +1 -1
  70. package/dist/embedding/agent.d.ts.map +1 -1
  71. package/dist/embedding/agent.js +1 -1
  72. package/dist/embedding/agent.js.map +1 -1
  73. package/dist/embedding/bridge.d.ts.map +1 -1
  74. package/dist/embedding/bridge.js +27 -1
  75. package/dist/embedding/bridge.js.map +1 -1
  76. package/dist/extensions/schema.d.ts +2 -2
  77. package/dist/mcp/build-server.d.ts +2 -0
  78. package/dist/mcp/build-server.d.ts.map +1 -1
  79. package/dist/mcp/build-server.js +32 -9
  80. package/dist/mcp/build-server.js.map +1 -1
  81. package/dist/mcp/embed-app.d.ts.map +1 -1
  82. package/dist/mcp/embed-app.js +120 -45
  83. package/dist/mcp/embed-app.js.map +1 -1
  84. package/dist/mcp/index.d.ts +1 -0
  85. package/dist/mcp/index.d.ts.map +1 -1
  86. package/dist/mcp/index.js +1 -0
  87. package/dist/mcp/index.js.map +1 -1
  88. package/dist/mcp/org-directory.d.ts +1 -0
  89. package/dist/mcp/org-directory.d.ts.map +1 -1
  90. package/dist/mcp/org-directory.js +59 -11
  91. package/dist/mcp/org-directory.js.map +1 -1
  92. package/dist/mcp/server.d.ts.map +1 -1
  93. package/dist/mcp/server.js +9 -0
  94. package/dist/mcp/server.js.map +1 -1
  95. package/dist/mcp-client/config.d.ts +12 -0
  96. package/dist/mcp-client/config.d.ts.map +1 -1
  97. package/dist/mcp-client/config.js.map +1 -1
  98. package/dist/mcp-client/index.d.ts +1 -1
  99. package/dist/mcp-client/index.d.ts.map +1 -1
  100. package/dist/mcp-client/index.js +1 -1
  101. package/dist/mcp-client/index.js.map +1 -1
  102. package/dist/mcp-client/manager.d.ts +2 -0
  103. package/dist/mcp-client/manager.d.ts.map +1 -1
  104. package/dist/mcp-client/manager.js +201 -9
  105. package/dist/mcp-client/manager.js.map +1 -1
  106. package/dist/mcp-client/remote-store.d.ts +27 -0
  107. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  108. package/dist/mcp-client/remote-store.js +105 -0
  109. package/dist/mcp-client/remote-store.js.map +1 -1
  110. package/dist/mcp-client/routes.d.ts +1 -0
  111. package/dist/mcp-client/routes.d.ts.map +1 -1
  112. package/dist/mcp-client/routes.js +1 -0
  113. package/dist/mcp-client/routes.js.map +1 -1
  114. package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
  115. package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
  116. package/dist/provider-api/corpus-jobs.d.ts +8 -8
  117. package/dist/server/agent-chat-plugin.d.ts +2 -0
  118. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  119. package/dist/server/agent-chat-plugin.js +8 -0
  120. package/dist/server/agent-chat-plugin.js.map +1 -1
  121. package/dist/server/index.d.ts +1 -1
  122. package/dist/server/index.d.ts.map +1 -1
  123. package/dist/server/index.js +1 -1
  124. package/dist/server/index.js.map +1 -1
  125. package/dist/shared/mcp-embed-headers.d.ts +3 -1
  126. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  127. package/dist/shared/mcp-embed-headers.js +40 -3
  128. package/dist/shared/mcp-embed-headers.js.map +1 -1
  129. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  130. package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
  131. package/dist/sharing/actions/share-resource.d.ts +1 -1
  132. package/dist/sharing/schema.d.ts +1 -1
  133. package/dist/vite/client.d.ts.map +1 -1
  134. package/dist/vite/client.js +44 -0
  135. package/dist/vite/client.js.map +1 -1
  136. package/package.json +6 -6
package/corpus/README.md CHANGED
@@ -28,4 +28,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core/corpus
28
28
  ## Generated Counts
29
29
 
30
30
  - core files: 1160
31
- - template files: 4008
31
+ - template files: 4012
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.72.3",
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
- return new jose.SignJWT({
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
- .sign(new TextEncoder().encode(secret));
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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
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 [loadedSrcDoc, setLoadedSrcDoc] = useState<string | null>(null);
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
- setLoadedSrcDoc(null);
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 || loadedSrcDoc !== srcDoc) return;
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: supportedPermissions,
135
- csp: uiMeta.csp ?? {},
202
+ permissions: supportedPermissionsRef.current,
203
+ csp: uiCspRef.current ?? {},
136
204
  },
137
205
  },
138
206
  {
139
207
  hostContext: buildHostContext(
140
- app,
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
- setReady(true);
156
- // Clear any error the initialize timeout may have set before a slow app
157
- // finished otherwise the error overlay stays stuck over a working app.
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(app.serverId, name);
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: app.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: app.serverId });
259
+ postMcpAppEndpoint("list-tools", { serverId: appRef.current.serverId });
192
260
  bridge.onreadresource = async ({ uri }) =>
193
- postMcpAppEndpoint("read-resource", { serverId: app.serverId, uri });
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 () => ({ isError: true });
198
- bridge.onupdatemodelcontext = async () => ({});
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
- app,
226
- applyHeight,
227
- loadedSrcDoc,
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 = sanitizeCspSources(csp?.connectDomains);
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;
@@ -20,6 +20,7 @@ export interface McpServer {
20
20
  url: string;
21
21
  headers?: Record<string, { set: true }>;
22
22
  description?: string;
23
+ firstParty?: boolean;
23
24
  createdAt: number;
24
25
  mergedId: string;
25
26
  status:
@@ -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, { credentials: "include" });
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: "include" });
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,4 +1,9 @@
1
- import { A2AClient, callAgent, type Message, type Task } from "../a2a/index.js";
1
+ import {
2
+ A2AClient,
3
+ callAgent,
4
+ type Message,
5
+ type Task,
6
+ } from "@agent-native/core/a2a";
2
7
 
3
8
  export interface AgentEndpointOptions {
4
9
  /**
@@ -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 && parentOrigin !== "*" ? [parentOrigin] : []);
121
+ defaultAllowedOrigins(parentOrigin, referrerOrigin);
89
122
  const pending = new Map<string, PendingRequest>();
90
123
  let destroyed = false;
91
124