@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
@@ -45,6 +45,18 @@ export interface McpHttpServerConfig {
45
45
  url: string;
46
46
  /** Extra headers to send with every request (e.g. Authorization). */
47
47
  headers?: Record<string, string>;
48
+ /**
49
+ * Trusted first-party Agent-Native app. This is set only by framework-owned
50
+ * org-scoped registrations, not by raw file/env config.
51
+ */
52
+ firstParty?: boolean;
53
+ /** Canonical first-party app id from the org directory, e.g. `assets`. */
54
+ firstPartyAppId?: string;
55
+ /**
56
+ * Org id the first-party server is trusted for. Runtime-only metadata set by
57
+ * framework-owned registrations; raw file/env config cannot provide it.
58
+ */
59
+ firstPartyOrgId?: string;
48
60
  /** Human-readable description (optional, shown in /mcp/status) */
49
61
  description?: string;
50
62
  }
@@ -24,6 +24,8 @@ export {
24
24
  export {
25
25
  listRemoteServers,
26
26
  addRemoteServer,
27
+ addFirstPartyRemoteServer,
28
+ isFirstPartyRemoteEndpointTrusted,
27
29
  removeRemoteServer,
28
30
  validateRemoteUrl,
29
31
  normalizeServerName,
@@ -11,6 +11,10 @@
11
11
  import type { McpConfig, McpServerConfig } from "./config.js";
12
12
  import { formatMcpConnectError } from "./errors.js";
13
13
  import { MCP_APP_EXTENSION_ID, MCP_APP_MIME_TYPE } from "../action.js";
14
+ import {
15
+ isFirstPartyRemoteEndpointTrusted,
16
+ parseMergedKey,
17
+ } from "./remote-store.js";
14
18
 
15
19
  export const MCP_TOOL_PREFIX = "mcp__";
16
20
 
@@ -93,7 +97,10 @@ function sameServerConfig(a: McpServerConfig, b: McpServerConfig): boolean {
93
97
  if (typeA === "http" && b.type === "http" && a.type === "http") {
94
98
  return (
95
99
  a.url === b.url &&
96
- JSON.stringify(a.headers ?? {}) === JSON.stringify(b.headers ?? {})
100
+ JSON.stringify(a.headers ?? {}) === JSON.stringify(b.headers ?? {}) &&
101
+ a.firstParty === b.firstParty &&
102
+ a.firstPartyAppId === b.firstPartyAppId &&
103
+ a.firstPartyOrgId === b.firstPartyOrgId
97
104
  );
98
105
  }
99
106
  if (a.type !== "http" && b.type !== "http") {
@@ -140,6 +147,38 @@ type SdkModules = {
140
147
  StreamableHTTPClientTransport: any | null;
141
148
  };
142
149
 
150
+ const DEFAULT_MCP_CONNECT_TIMEOUT_MS = 5_000;
151
+
152
+ function mcpConnectTimeoutMs(): number {
153
+ const raw =
154
+ typeof process !== "undefined"
155
+ ? process.env.AGENT_NATIVE_MCP_CLIENT_CONNECT_TIMEOUT_MS ||
156
+ process.env.MCP_CLIENT_CONNECT_TIMEOUT_MS
157
+ : undefined;
158
+ const parsed = raw ? Number(raw) : NaN;
159
+ if (Number.isFinite(parsed) && parsed >= 0) return parsed;
160
+ return DEFAULT_MCP_CONNECT_TIMEOUT_MS;
161
+ }
162
+
163
+ async function withConnectTimeout<T>(
164
+ work: Promise<T>,
165
+ label: string,
166
+ timeoutMs: number,
167
+ ): Promise<T> {
168
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return work;
169
+ let timer: ReturnType<typeof setTimeout> | undefined;
170
+ const timeout = new Promise<never>((_resolve, reject) => {
171
+ timer = setTimeout(() => {
172
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
173
+ }, timeoutMs);
174
+ });
175
+ try {
176
+ return await Promise.race([work, timeout]);
177
+ } finally {
178
+ if (timer) clearTimeout(timer);
179
+ }
180
+ }
181
+
143
182
  export class McpClientManager {
144
183
  private readonly servers: Map<string, ServerEntry> = new Map();
145
184
  private readonly debug: boolean;
@@ -335,12 +374,33 @@ export class McpClientManager {
335
374
  throw new Error("HTTP transport not available");
336
375
  }
337
376
  const requestInit: Record<string, unknown> = {};
338
- if (cfg.headers && Object.keys(cfg.headers).length > 0) {
339
- requestInit.headers = cfg.headers;
377
+ const staticHeaders = { ...(cfg.headers ?? {}) };
378
+ const transportOptions: Record<string, unknown> = { requestInit };
379
+ if (this.shouldInjectFirstPartyIdentity(entry)) {
380
+ const baselineHeaders = { ...staticHeaders };
381
+ removeAuthorizationHeaders(baselineHeaders);
382
+ if (Object.keys(baselineHeaders).length > 0) {
383
+ requestInit.headers = baselineHeaders;
384
+ }
385
+ if (typeof fetch === "function") {
386
+ const nativeFetch = fetch.bind(globalThis);
387
+ transportOptions.fetch = async (input: unknown, init?: unknown) =>
388
+ nativeFetch(
389
+ input as Parameters<typeof fetch>[0],
390
+ await buildFirstPartyRequestInit(
391
+ staticHeaders,
392
+ init,
393
+ this.firstPartyTrust(entry),
394
+ ),
395
+ );
396
+ }
397
+ } else if (Object.keys(staticHeaders).length > 0) {
398
+ requestInit.headers = staticHeaders;
340
399
  }
341
- transport = new sdk.StreamableHTTPClientTransport(new URL(cfg.url), {
342
- requestInit,
343
- });
400
+ transport = new sdk.StreamableHTTPClientTransport(
401
+ new URL(cfg.url),
402
+ transportOptions,
403
+ );
344
404
  } else {
345
405
  if (!sdk.StdioClientTransport) {
346
406
  throw new Error(
@@ -411,8 +471,17 @@ export class McpClientManager {
411
471
  // process (stdio) or pending HTTP session — otherwise repeated failures
412
472
  // leak transports. Assign to the entry only after the handshake succeeds.
413
473
  try {
414
- await client.connect(transport);
415
- const listed = await client.listTools();
474
+ const timeoutMs = mcpConnectTimeoutMs();
475
+ await withConnectTimeout(
476
+ Promise.resolve(client.connect(transport)),
477
+ `MCP server ${entry.id} connect`,
478
+ timeoutMs,
479
+ );
480
+ const listed = await withConnectTimeout(
481
+ Promise.resolve(client.listTools()),
482
+ `MCP server ${entry.id} tools/list`,
483
+ timeoutMs,
484
+ );
416
485
  const rawTools: Array<{
417
486
  name: string;
418
487
  title?: string;
@@ -509,7 +578,9 @@ export class McpClientManager {
509
578
  } else if (!sameServerConfig(prevServers[id], nextServers[id])) {
510
579
  reconnected.push(id);
511
580
  } else {
512
- unchanged.push(id);
581
+ const entry = this.servers.get(id);
582
+ if (entry?.error) reconnected.push(id);
583
+ else unchanged.push(id);
513
584
  }
514
585
  }
515
586
  for (const id of Object.keys(nextServers)) {
@@ -704,4 +775,192 @@ export class McpClientManager {
704
775
  errors,
705
776
  };
706
777
  }
778
+
779
+ private shouldInjectFirstPartyIdentity(entry: ServerEntry): boolean {
780
+ return this.firstPartyTrust(entry) !== null;
781
+ }
782
+
783
+ private firstPartyTrust(
784
+ entry: ServerEntry,
785
+ ): { orgId: string; appId: string; url: string } | null {
786
+ if (entry.config.type !== "http" || entry.config.firstParty !== true) {
787
+ return null;
788
+ }
789
+ const parsed = parseMergedKey(entry.id);
790
+ if (parsed?.scope !== "org") return null;
791
+ return {
792
+ orgId: entry.config.firstPartyOrgId ?? parsed.owner,
793
+ appId: entry.config.firstPartyAppId ?? parsed.name,
794
+ url: entry.config.url,
795
+ };
796
+ }
797
+ }
798
+
799
+ function removeAuthorizationHeaders(headers: Record<string, string>): void {
800
+ for (const key of Object.keys(headers)) {
801
+ if (key.toLowerCase() === "authorization") delete headers[key];
802
+ }
803
+ }
804
+
805
+ function headersToRecord(value: unknown): Record<string, string> {
806
+ if (!value) return {};
807
+ if (typeof Headers !== "undefined" && value instanceof Headers) {
808
+ return Object.fromEntries(value.entries());
809
+ }
810
+ if (Array.isArray(value)) {
811
+ return Object.fromEntries(
812
+ value
813
+ .filter((entry) => Array.isArray(entry) && entry.length >= 2)
814
+ .map(([key, headerValue]) => [String(key), String(headerValue)]),
815
+ );
816
+ }
817
+ if (typeof value === "object") {
818
+ return Object.fromEntries(
819
+ Object.entries(value as Record<string, unknown>).map(
820
+ ([key, headerValue]) => [key, String(headerValue)],
821
+ ),
822
+ );
823
+ }
824
+ return {};
825
+ }
826
+
827
+ async function buildFirstPartyRequestInit(
828
+ staticHeaders: Record<string, string>,
829
+ rawInit: unknown,
830
+ trust: { orgId: string; appId: string; url: string } | null,
831
+ ): Promise<RequestInit> {
832
+ const init =
833
+ rawInit && typeof rawInit === "object" ? (rawInit as RequestInit) : {};
834
+ const headers = {
835
+ ...headersToRecord(staticHeaders),
836
+ ...headersToRecord(init.headers),
837
+ };
838
+ removeAuthorizationHeaders(headers);
839
+ const token = await mintFirstPartyMcpIdentityToken(trust).catch(() => null);
840
+ if (token) headers.Authorization = `Bearer ${token}`;
841
+ headers["x-agent-native-mcp-inline-apps"] = "1";
842
+ return { ...init, headers };
843
+ }
844
+
845
+ function randomJti(): string {
846
+ return (
847
+ globalThis.crypto?.randomUUID?.() ??
848
+ `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
849
+ );
850
+ }
851
+
852
+ const FIRST_PARTY_TRUST_TTL_MS = 60_000;
853
+ const FIRST_PARTY_TRUST_FAILURE_TTL_MS = 10_000;
854
+ const ORG_SIGNING_CONTEXT_TTL_MS = 60_000;
855
+
856
+ const firstPartyTrustCache = new Map<
857
+ string,
858
+ { ok: boolean; expiresAt: number }
859
+ >();
860
+ const orgSigningContextCache = new Map<
861
+ string,
862
+ { orgDomain?: string; orgSecret?: string; expiresAt: number }
863
+ >();
864
+
865
+ async function isFirstPartyTrustCached(trust: {
866
+ orgId: string;
867
+ appId: string;
868
+ url: string;
869
+ }): Promise<boolean> {
870
+ const key = `${trust.orgId}\n${trust.appId}\n${trust.url}`;
871
+ const cached = firstPartyTrustCache.get(key);
872
+ if (cached && cached.expiresAt > Date.now()) return cached.ok;
873
+ const result = await isFirstPartyRemoteEndpointTrusted(
874
+ trust.orgId,
875
+ trust.appId,
876
+ trust.url,
877
+ );
878
+ const ok = result.ok === true;
879
+ firstPartyTrustCache.set(key, {
880
+ ok,
881
+ expiresAt:
882
+ Date.now() +
883
+ (ok ? FIRST_PARTY_TRUST_TTL_MS : FIRST_PARTY_TRUST_FAILURE_TTL_MS),
884
+ });
885
+ return ok;
886
+ }
887
+
888
+ async function resolveOrgSigningContext(orgId: string): Promise<{
889
+ orgDomain?: string;
890
+ orgSecret?: string;
891
+ }> {
892
+ const cached = orgSigningContextCache.get(orgId);
893
+ if (cached && cached.expiresAt > Date.now()) {
894
+ return { orgDomain: cached.orgDomain, orgSecret: cached.orgSecret };
895
+ }
896
+ let orgDomain: string | undefined;
897
+ let orgSecret: string | undefined;
898
+ try {
899
+ const { getOrgDomain, getOrgA2ASecret } = await import("../org/context.js");
900
+ orgDomain = (await getOrgDomain(orgId)) ?? undefined;
901
+ orgSecret = (await getOrgA2ASecret(orgId)) ?? undefined;
902
+ } catch {
903
+ // Global A2A_SECRET remains the hosted/common path.
904
+ }
905
+ orgSigningContextCache.set(orgId, {
906
+ orgDomain,
907
+ orgSecret,
908
+ expiresAt: Date.now() + ORG_SIGNING_CONTEXT_TTL_MS,
909
+ });
910
+ return { orgDomain, orgSecret };
911
+ }
912
+
913
+ async function mintFirstPartyMcpIdentityToken(
914
+ trust: { orgId: string; appId: string; url: string } | null,
915
+ ): Promise<string | null> {
916
+ if (!trust) return null;
917
+ const [{ getRequestOrgId, getRequestUserEmail }, { signA2AToken }] =
918
+ await Promise.all([
919
+ import("../server/request-context.js"),
920
+ import("../a2a/client.js"),
921
+ ]);
922
+ const userEmail = getRequestUserEmail();
923
+ const requestOrgId = getRequestOrgId();
924
+ if (requestOrgId && requestOrgId !== trust.orgId) {
925
+ return null;
926
+ }
927
+ const orgId = requestOrgId ?? trust.orgId;
928
+ if (!orgId) return null;
929
+ if (!(await isFirstPartyTrustCached({ ...trust, orgId }))) return null;
930
+
931
+ const { orgDomain, orgSecret } = await resolveOrgSigningContext(orgId);
932
+ const subject =
933
+ userEmail && requestOrgId
934
+ ? userEmail
935
+ : (await import("../mcp/connect-store.js")).serviceIdentityEmail(
936
+ "mcp-client",
937
+ orgId,
938
+ );
939
+
940
+ return signA2AToken(subject, orgDomain, orgSecret, {
941
+ expiresIn: "5m",
942
+ audience: firstPartyMcpAudienceForUrl(trust.url),
943
+ preferGlobalSecret: !orgSecret,
944
+ extraClaims: {
945
+ jti: randomJti(),
946
+ scope: "mcp-connect",
947
+ org_id: orgId,
948
+ agent_native_first_party_mcp: true,
949
+ },
950
+ });
951
+ }
952
+
953
+ function firstPartyMcpAudienceForUrl(rawUrl: string): string | undefined {
954
+ try {
955
+ const url = new URL(rawUrl);
956
+ const pathname = url.pathname.replace(/\/+$/, "");
957
+ url.pathname = pathname.endsWith("/_agent-native/mcp")
958
+ ? pathname
959
+ : `${pathname}/_agent-native/mcp`;
960
+ url.search = "";
961
+ url.hash = "";
962
+ return url.toString().replace(/\/+$/, "");
963
+ } catch {
964
+ return undefined;
965
+ }
707
966
  }
@@ -69,6 +69,14 @@ export interface StoredRemoteMcpServer {
69
69
  * supplied (or for legacy cleartext rows).
70
70
  */
71
71
  headerSecretKey?: string;
72
+ /**
73
+ * Trusted first-party Agent-Native app. Only framework-controlled
74
+ * registrations should set this; management routes intentionally do not
75
+ * expose it for arbitrary user-added MCP servers.
76
+ */
77
+ firstParty?: boolean;
78
+ /** Canonical first-party app id from the org directory, e.g. `assets`. */
79
+ firstPartyAppId?: string;
72
80
  /** Optional description shown in the UI. */
73
81
  description?: string;
74
82
  /** ms since epoch. */
@@ -316,12 +324,66 @@ export async function addRemoteServer(
316
324
  url: string;
317
325
  headers?: Record<string, string>;
318
326
  description?: string;
327
+ firstParty?: boolean;
328
+ },
329
+ ): Promise<
330
+ { ok: true; server: StoredRemoteMcpServer } | { ok: false; error: string }
331
+ > {
332
+ if (input.firstParty) {
333
+ return {
334
+ ok: false,
335
+ error:
336
+ "First-party MCP servers must be registered through the trusted first-party registration path",
337
+ };
338
+ }
339
+ return addRemoteServerInternal(scope, scopeId, input);
340
+ }
341
+
342
+ export async function addFirstPartyRemoteServer(
343
+ orgId: string,
344
+ input: {
345
+ appId: string;
346
+ name: string;
347
+ url: string;
348
+ description?: string;
349
+ },
350
+ ): Promise<
351
+ { ok: true; server: StoredRemoteMcpServer } | { ok: false; error: string }
352
+ > {
353
+ const trust = await isFirstPartyRemoteEndpointTrusted(
354
+ orgId,
355
+ input.appId,
356
+ input.url,
357
+ );
358
+ if (!trust.ok) return { ok: false, error: trust.error };
359
+ return addRemoteServerInternal("org", orgId, {
360
+ name: input.name,
361
+ url: input.url,
362
+ description: input.description,
363
+ firstParty: true,
364
+ firstPartyAppId: input.appId.trim().toLowerCase(),
365
+ });
366
+ }
367
+
368
+ async function addRemoteServerInternal(
369
+ scope: RemoteMcpScope,
370
+ scopeId: string,
371
+ input: {
372
+ name: string;
373
+ url: string;
374
+ headers?: Record<string, string>;
375
+ description?: string;
376
+ firstParty?: boolean;
377
+ firstPartyAppId?: string;
319
378
  },
320
379
  ): Promise<
321
380
  { ok: true; server: StoredRemoteMcpServer } | { ok: false; error: string }
322
381
  > {
323
382
  const name = normalizeServerName(input.name);
324
383
  if (!name) return { ok: false, error: "Name is required" };
384
+ if (input.firstParty && scope !== "org") {
385
+ return { ok: false, error: "First-party MCP servers must be org-scoped" };
386
+ }
325
387
  const urlCheck = validateRemoteUrl(input.url);
326
388
  if (!urlCheck.ok) return { ok: false, error: urlCheck.error ?? "Bad URL" };
327
389
 
@@ -360,6 +422,10 @@ export async function addRemoteServer(
360
422
  url: urlCheck.url!.toString(),
361
423
  headers: cleartext,
362
424
  headerSecretKey,
425
+ ...(input.firstParty ? { firstParty: true } : {}),
426
+ ...(input.firstPartyAppId
427
+ ? { firstPartyAppId: input.firstPartyAppId }
428
+ : {}),
363
429
  description: input.description?.trim() || undefined,
364
430
  createdAt: Date.now(),
365
431
  };
@@ -367,6 +433,83 @@ export async function addRemoteServer(
367
433
  return { ok: true, server };
368
434
  }
369
435
 
436
+ function normalizeEndpointPath(pathname: string): string {
437
+ return pathname.replace(/\/+$/, "") || "/";
438
+ }
439
+
440
+ function expectedFirstPartyMcpUrl(appUrl: string): URL | null {
441
+ try {
442
+ return new URL(`${appUrl.replace(/\/+$/, "")}/_agent-native/mcp`);
443
+ } catch {
444
+ return null;
445
+ }
446
+ }
447
+
448
+ function sameFirstPartyMcpEndpoint(endpointUrl: URL, appUrl: string): boolean {
449
+ const expected = expectedFirstPartyMcpUrl(appUrl);
450
+ if (!expected) return false;
451
+ return (
452
+ endpointUrl.origin === expected.origin &&
453
+ normalizeEndpointPath(endpointUrl.pathname) ===
454
+ normalizeEndpointPath(expected.pathname) &&
455
+ endpointUrl.search === expected.search
456
+ );
457
+ }
458
+
459
+ export async function isFirstPartyRemoteEndpointTrusted(
460
+ orgId: string,
461
+ appId: string,
462
+ endpoint: string,
463
+ ): Promise<{ ok: true } | { ok: false; error: string }> {
464
+ const normalizedAppId = appId.trim().toLowerCase();
465
+ if (!orgId.trim()) {
466
+ return { ok: false, error: "Organization id is required" };
467
+ }
468
+ if (!normalizedAppId) {
469
+ return { ok: false, error: "First-party app id is required" };
470
+ }
471
+
472
+ let endpointUrl: URL;
473
+ try {
474
+ endpointUrl = new URL(endpoint);
475
+ } catch {
476
+ return { ok: false, error: "Not a valid URL" };
477
+ }
478
+
479
+ try {
480
+ const { getRequestOrgId } = await import("../server/request-context.js");
481
+ const requestOrgId = getRequestOrgId();
482
+ if (requestOrgId && requestOrgId !== orgId) {
483
+ return {
484
+ ok: false,
485
+ error:
486
+ "First-party MCP server org does not match the active request organization",
487
+ };
488
+ }
489
+ } catch {
490
+ // No request context helper available: the org-directory lookup below will
491
+ // still fail closed if it cannot authenticate a first-party app list.
492
+ }
493
+
494
+ const { fetchOrgApps } = await import("../mcp/org-directory.js");
495
+ const apps = await fetchOrgApps({ serviceOrgId: orgId });
496
+ const app = apps.find((candidate) => candidate.id === normalizedAppId);
497
+ if (!app) {
498
+ return {
499
+ ok: false,
500
+ error: "Could not verify the first-party app in the org directory",
501
+ };
502
+ }
503
+ if (!sameFirstPartyMcpEndpoint(endpointUrl, app.url)) {
504
+ return {
505
+ ok: false,
506
+ error:
507
+ "First-party MCP URL does not match the org-directory app endpoint",
508
+ };
509
+ }
510
+ return { ok: true };
511
+ }
512
+
370
513
  export async function removeRemoteServer(
371
514
  scope: RemoteMcpScope,
372
515
  scopeId: string,
@@ -463,6 +606,10 @@ export function toHttpServerConfig(
463
606
  type: "http",
464
607
  url: stored.url,
465
608
  headers: stored.headers,
609
+ ...(stored.firstParty ? { firstParty: true } : {}),
610
+ ...(stored.firstPartyAppId
611
+ ? { firstPartyAppId: stored.firstPartyAppId }
612
+ : {}),
466
613
  description: stored.description,
467
614
  };
468
615
  }
@@ -484,6 +631,13 @@ export async function toHttpServerConfigAsync(
484
631
  type: "http",
485
632
  url: stored.url,
486
633
  headers: await materializeHeaders(scope, scopeId, stored),
634
+ ...(stored.firstParty ? { firstParty: true } : {}),
635
+ ...(stored.firstPartyAppId
636
+ ? { firstPartyAppId: stored.firstPartyAppId }
637
+ : {}),
638
+ ...(stored.firstParty && scope === "org"
639
+ ? { firstPartyOrgId: scopeId }
640
+ : {}),
487
641
  description: stored.description,
488
642
  };
489
643
  }
@@ -97,6 +97,7 @@ function projectForClient(
97
97
  url: stored.url,
98
98
  headers: redactHeaders(stored.headers),
99
99
  description: stored.description,
100
+ firstParty: stored.firstParty === true,
100
101
  createdAt: stored.createdAt,
101
102
  mergedId: mergedConfigKey(scope, stored, ownerId),
102
103
  status,
@@ -110,6 +111,7 @@ export interface ClientServer {
110
111
  url: string;
111
112
  headers?: Record<string, { set: true }>;
112
113
  description?: string;
114
+ firstParty?: boolean;
113
115
  createdAt: number;
114
116
  /** The key under which this server is registered in the running MCP manager. */
115
117
  mergedId: string;
@@ -8043,6 +8043,14 @@ export function getGlobalMcpManager(): McpClientManager | null {
8043
8043
  return _globalMcpManager;
8044
8044
  }
8045
8045
 
8046
+ /** Internal: reload the process's MCP client manager after persisted settings change. */
8047
+ export async function refreshGlobalMcpManager(): Promise<boolean> {
8048
+ const manager = getGlobalMcpManager();
8049
+ if (!manager) return false;
8050
+ await manager.reconfigure(await buildMergedConfig());
8051
+ return true;
8052
+ }
8053
+
8046
8054
  function mountMcpHubStatusRoute(nitroApp: any): void {
8047
8055
  const mountedApps: WeakSet<object> = ((
8048
8056
  globalThis as any
@@ -162,6 +162,7 @@ export type { GoogleAuthMode } from "./google-auth-mode.js";
162
162
  export {
163
163
  createAgentChatPlugin,
164
164
  defaultAgentChatPlugin,
165
+ refreshGlobalMcpManager,
165
166
  type AgentChatPluginOptions,
166
167
  } from "./agent-chat-plugin.js";
167
168
  export {
@@ -1,10 +1,16 @@
1
1
  export const MCP_EMBED_CORS_ALLOW_HEADERS =
2
- "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-User-Timezone,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
2
+ "Content-Type,Authorization,X-Requested-With,X-Request-Source,X-Agent-Native-CSRF,X-Agent-Native-Frontend,X-User-Timezone,X-Agent-Native-Embed-Target,X-Agent-Native-Embed-Transplant";
3
3
  export const EMBED_TRANSPLANT_HEADER = "x-agent-native-embed-transplant";
4
4
 
5
5
  const CLAUDE_MCP_CONTENT_HOST_RE = /^[a-f0-9]{32}\.claudemcpcontent\.com$/i;
6
6
  const CHATGPT_MCP_SANDBOX_HOST_RE =
7
7
  /^(?:[^.]+\.)?web-sandbox\.oaiusercontent\.com$/i;
8
+ const AGENT_NATIVE_FIRST_PARTY_APP_HOST_SUFFIX = ".agent-native.com";
9
+ const MCP_PRODUCT_HOST_ORIGINS = new Set([
10
+ "https://chat.openai.com",
11
+ "https://chatgpt.com",
12
+ "https://claude.ai",
13
+ ]);
8
14
 
9
15
  export function isLocalMcpEmbedOrigin(
10
16
  origin: string | null | undefined,
@@ -50,6 +56,37 @@ export function isChatGptMcpSandboxOrigin(
50
56
  }
51
57
  }
52
58
 
59
+ export function isMcpProductHostOrigin(
60
+ origin: string | null | undefined,
61
+ ): boolean {
62
+ if (!origin) return false;
63
+ try {
64
+ return MCP_PRODUCT_HOST_ORIGINS.has(new URL(origin).origin);
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ export function isAgentNativeFirstPartyAppOrigin(
71
+ origin: string | null | undefined,
72
+ ): boolean {
73
+ if (!origin) return false;
74
+ try {
75
+ const url = new URL(origin);
76
+ const hostname = url.hostname.toLowerCase();
77
+ return (
78
+ url.protocol === "https:" &&
79
+ !url.username &&
80
+ !url.password &&
81
+ !url.port &&
82
+ hostname.endsWith(AGENT_NATIVE_FIRST_PARTY_APP_HOST_SUFFIX) &&
83
+ hostname.length > AGENT_NATIVE_FIRST_PARTY_APP_HOST_SUFFIX.length
84
+ );
85
+ } catch {
86
+ return false;
87
+ }
88
+ }
89
+
53
90
  export function isMcpEmbedCorsOrigin(
54
91
  origin: string | null | undefined,
55
92
  ): boolean {
@@ -57,7 +94,9 @@ export function isMcpEmbedCorsOrigin(
57
94
  origin === "null" ||
58
95
  isLocalMcpEmbedOrigin(origin) ||
59
96
  isClaudeMcpContentOrigin(origin) ||
60
- isChatGptMcpSandboxOrigin(origin)
97
+ isChatGptMcpSandboxOrigin(origin) ||
98
+ isMcpProductHostOrigin(origin) ||
99
+ isAgentNativeFirstPartyAppOrigin(origin)
61
100
  );
62
101
  }
63
102
 
@@ -67,7 +106,9 @@ export function shouldAllowMcpEmbedCredentials(
67
106
  return (
68
107
  origin !== "null" &&
69
108
  !isClaudeMcpContentOrigin(origin) &&
70
- !isChatGptMcpSandboxOrigin(origin)
109
+ !isChatGptMcpSandboxOrigin(origin) &&
110
+ !isMcpProductHostOrigin(origin) &&
111
+ !isAgentNativeFirstPartyAppOrigin(origin)
71
112
  );
72
113
  }
73
114