@agent-native/core 0.72.2 → 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 (169) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -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/AgentPanel.tsx +12 -1
  6. package/corpus/core/src/client/MultiTabAssistantChat.tsx +263 -13
  7. package/corpus/core/src/client/api-path.ts +28 -0
  8. package/corpus/core/src/client/embed-auth.ts +26 -0
  9. package/corpus/core/src/client/mcp-app-host.ts +6 -0
  10. package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
  11. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  12. package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
  13. package/corpus/core/src/client/use-agent-chat-home-handoff.ts +16 -4
  14. package/corpus/core/src/client/use-chat-threads.ts +86 -5
  15. package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
  16. package/corpus/core/src/embedding/agent.ts +6 -1
  17. package/corpus/core/src/embedding/bridge.ts +34 -1
  18. package/corpus/core/src/mcp/build-server.ts +38 -10
  19. package/corpus/core/src/mcp/embed-app.ts +120 -45
  20. package/corpus/core/src/mcp/index.ts +5 -0
  21. package/corpus/core/src/mcp/org-directory.ts +70 -11
  22. package/corpus/core/src/mcp/server.ts +14 -0
  23. package/corpus/core/src/mcp-client/config.ts +12 -0
  24. package/corpus/core/src/mcp-client/index.ts +2 -0
  25. package/corpus/core/src/mcp-client/manager.ts +268 -9
  26. package/corpus/core/src/mcp-client/remote-store.ts +154 -0
  27. package/corpus/core/src/mcp-client/routes.ts +2 -0
  28. package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
  29. package/corpus/core/src/server/index.ts +1 -0
  30. package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
  31. package/corpus/core/src/vite/client.ts +53 -0
  32. package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
  33. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
  34. package/corpus/templates/assets/AGENTS.md +7 -5
  35. package/corpus/templates/assets/actions/generate-asset.ts +161 -0
  36. package/corpus/templates/assets/actions/list-assets.ts +4 -2
  37. package/corpus/templates/assets/actions/navigate.ts +2 -1
  38. package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
  39. package/corpus/templates/assets/app/components/layout/Layout.tsx +3 -2
  40. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +27 -4
  41. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +35 -5
  42. package/corpus/templates/assets/app/routes/_index.tsx +12 -0
  43. package/corpus/templates/assets/app/routes/chat.$threadId.tsx +1 -0
  44. package/corpus/templates/assets/app/routes/library.tsx +243 -40
  45. package/corpus/templates/chat/actions/navigate.ts +3 -0
  46. package/corpus/templates/chat/app/components/layout/Layout.tsx +7 -2
  47. package/corpus/templates/chat/app/components/layout/Sidebar.tsx +27 -4
  48. package/corpus/templates/chat/app/hooks/use-navigation-state.ts +38 -6
  49. package/corpus/templates/chat/app/routes/_index.tsx +13 -0
  50. package/corpus/templates/chat/app/routes/chat.$threadId.tsx +1 -0
  51. package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
  52. package/corpus/templates/design/AGENTS.md +10 -5
  53. package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
  54. package/corpus/templates/design/actions/insert-asset.ts +219 -0
  55. package/corpus/templates/design/actions/view-screen.ts +3 -1
  56. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  57. package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
  58. package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
  59. package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
  60. package/corpus/templates/dispatch/app/routes/chat.$threadId.tsx +26 -0
  61. package/dist/a2a/client.d.ts +1 -0
  62. package/dist/a2a/client.d.ts.map +1 -1
  63. package/dist/a2a/client.js +5 -3
  64. package/dist/a2a/client.js.map +1 -1
  65. package/dist/client/AgentPanel.d.ts +6 -2
  66. package/dist/client/AgentPanel.d.ts.map +1 -1
  67. package/dist/client/AgentPanel.js +4 -4
  68. package/dist/client/AgentPanel.js.map +1 -1
  69. package/dist/client/MultiTabAssistantChat.d.ts +22 -1
  70. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  71. package/dist/client/MultiTabAssistantChat.js +215 -13
  72. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  73. package/dist/client/api-path.d.ts.map +1 -1
  74. package/dist/client/api-path.js +27 -0
  75. package/dist/client/api-path.js.map +1 -1
  76. package/dist/client/embed-auth.d.ts.map +1 -1
  77. package/dist/client/embed-auth.js +27 -0
  78. package/dist/client/embed-auth.js.map +1 -1
  79. package/dist/client/mcp-app-host.d.ts.map +1 -1
  80. package/dist/client/mcp-app-host.js +5 -0
  81. package/dist/client/mcp-app-host.js.map +1 -1
  82. package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
  83. package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
  84. package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
  85. package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
  86. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  87. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  88. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  89. package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
  90. package/dist/client/settings/DemoModeSection.js +3 -1
  91. package/dist/client/settings/DemoModeSection.js.map +1 -1
  92. package/dist/client/use-agent-chat-home-handoff.d.ts +7 -1
  93. package/dist/client/use-agent-chat-home-handoff.d.ts.map +1 -1
  94. package/dist/client/use-agent-chat-home-handoff.js +9 -5
  95. package/dist/client/use-agent-chat-home-handoff.js.map +1 -1
  96. package/dist/client/use-chat-threads.d.ts +6 -0
  97. package/dist/client/use-chat-threads.d.ts.map +1 -1
  98. package/dist/client/use-chat-threads.js +79 -5
  99. package/dist/client/use-chat-threads.js.map +1 -1
  100. package/dist/demo/fetch-interceptor.js +1 -1
  101. package/dist/demo/fetch-interceptor.js.map +1 -1
  102. package/dist/embedding/agent.d.ts +1 -1
  103. package/dist/embedding/agent.d.ts.map +1 -1
  104. package/dist/embedding/agent.js +1 -1
  105. package/dist/embedding/agent.js.map +1 -1
  106. package/dist/embedding/bridge.d.ts.map +1 -1
  107. package/dist/embedding/bridge.js +27 -1
  108. package/dist/embedding/bridge.js.map +1 -1
  109. package/dist/extensions/schema.d.ts +2 -2
  110. package/dist/mcp/build-server.d.ts +2 -0
  111. package/dist/mcp/build-server.d.ts.map +1 -1
  112. package/dist/mcp/build-server.js +32 -9
  113. package/dist/mcp/build-server.js.map +1 -1
  114. package/dist/mcp/embed-app.d.ts.map +1 -1
  115. package/dist/mcp/embed-app.js +120 -45
  116. package/dist/mcp/embed-app.js.map +1 -1
  117. package/dist/mcp/index.d.ts +1 -0
  118. package/dist/mcp/index.d.ts.map +1 -1
  119. package/dist/mcp/index.js +1 -0
  120. package/dist/mcp/index.js.map +1 -1
  121. package/dist/mcp/org-directory.d.ts +1 -0
  122. package/dist/mcp/org-directory.d.ts.map +1 -1
  123. package/dist/mcp/org-directory.js +59 -11
  124. package/dist/mcp/org-directory.js.map +1 -1
  125. package/dist/mcp/server.d.ts.map +1 -1
  126. package/dist/mcp/server.js +9 -0
  127. package/dist/mcp/server.js.map +1 -1
  128. package/dist/mcp-client/config.d.ts +12 -0
  129. package/dist/mcp-client/config.d.ts.map +1 -1
  130. package/dist/mcp-client/config.js.map +1 -1
  131. package/dist/mcp-client/index.d.ts +1 -1
  132. package/dist/mcp-client/index.d.ts.map +1 -1
  133. package/dist/mcp-client/index.js +1 -1
  134. package/dist/mcp-client/index.js.map +1 -1
  135. package/dist/mcp-client/manager.d.ts +2 -0
  136. package/dist/mcp-client/manager.d.ts.map +1 -1
  137. package/dist/mcp-client/manager.js +201 -9
  138. package/dist/mcp-client/manager.js.map +1 -1
  139. package/dist/mcp-client/remote-store.d.ts +27 -0
  140. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  141. package/dist/mcp-client/remote-store.js +105 -0
  142. package/dist/mcp-client/remote-store.js.map +1 -1
  143. package/dist/mcp-client/routes.d.ts +1 -0
  144. package/dist/mcp-client/routes.d.ts.map +1 -1
  145. package/dist/mcp-client/routes.js +1 -0
  146. package/dist/mcp-client/routes.js.map +1 -1
  147. package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
  148. package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
  149. package/dist/provider-api/corpus-jobs.d.ts +8 -8
  150. package/dist/server/agent-chat-plugin.d.ts +2 -0
  151. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  152. package/dist/server/agent-chat-plugin.js +8 -0
  153. package/dist/server/agent-chat-plugin.js.map +1 -1
  154. package/dist/server/index.d.ts +1 -1
  155. package/dist/server/index.d.ts.map +1 -1
  156. package/dist/server/index.js +1 -1
  157. package/dist/server/index.js.map +1 -1
  158. package/dist/shared/mcp-embed-headers.d.ts +3 -1
  159. package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
  160. package/dist/shared/mcp-embed-headers.js +40 -3
  161. package/dist/shared/mcp-embed-headers.js.map +1 -1
  162. package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
  163. package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
  164. package/dist/sharing/actions/share-resource.d.ts +1 -1
  165. package/dist/sharing/schema.d.ts +1 -1
  166. package/dist/vite/client.d.ts.map +1 -1
  167. package/dist/vite/client.js +44 -0
  168. package/dist/vite/client.js.map +1 -1
  169. package/package.json +6 -6
