@agent-native/core 0.72.3 → 0.72.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +10 -0
- package/corpus/core/package.json +6 -6
- package/corpus/core/src/a2a/client.ts +7 -3
- package/corpus/core/src/client/api-path.ts +28 -0
- package/corpus/core/src/client/embed-auth.ts +26 -0
- package/corpus/core/src/client/mcp-app-host.ts +6 -0
- package/corpus/core/src/client/mcp-apps/McpAppRenderer.tsx +227 -33
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/settings/DemoModeSection.tsx +3 -1
- package/corpus/core/src/demo/fetch-interceptor.ts +1 -1
- package/corpus/core/src/embedding/agent.ts +6 -1
- package/corpus/core/src/embedding/bridge.ts +34 -1
- package/corpus/core/src/mcp/build-server.ts +38 -10
- package/corpus/core/src/mcp/embed-app.ts +120 -45
- package/corpus/core/src/mcp/index.ts +5 -0
- package/corpus/core/src/mcp/org-directory.ts +70 -11
- package/corpus/core/src/mcp/server.ts +14 -0
- package/corpus/core/src/mcp-client/config.ts +12 -0
- package/corpus/core/src/mcp-client/index.ts +2 -0
- package/corpus/core/src/mcp-client/manager.ts +268 -9
- package/corpus/core/src/mcp-client/remote-store.ts +154 -0
- package/corpus/core/src/mcp-client/routes.ts +2 -0
- package/corpus/core/src/server/agent-chat-plugin.ts +8 -0
- package/corpus/core/src/server/index.ts +1 -0
- package/corpus/core/src/shared/mcp-embed-headers.ts +44 -3
- package/corpus/core/src/vite/client.ts +53 -0
- package/corpus/templates/assets/.agents/skills/a2a-assets/SKILL.md +17 -10
- package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +15 -6
- package/corpus/templates/assets/AGENTS.md +7 -5
- package/corpus/templates/assets/actions/generate-asset.ts +161 -0
- package/corpus/templates/assets/actions/list-assets.ts +4 -2
- package/corpus/templates/assets/actions/open-asset-picker.ts +84 -3
- package/corpus/templates/assets/app/routes/library.tsx +243 -40
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +9 -0
- package/corpus/templates/design/AGENTS.md +10 -5
- package/corpus/templates/design/actions/connect-assets-mcp.ts +131 -0
- package/corpus/templates/design/actions/insert-asset.ts +219 -0
- package/corpus/templates/design/actions/view-screen.ts +3 -1
- package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignEditor.tsx +53 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +5 -1
- package/corpus/templates/design/server/plugins/assets-mcp.ts +60 -0
- package/dist/a2a/client.d.ts +1 -0
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +5 -3
- package/dist/a2a/client.js.map +1 -1
- package/dist/client/api-path.d.ts.map +1 -1
- package/dist/client/api-path.js +27 -0
- package/dist/client/api-path.js.map +1 -1
- package/dist/client/embed-auth.d.ts.map +1 -1
- package/dist/client/embed-auth.js +27 -0
- package/dist/client/embed-auth.js.map +1 -1
- package/dist/client/mcp-app-host.d.ts.map +1 -1
- package/dist/client/mcp-app-host.js +5 -0
- package/dist/client/mcp-app-host.js.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.d.ts +1 -0
- package/dist/client/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/client/mcp-apps/McpAppRenderer.js +179 -33
- package/dist/client/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/settings/DemoModeSection.d.ts.map +1 -1
- package/dist/client/settings/DemoModeSection.js +3 -1
- package/dist/client/settings/DemoModeSection.js.map +1 -1
- package/dist/demo/fetch-interceptor.js +1 -1
- package/dist/demo/fetch-interceptor.js.map +1 -1
- package/dist/embedding/agent.d.ts +1 -1
- package/dist/embedding/agent.d.ts.map +1 -1
- package/dist/embedding/agent.js +1 -1
- package/dist/embedding/agent.js.map +1 -1
- package/dist/embedding/bridge.d.ts.map +1 -1
- package/dist/embedding/bridge.js +27 -1
- package/dist/embedding/bridge.js.map +1 -1
- package/dist/extensions/schema.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +32 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/mcp/embed-app.d.ts.map +1 -1
- package/dist/mcp/embed-app.js +120 -45
- package/dist/mcp/embed-app.js.map +1 -1
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/org-directory.d.ts +1 -0
- package/dist/mcp/org-directory.d.ts.map +1 -1
- package/dist/mcp/org-directory.js +59 -11
- package/dist/mcp/org-directory.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +9 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp-client/config.d.ts +12 -0
- package/dist/mcp-client/config.d.ts.map +1 -1
- package/dist/mcp-client/config.js.map +1 -1
- package/dist/mcp-client/index.d.ts +1 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +1 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/manager.d.ts +2 -0
- package/dist/mcp-client/manager.d.ts.map +1 -1
- package/dist/mcp-client/manager.js +201 -9
- package/dist/mcp-client/manager.js.map +1 -1
- package/dist/mcp-client/remote-store.d.ts +27 -0
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +105 -0
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +1 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/provider-api/actions/github-repo-files.d.ts +2 -2
- package/dist/provider-api/actions/query-staged-dataset.d.ts +3 -3
- package/dist/provider-api/corpus-jobs.d.ts +8 -8
- package/dist/server/agent-chat-plugin.d.ts +2 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +8 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/shared/mcp-embed-headers.d.ts +3 -1
- package/dist/shared/mcp-embed-headers.d.ts.map +1 -1
- package/dist/shared/mcp-embed-headers.js +40 -3
- package/dist/shared/mcp-embed-headers.js.map +1 -1
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/sharing/actions/set-resource-visibility.d.ts +2 -2
- package/dist/sharing/actions/share-resource.d.ts +1 -1
- package/dist/sharing/schema.d.ts +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +44 -0
- package/dist/vite/client.js.map +1 -1
- package/package.json +6 -6
|
@@ -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
|
-
|
|
339
|
-
|
|
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(
|
|
342
|
-
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
|
@@ -954,6 +954,52 @@ const VITE_RUNTIME_PATH_PREFIXES = [
|
|
|
954
954
|
"/src/",
|
|
955
955
|
];
|
|
956
956
|
|
|
957
|
+
const EMBED_DEV_STATIC_ASSET_PATH_PREFIXES = [
|
|
958
|
+
...VITE_RUNTIME_PATH_PREFIXES,
|
|
959
|
+
"/assets/",
|
|
960
|
+
"/library-presets/",
|
|
961
|
+
];
|
|
962
|
+
|
|
963
|
+
const EMBED_DEV_STATIC_ASSET_PATHS = new Set([
|
|
964
|
+
"/favicon.ico",
|
|
965
|
+
"/favicon.svg",
|
|
966
|
+
"/manifest.json",
|
|
967
|
+
]);
|
|
968
|
+
|
|
969
|
+
function mountedPathCandidates(
|
|
970
|
+
reqUrl: string | undefined,
|
|
971
|
+
base: string | undefined,
|
|
972
|
+
): string[] {
|
|
973
|
+
if (!reqUrl) return [];
|
|
974
|
+
let pathname: string;
|
|
975
|
+
try {
|
|
976
|
+
pathname = new URL(reqUrl, "http://agent-native.local").pathname;
|
|
977
|
+
} catch {
|
|
978
|
+
return [];
|
|
979
|
+
}
|
|
980
|
+
if (base && base !== "/") {
|
|
981
|
+
const normalizedBase = base.endsWith("/") ? base : `${base}/`;
|
|
982
|
+
if (pathname.startsWith(normalizedBase)) {
|
|
983
|
+
return [pathname.slice(normalizedBase.length - 1) || "/"];
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return [pathname];
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function isEmbedDevStaticAssetRequest(
|
|
990
|
+
reqUrl: string | undefined,
|
|
991
|
+
base: string | undefined,
|
|
992
|
+
): boolean {
|
|
993
|
+
return mountedPathCandidates(reqUrl, base).some((pathname) => {
|
|
994
|
+
if (EMBED_DEV_STATIC_ASSET_PATHS.has(pathname)) return true;
|
|
995
|
+
if (/^\/icon-[^/]+\.svg$/i.test(pathname)) return true;
|
|
996
|
+
if (/^\/agent-native-[^/]+\.svg$/i.test(pathname)) return true;
|
|
997
|
+
return EMBED_DEV_STATIC_ASSET_PATH_PREFIXES.some((prefix) =>
|
|
998
|
+
pathname.startsWith(prefix),
|
|
999
|
+
);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
957
1003
|
function cookieValue(req: IncomingMessage, name: string): string | undefined {
|
|
958
1004
|
const header = req.headers.cookie;
|
|
959
1005
|
if (typeof header !== "string" || !header) return undefined;
|
|
@@ -1166,6 +1212,13 @@ function embedDevFrameHeaders(): Plugin {
|
|
|
1166
1212
|
configureServer(server) {
|
|
1167
1213
|
server.middlewares.use((req, res, next) => {
|
|
1168
1214
|
const origin = String(req.headers.origin ?? "");
|
|
1215
|
+
if (isEmbedDevStaticAssetRequest(req.url, server.config?.base)) {
|
|
1216
|
+
for (const [name, value] of Object.entries(
|
|
1217
|
+
MCP_EMBED_STATIC_ASSET_HEADERS,
|
|
1218
|
+
)) {
|
|
1219
|
+
res.setHeader(name, value);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1169
1222
|
if (isMcpEmbedCorsOrigin(origin)) {
|
|
1170
1223
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
1171
1224
|
res.setHeader("Vary", "Origin");
|