@@ -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
 
@@ -152,6 +152,8 @@ export interface MCPCallerIdentity {
152
152
  oauthScopes?: string[];
153
153
  /** Present only for standard remote MCP OAuth access tokens. */
154
154
  oauthClientId?: string;
155
+ /** Present only for framework-minted first-party MCP client tokens. */
156
+ firstPartyMcp?: boolean;
155
157
  }
156
158
 
157
159
  /** Per-request context used to turn an action's relative deep link into the
@@ -748,7 +750,7 @@ function safeUiSegment(value: string | undefined, fallback: string): string {
748
750
 
749
751
  // ChatGPT and Claude cache MCP App resource HTML by `ui://` URI. Bump this
750
752
  // when the shared shell changes in a way that must invalidate host caches.
751
- const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v46";
753
+ const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v51";
752
754
 
753
755
  function legacyDefaultMcpAppUri(config: MCPConfig, actionName: string): string {
754
756
  const app = safeUiSegment(config.appId ?? config.name, "agent-native");
@@ -1785,6 +1787,7 @@ function addSecretCandidate(
1785
1787
 
1786
1788
  async function verifyA2AJwtForMcp(
1787
1789
  token: string,
1790
+ resourceUrl?: string | string[],
1788
1791
  ): Promise<Record<string, unknown> | null> {
1789
1792
  const jose = await import("jose");
1790
1793
  let unverifiedPayload: Record<string, unknown> | null = null;
@@ -1813,21 +1816,43 @@ async function verifyA2AJwtForMcp(
1813
1816
  }
1814
1817
  }
1815
1818
 
1819
+ const firstPartyMcp = unverifiedPayload.agent_native_first_party_mcp === true;
1820
+ const audiences = firstPartyMcp ? mcpAudienceList(resourceUrl) : null;
1821
+ if (firstPartyMcp && !audiences?.length) return null;
1822
+
1816
1823
  for (const secret of candidateSecrets) {
1817
- try {
1818
- const { payload } = await jose.jwtVerify(
1819
- token,
1820
- new TextEncoder().encode(secret),
1821
- );
1822
- return payload as Record<string, unknown>;
1823
- } catch {
1824
- // Try the next candidate without exposing which secret matched.
1824
+ const encodedSecret = new TextEncoder().encode(secret);
1825
+ for (const audience of audiences ?? [undefined]) {
1826
+ try {
1827
+ const { payload } = await jose.jwtVerify(
1828
+ token,
1829
+ encodedSecret,
1830
+ audience ? { audience } : undefined,
1831
+ );
1832
+ return payload as Record<string, unknown>;
1833
+ } catch {
1834
+ // Try the next candidate without exposing which secret matched.
1835
+ }
1825
1836
  }
1826
1837
  }
1827
1838
 
1828
1839
  return null;
1829
1840
  }
1830
1841
 
1842
+ function mcpAudienceList(resource: string | string[] | undefined): string[] {
1843
+ const raw = Array.isArray(resource) ? resource : resource ? [resource] : [];
1844
+ const seen = new Set<string>();
1845
+ const out: string[] = [];
1846
+ for (const value of raw) {
1847
+ const normalized = value.replace(/\/+$/, "");
1848
+ if (normalized && !seen.has(normalized)) {
1849
+ seen.add(normalized);
1850
+ out.push(normalized);
1851
+ }
1852
+ }
1853
+ return out;
1854
+ }
1855
+
1831
1856
  async function isConnectTokenAllowed(
1832
1857
  jti: string | undefined,
1833
1858
  ): Promise<boolean> {
@@ -1941,7 +1966,7 @@ export async function verifyAuth(
1941
1966
 
1942
1967
  // Try an A2A JWT via the shared A2A_SECRET first, then the caller org's
1943
1968
  // synced A2A secret when the token carries org_domain.
1944
- const payload = await verifyA2AJwtForMcp(token);
1969
+ const payload = await verifyA2AJwtForMcp(token, options.resourceUrl);
1945
1970
  if (payload) {
1946
1971
  const tokenScope =
1947
1972
  typeof payload.scope === "string" ? payload.scope : undefined;
@@ -1976,6 +2001,9 @@ export async function verifyAuth(
1976
2001
  typeof payload.org_domain === "string"
1977
2002
  ? (payload.org_domain as string)
1978
2003
  : undefined,
2004
+ ...(payload.agent_native_first_party_mcp === true
2005
+ ? { firstPartyMcp: true }
2006
+ : {}),
1979
2007
  },
1980
2008
  // Verified JWT (connect-minted or A2A delegation) — a real caller.
1981
2009
  fullSurface: true,
@@ -114,6 +114,7 @@ export function embedApp(
114
114
  const frameReadyMessageDelays = [0, 200, 500, 1500, 3000, 7000, 15000, 30000];
115
115
  const frameReadyTimeoutMs = 45000;
116
116
  const frameLoadTimeoutMs = 45000;
117
+ const maxEmbedSessionRefreshAttempts = 2;
117
118
  const defaultOpenAiBridgeWaitMs = 200;
118
119
  const chatGptOpenAiBridgeWaitMs = 5000;
119
120
  const openAiBridgePollMs = 50;
@@ -121,6 +122,7 @@ export function embedApp(
121
122
  const nativeBridgeRequestTimeoutMs = 30000;
122
123
  const wrapperRequestTimeoutMs = 5000;
123
124
  let app = null;
125
+ let appConnectPromise = null;
124
126
  let openAiBridge = null;
125
127
  let wrapperRequestId = 0;
126
128
  const wrapperRequests = new Map();
@@ -134,6 +136,7 @@ export function embedApp(
134
136
  let appFrameReadyTimer = null;
135
137
  let appFrameLoadTimer = null;
136
138
  let lastFrameSrc = "";
139
+ let embedSessionRefreshAttempts = 0;
137
140
 
138
141
  function esc(value) {
139
142
  return String(value ?? "")
@@ -269,9 +272,9 @@ export function embedApp(
269
272
  record.deepLinkUrl,
270
273
  record.deepLink,
271
274
  record.openUrl,
272
- record.url,
273
275
  structuredOpenLinkUrl,
274
- metaUrl
276
+ metaUrl,
277
+ record.url
275
278
  ]);
276
279
  }
277
280
 
@@ -324,6 +327,12 @@ export function embedApp(
324
327
  try { appFrame.contentWindow.postMessage(message, "*"); } catch {}
325
328
  }
326
329
 
330
+ function notifyOuterMcpAppReady() {
331
+ try {
332
+ window.parent.postMessage({ type: "agentNative.embeddedAppReady" }, "*");
333
+ } catch {}
334
+ }
335
+
327
336
  function nextWrapperRequestId() {
328
337
  wrapperRequestId += 1;
329
338
  return "mcp-wrapper-" + Date.now() + "-" + wrapperRequestId;
@@ -640,6 +649,21 @@ export function embedApp(
640
649
  );
641
650
  }
642
651
 
652
+ function importHeadChildrenWithBase(source, baseHref) {
653
+ const base = document.createElement("base");
654
+ base.href = baseHref;
655
+ const nodes = Array.from(source.childNodes).filter((node) => {
656
+ return !(
657
+ node.nodeType === 1 &&
658
+ String(node.nodeName || "").toLowerCase() === "base"
659
+ );
660
+ });
661
+ document.head.replaceChildren(
662
+ base,
663
+ ...nodes.map((node) => document.importNode(node, true))
664
+ );
665
+ }
666
+
643
667
  function isModuleScript(script) {
644
668
  return (script.getAttribute("type") || "").trim().toLowerCase() === "module";
645
669
  }
@@ -835,10 +859,7 @@ export function embedApp(
835
859
  );
836
860
  const scripts = Array.from(parsed.querySelectorAll("script"));
837
861
  copyDocumentElementAttributes(parsed.documentElement);
838
- importChildren(parsed.head, document.head);
839
- const base = document.createElement("base");
840
- base.href = config.baseHref;
841
- document.head.prepend(base);
862
+ importHeadChildrenWithBase(parsed.head, config.baseHref);
842
863
  importChildren(parsed.body, document.body);
843
864
  installExternalOpenControl(appUrl);
844
865
  for (const script of scripts) {
@@ -920,6 +941,7 @@ export function embedApp(
920
941
  window.history.replaceState(window.history.state, "", localPathFromUrl(appUrl, false));
921
942
  } catch {}
922
943
  await mountTransplantedHtml(html, appUrl);
944
+ embedSessionRefreshAttempts = 0;
923
945
  notifyHostHeightRepeatedly();
924
946
  }
925
947
 
@@ -1159,6 +1181,7 @@ export function embedApp(
1159
1181
  frame.addEventListener("load", () => {
1160
1182
  if (appFrame !== frame) return;
1161
1183
  clearFrameLoadTimer();
1184
+ notifyOuterMcpAppReady();
1162
1185
  sendFrameReadyMessages(frame);
1163
1186
  startFrameReadyTimer(frame);
1164
1187
  });
@@ -1177,6 +1200,13 @@ export function embedApp(
1177
1200
  renderFrameFallback();
1178
1201
  return;
1179
1202
  }
1203
+ if (embedSessionRefreshAttempts >= maxEmbedSessionRefreshAttempts) {
1204
+ renderAppLaunchError(
1205
+ "Embedded app session expired. Reopen the app to mint a fresh session."
1206
+ );
1207
+ return;
1208
+ }
1209
+ embedSessionRefreshAttempts += 1;
1180
1210
  openStartUrl = "";
1181
1211
  startedFor = "";
1182
1212
  lastFrameSrc = "";
@@ -1193,19 +1223,6 @@ export function embedApp(
1193
1223
  return true;
1194
1224
  }
1195
1225
 
1196
- // We have connected to a standards-track MCP Apps host (Codex, Cursor,
1197
- // Claude over the SDK, our own renderer, …) through the postMessage
1198
- // \`ui/*\` bridge rather than ChatGPT's \`window.openai\` global. These hosts
1199
- // render the resource in a strict sandboxed iframe (typically
1200
- // \`sandbox="allow-scripts"\`, opaque origin). Self-navigating that iframe to
1201
- // the real app origin tears down the host bridge and loses the opaque-origin
1202
- // auth context, which shows up as a permanent / flashing loading state.
1203
- // Transplanting the app document into the shell keeps the bridge alive and
1204
- // works under the opaque origin via embed-token auth, exactly like Claude.
1205
- function isNativeMcpAppsBridgeHost() {
1206
- return !!app && !openAiBridge;
1207
- }
1208
-
1209
1226
  function shouldTransplantAppDocument() {
1210
1227
  const render = renderModeSource();
1211
1228
  const mode = render.mode;
@@ -1215,7 +1232,6 @@ export function embedApp(
1215
1232
  return (
1216
1233
  isClaudeMcpContentHost() ||
1217
1234
  isChatGptSandboxHost() ||
1218
- isNativeMcpAppsBridgeHost() ||
1219
1235
  mode === "transplant" ||
1220
1236
  render.frame === "transplant"
1221
1237
  );
@@ -1240,10 +1256,29 @@ export function embedApp(
1240
1256
  }
1241
1257
 
1242
1258
  function shouldRenderControlledAppFrame() {
1243
- return !!openAiBridge || isChatGptSandboxHost();
1259
+ return !!openAiBridge || !!app || isChatGptSandboxHost();
1260
+ }
1261
+
1262
+ function shouldDirectRenderKnownAppRoute(src) {
1263
+ return !!app && !openAiBridge && !isEmbedStartUrl(src) && !shouldTransplantAppDocument();
1264
+ }
1265
+
1266
+ function isCurrentFrameUrl(src) {
1267
+ try {
1268
+ return new URL(src, window.location.href).href === window.location.href;
1269
+ } catch {
1270
+ return false;
1271
+ }
1244
1272
  }
1245
1273
 
1246
1274
  function navigateToAppFrame(src) {
1275
+ if (isCurrentFrameUrl(src)) {
1276
+ clearFrameReadyTimer();
1277
+ clearFrameLoadTimer();
1278
+ setMessage("App opened");
1279
+ notifyHostHeightRepeatedly();
1280
+ return;
1281
+ }
1247
1282
  clearFrameReadyTimer();
1248
1283
  clearFrameLoadTimer();
1249
1284
  appFrame = null;
@@ -1257,6 +1292,24 @@ export function embedApp(
1257
1292
  }
1258
1293
  }
1259
1294
 
1295
+ async function ensureHostAppConnected() {
1296
+ if (!app || typeof app.connect !== "function") return;
1297
+ if (!appConnectPromise) {
1298
+ appConnectPromise = Promise.resolve(app.connect())
1299
+ .then(async (value) => {
1300
+ // Let the host process ui/notifications/initialized before follow-up calls.
1301
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
1302
+ await new Promise((resolve) => window.setTimeout(resolve, 0));
1303
+ return value;
1304
+ })
1305
+ .catch((err) => {
1306
+ appConnectPromise = null;
1307
+ throw err;
1308
+ });
1309
+ }
1310
+ await appConnectPromise;
1311
+ }
1312
+
1260
1313
  async function updateHostModelContext(data) {
1261
1314
  const params = {};
1262
1315
  if (Array.isArray(data && data.content)) params.content = data.content;
@@ -1271,6 +1324,7 @@ export function embedApp(
1271
1324
  return { ok: true };
1272
1325
  }
1273
1326
  if (!app || typeof app.updateModelContext !== "function") return { ok: false };
1327
+ await ensureHostAppConnected();
1274
1328
  await app.updateModelContext(params);
1275
1329
  return { ok: true };
1276
1330
  }
@@ -1356,6 +1410,7 @@ export function embedApp(
1356
1410
  agentNativeModelContext: modelContext
1357
1411
  });
1358
1412
  } else if (app && typeof app.updateModelContext === "function") {
1413
+ await ensureHostAppConnected();
1359
1414
  await app.updateModelContext(modelContext);
1360
1415
  }
1361
1416
  } catch (err) {
@@ -1372,6 +1427,7 @@ export function embedApp(
1372
1427
  }
1373
1428
  let result = null;
1374
1429
  if (app && typeof app.sendMessage === "function") {
1430
+ await ensureHostAppConnected();
1375
1431
  result = await app.sendMessage({
1376
1432
  role: "user",
1377
1433
  content
@@ -1412,8 +1468,10 @@ export function embedApp(
1412
1468
  const data = event.data.data || {};
1413
1469
  if (event.data.type === "agentNative.embeddedAppReady") {
1414
1470
  appFrameReady = true;
1471
+ embedSessionRefreshAttempts = 0;
1415
1472
  clearFrameLoadTimer();
1416
1473
  clearFrameReadyTimer();
1474
+ notifyOuterMcpAppReady();
1417
1475
  return;
1418
1476
  }
1419
1477
  if (event.data.type === "agentNative.embedSessionExpired") {
@@ -1454,7 +1512,7 @@ export function embedApp(
1454
1512
  }
1455
1513
 
1456
1514
  async function launchEmbed() {
1457
- const launchUrl = openStartUrl || openUrl;
1515
+ let launchUrl = openStartUrl || openUrl;
1458
1516
  if (!launchUrl) {
1459
1517
  renderAppLaunchError("Open link was not available.");
1460
1518
  return;
@@ -1463,11 +1521,11 @@ export function embedApp(
1463
1521
  setMessage("Ready to open.");
1464
1522
  return;
1465
1523
  }
1466
- if (startedFor === launchUrl) return;
1467
- startedFor = launchUrl;
1468
1524
  setMessage("Loading app");
1469
1525
  try {
1470
1526
  const selfNavigate = shouldSelfNavigateToApp();
1527
+ if (startedFor === launchUrl) return;
1528
+ startedFor = launchUrl;
1471
1529
  const embedUrl = withChatBridgeParam(launchUrl);
1472
1530
  if (selfNavigate && isEmbedStartUrl(embedUrl)) {
1473
1531
  if (shouldTransplantAppDocument()) {
@@ -1483,6 +1541,10 @@ export function embedApp(
1483
1541
  renderFrame(embedUrl);
1484
1542
  return;
1485
1543
  }
1544
+ if (shouldDirectRenderKnownAppRoute(embedUrl)) {
1545
+ renderFrame(embedUrl);
1546
+ return;
1547
+ }
1486
1548
  const result = await callEmbedSessionTool(embedSessionArgsFor(embedUrl));
1487
1549
  const data = parseToolResult(result);
1488
1550
  if (typeof data.startUrl !== "string" || !data.startUrl) {
@@ -1640,7 +1702,7 @@ export function embedApp(
1640
1702
 
1641
1703
  function createNativeMcpAppsBridge() {
1642
1704
  let rpcId = 0;
1643
- let connected = false;
1705
+ let connectPromise = null;
1644
1706
  let hostContext = {};
1645
1707
  const pendingRequests = new Map();
1646
1708
 
@@ -1708,26 +1770,32 @@ export function embedApp(
1708
1770
  return hostContext.protocolVersion || "mcp-apps-postmessage";
1709
1771
  },
1710
1772
  async connect() {
1711
- if (connected) return hostContext;
1712
- connected = true;
1713
- window.addEventListener("message", onMessage, { passive: true });
1714
- const result = await rpcRequest(
1715
- "ui/initialize",
1716
- {
1717
- appInfo: { name: "Agent Native Embed", version: "1.0.0" },
1718
- appCapabilities: {},
1719
- protocolVersion: "2026-01-26"
1720
- },
1721
- nativeBridgeInitializeTimeoutMs
1722
- );
1723
- hostContext = objectValue(result);
1724
- rpcNotify("ui/notifications/initialized", {});
1725
- if (typeof nativeApp.onhostcontextchanged === "function") {
1726
- nativeApp.onhostcontextchanged(hostContext);
1727
- }
1728
- return hostContext;
1773
+ if (connectPromise) return await connectPromise;
1774
+ connectPromise = (async () => {
1775
+ window.addEventListener("message", onMessage, { passive: true });
1776
+ const result = await rpcRequest(
1777
+ "ui/initialize",
1778
+ {
1779
+ appInfo: { name: "Agent Native Embed", version: "1.0.0" },
1780
+ appCapabilities: {},
1781
+ protocolVersion: "2026-01-26"
1782
+ },
1783
+ nativeBridgeInitializeTimeoutMs
1784
+ );
1785
+ hostContext = objectValue(result);
1786
+ rpcNotify("ui/notifications/initialized", {});
1787
+ if (typeof nativeApp.onhostcontextchanged === "function") {
1788
+ nativeApp.onhostcontextchanged(hostContext);
1789
+ }
1790
+ return hostContext;
1791
+ })().catch((err) => {
1792
+ connectPromise = null;
1793
+ throw err;
1794
+ });
1795
+ return await connectPromise;
1729
1796
  },
1730
1797
  async callServerTool(request) {
1798
+ await nativeApp.connect();
1731
1799
  const record = objectValue(request);
1732
1800
  return await rpcRequest("tools/call", {
1733
1801
  name: record.name,
@@ -1735,6 +1803,7 @@ export function embedApp(
1735
1803
  });
1736
1804
  },
1737
1805
  async updateModelContext(params) {
1806
+ await nativeApp.connect();
1738
1807
  return await rpcRequest("ui/update-model-context", objectValue(params));
1739
1808
  },
1740
1809
  async openLink(params) {
@@ -1749,12 +1818,14 @@ export function embedApp(
1749
1818
  return opened ? { ok: true } : { isError: true };
1750
1819
  },
1751
1820
  async requestDisplayMode(params) {
1821
+ await nativeApp.connect();
1752
1822
  return await rpcRequest("ui/request-display-mode", objectValue(params));
1753
1823
  },
1754
1824
  sendSizeChanged(params) {
1755
1825
  rpcNotify("ui/notifications/size-changed", objectValue(params));
1756
1826
  },
1757
1827
  async sendMessage(params) {
1828
+ await nativeApp.connect();
1758
1829
  return await rpcRequest("ui/message", objectValue(params));
1759
1830
  }
1760
1831
  };
@@ -1798,6 +1869,7 @@ export function embedApp(
1798
1869
 
1799
1870
  async function startNativeMcpAppsBridge() {
1800
1871
  app = createNativeMcpAppsBridge();
1872
+ appConnectPromise = null;
1801
1873
  app.ontoolinput = (params) => {
1802
1874
  toolInput = params.arguments || {};
1803
1875
  };
@@ -1815,7 +1887,8 @@ export function embedApp(
1815
1887
  notifyHostHeight();
1816
1888
  sendHostContext();
1817
1889
  };
1818
- await app.connect();
1890
+ await ensureHostAppConnected();
1891
+ notifyOuterMcpAppReady();
1819
1892
  updateDisplayButton();
1820
1893
  notifyHostHeight();
1821
1894
  sendHostContext();
@@ -1828,6 +1901,7 @@ export function embedApp(
1828
1901
  {},
1829
1902
  { autoResize: false }
1830
1903
  );
1904
+ appConnectPromise = null;
1831
1905
  app.ontoolinput = (params) => {
1832
1906
  toolInput = params.arguments || {};
1833
1907
  };
@@ -1845,7 +1919,8 @@ export function embedApp(
1845
1919
  notifyHostHeight();
1846
1920
  sendHostContext();
1847
1921
  };
1848
- await app.connect();
1922
+ await ensureHostAppConnected();
1923
+ notifyOuterMcpAppReady();
1849
1924
  updateDisplayButton();
1850
1925
  notifyHostHeight();
1851
1926
  sendHostContext();
@@ -37,3 +37,8 @@ export {
37
37
  findWorkspaceRoot,
38
38
  } from "./workspace-resolve.js";
39
39
  export type { ResolvedApp, ResolvedWorkspace } from "./workspace-resolve.js";
40
+ export {
41
+ fetchOrgApps,
42
+ resolveOrgDirectoryOrigin,
43
+ type OrgApp,
44
+ } from "./org-directory.js";
@@ -146,6 +146,10 @@ function scopedCacheKey(
146
146
  ].join("|");
147
147
  }
148
148
 
149
+ function serviceScopedCacheKey(origin: string, orgId: string): string {
150
+ return [origin, `service-org:${orgId}`].join("|");
151
+ }
152
+
149
153
  /**
150
154
  * Fetch the org's first-party sibling apps from the org directory.
151
155
  *
@@ -162,6 +166,7 @@ function scopedCacheKey(
162
166
  export async function fetchOrgApps(opts?: {
163
167
  selfId?: string;
164
168
  selfOrigin?: string;
169
+ serviceOrgId?: string;
165
170
  env?: NodeJS.ProcessEnv;
166
171
  }): Promise<OrgApp[]> {
167
172
  const env = opts?.env ?? process.env;
@@ -182,24 +187,32 @@ export async function fetchOrgApps(opts?: {
182
187
  let cacheKey: string | null = null;
183
188
  let apps: OrgApp[] = [];
184
189
  let ttl = EMPTY_TTL_MS;
190
+ const serviceOrgId = opts?.serviceOrgId?.trim();
191
+ if (serviceOrgId) {
192
+ const serviceCacheKey = serviceScopedCacheKey(origin, serviceOrgId);
193
+ const cached = cache.get(serviceCacheKey);
194
+ if (cached && cached.expiresAt > Date.now()) {
195
+ return stripSelf(cached.apps);
196
+ }
197
+ cacheKey = serviceCacheKey;
198
+ }
185
199
  try {
186
- // Reuse the existing A2A caller-auth: it reads userEmail + orgId from the
187
- // request context, loads the org A2A secret via getOrgA2ASecret (falling
188
- // back to the global A2A_SECRET env), and signs the same bearer JWT A2A
189
- // peers already use. No new secret loading is invented here.
190
- const { resolveA2ACallerAuth } = await import("../a2a/caller-auth.js");
191
- const auth = await resolveA2ACallerAuth();
200
+ const auth = serviceOrgId
201
+ ? await resolveOrgDirectoryServiceAuth(serviceOrgId)
202
+ : await resolveOrgDirectoryCallerAuth();
192
203
  if (!auth.apiKey) {
193
204
  // No signed token available (no A2A secret / no caller identity) — the
194
205
  // directory requires the org bearer, so degrade silently to local-only.
195
206
  return [];
196
207
  }
197
208
 
198
- const now = Date.now();
199
- cacheKey = scopedCacheKey(origin, auth);
200
- const cached = cache.get(cacheKey);
201
- if (cached && cached.expiresAt > now) {
202
- return stripSelf(cached.apps);
209
+ if (!cacheKey) {
210
+ const now = Date.now();
211
+ cacheKey = scopedCacheKey(origin, auth);
212
+ const cached = cache.get(cacheKey);
213
+ if (cached && cached.expiresAt > now) {
214
+ return stripSelf(cached.apps);
215
+ }
203
216
  }
204
217
 
205
218
  const res = await fetch(`${origin}/_agent-native/org/apps`, {
@@ -236,3 +249,49 @@ export async function fetchOrgApps(opts?: {
236
249
  export function _resetOrgDirectoryCache(): void {
237
250
  cache.clear();
238
251
  }
252
+
253
+ async function resolveOrgDirectoryCallerAuth(): Promise<{
254
+ apiKey?: string;
255
+ userEmail?: string;
256
+ orgId?: string;
257
+ orgDomain?: string;
258
+ }> {
259
+ // Reuse the existing A2A caller-auth: it reads userEmail + orgId from the
260
+ // request context, loads the org A2A secret via getOrgA2ASecret (falling
261
+ // back to the global A2A_SECRET env), and signs the same bearer JWT A2A
262
+ // peers already use. No new secret loading is invented for normal callers.
263
+ const { resolveA2ACallerAuth } = await import("../a2a/caller-auth.js");
264
+ return resolveA2ACallerAuth();
265
+ }
266
+
267
+ async function resolveOrgDirectoryServiceAuth(orgId: string): Promise<{
268
+ apiKey?: string;
269
+ userEmail?: string;
270
+ orgId?: string;
271
+ orgDomain?: string;
272
+ }> {
273
+ const trimmedOrgId = orgId.trim();
274
+ if (!trimmedOrgId) return {};
275
+ let orgDomain: string | undefined;
276
+ let orgSecret: string | undefined;
277
+ try {
278
+ const { getOrgDomain, getOrgA2ASecret } = await import("../org/context.js");
279
+ orgDomain = (await getOrgDomain(trimmedOrgId)) ?? undefined;
280
+ orgSecret = (await getOrgA2ASecret(trimmedOrgId)) ?? undefined;
281
+ } catch {}
282
+ try {
283
+ const [{ signA2AToken }, { serviceIdentityEmail }] = await Promise.all([
284
+ import("../a2a/client.js"),
285
+ import("./connect-store.js"),
286
+ ]);
287
+ const userEmail = serviceIdentityEmail("mcp-client", trimmedOrgId);
288
+ const apiKey = await signA2AToken(userEmail, orgDomain, orgSecret, {
289
+ expiresIn: "5m",
290
+ preferGlobalSecret: !orgSecret,
291
+ extraClaims: { org_id: trimmedOrgId },
292
+ });
293
+ return { apiKey, userEmail, orgId: trimmedOrgId, orgDomain };
294
+ } catch {
295
+ return { orgId: trimmedOrgId, orgDomain };
296
+ }
297
+ }
@@ -103,6 +103,14 @@ function deriveRequestMeta(event: H3Event): MCPRequestMeta {
103
103
  fullCatalogHeader === "1" ||
104
104
  fullCatalogHeader === "true" ||
105
105
  fullCatalogHeader === "yes";
106
+ const inlineAppsHeader = getRequestHeader(
107
+ event,
108
+ "x-agent-native-mcp-inline-apps",
109
+ )?.toLowerCase();
110
+ const inlineAppsRequested =
111
+ inlineAppsHeader === "1" ||
112
+ inlineAppsHeader === "true" ||
113
+ inlineAppsHeader === "yes";
106
114
  const basePath = getConfiguredAppBasePath();
107
115
  return {
108
116
  origin,
@@ -111,6 +119,7 @@ function deriveRequestMeta(event: H3Event): MCPRequestMeta {
111
119
  clientName,
112
120
  clientHint,
113
121
  ...(fullCatalog ? { fullCatalog } : {}),
122
+ ...(inlineAppsRequested ? { inlineMcpApps: true } : {}),
114
123
  };
115
124
  }
116
125
 
@@ -357,6 +366,11 @@ export async function handleMcpRequest(
357
366
  const server = await createMCPServerForRequest(config, authResult.identity, {
358
367
  ...requestMeta,
359
368
  fullSurface: authResult.fullSurface === true,
369
+ inlineMcpApps:
370
+ requestMeta.inlineMcpApps === true &&
371
+ authResult.identity?.firstPartyMcp === true
372
+ ? true
373
+ : undefined,
360
374
  // When the caller minted their token with --full-catalog (catalog_scope:
361
375
  // "full" JWT claim), bypass the connector-catalog tier filter.
362
376
  ...(authResult.fullCatalog === true ? { fullCatalog: true } : {}),