@agent-native/core 0.206.0-nightly-20261009153146 → 0.206.0
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/dist/action.d.ts +7 -1
- package/dist/client/agent-chat/index.d.ts +1 -1
- package/dist/client/agent-chat/index.js +1 -1
- package/dist/client/embed-auth.d.ts +2 -0
- package/dist/client/embed-auth.js +160 -12
- package/dist/client/host/index.d.ts +1 -1
- package/dist/client/host/index.js +1 -1
- package/dist/client/mcp-app-host.d.ts +1 -0
- package/dist/client/mcp-app-host.js +4 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/mcp/build-server.d.ts +15 -0
- package/dist/mcp/build-server.js +307 -91
- package/dist/mcp/connect-route.js +1 -0
- package/dist/mcp/embed-app.js +291 -31
- package/dist/mcp/oauth-route.js +3 -0
- package/dist/mcp/oauth-store.d.ts +2 -1
- package/dist/mcp/oauth-store.js +5 -6
- package/dist/mcp/oauth-token.d.ts +5 -0
- package/dist/mcp/oauth-token.js +17 -0
- package/dist/mcp/screen-memory-stdio.d.ts +7 -7
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/workspace-app-access-request.d.ts +8 -0
- package/dist/org/workspace-app-access-request.js +20 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +4 -4
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +11 -11
- package/dist/server/action-routes.d.ts +2 -0
- package/dist/server/action-routes.js +187 -31
- package/dist/server/agent-chat/mcp-options.d.ts +3 -1
- package/dist/server/agent-chat-plugin.js +26 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/auth.js +11 -20
- package/dist/server/embed-route.js +5 -3
- package/dist/server/embed-session.d.ts +30 -1
- package/dist/server/embed-session.js +376 -30
- package/dist/server/realtime-token.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.js +25 -1
- package/dist/shared/embed-auth.d.ts +74 -0
- package/dist/shared/embed-auth.js +379 -10
- package/dist/sharing/actions/list-resource-shares.d.ts +1 -1
- package/dist/triggers/actions/manage-automation.d.ts +3 -3
- package/package.json +4 -4
package/dist/mcp/build-server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { buildDeepLink, isAgentNativeOpenUrl, isSameOriginUrl, toAbsoluteOpenUrl
|
|
|
16
16
|
import { getRequestContext, getRequestOrgId, getRequestUserEmail, runWithRequestContext, } from "../server/request-context.js";
|
|
17
17
|
import { agentNativeMcpInstructions, agentNativeToolTitle, } from "../shared/agent-mcp-metadata.js";
|
|
18
18
|
import { withCollapsedAgentSidebarParam } from "../shared/agent-sidebar-url.js";
|
|
19
|
-
import { createMcpDirectoryWidgetReadCapability, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, } from "../shared/embed-auth.js";
|
|
19
|
+
import { createMcpDirectoryWidgetReadCapability, createMcpDirectoryWidgetWriteCapability, MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS, MCP_APP_CHAT_BRIDGE_QUERY_PARAM, renewMcpDirectoryWidgetCapabilityScope, } from "../shared/embed-auth.js";
|
|
20
20
|
import { describeMcpError, readClientInfoFromRequest, trackMcpResourceRead, trackMcpResourcesList, trackMcpToolCall, trackMcpToolsList, } from "./analytics.js";
|
|
21
21
|
import { consumeMcpApprovalGrant, createMcpApprovalGrant, } from "./approval-store.js";
|
|
22
22
|
import { getBuiltinCrossAppTools } from "./builtin-tools.js";
|
|
@@ -199,6 +199,13 @@ export function selectMcpDirectoryWidgetReadActions(profile, actions) {
|
|
|
199
199
|
.map((name) => [name, actions[name]])
|
|
200
200
|
.filter((entry) => Boolean(entry[1])));
|
|
201
201
|
}
|
|
202
|
+
export function selectMcpDirectoryWidgetWriteActions(profile, actions) {
|
|
203
|
+
if (!profile)
|
|
204
|
+
return undefined;
|
|
205
|
+
return Object.fromEntries(Object.keys(profile.widgetWriteActionArguments ?? {})
|
|
206
|
+
.map((name) => [name, actions[name]])
|
|
207
|
+
.filter((entry) => Boolean(entry[1])));
|
|
208
|
+
}
|
|
202
209
|
export function getConfiguredMcpOwnerEmail() {
|
|
203
210
|
return getAppConfig().auth.mcpOwnerEmail;
|
|
204
211
|
}
|
|
@@ -349,6 +356,45 @@ export function validateMcpDirectoryProfile(config, sourceActions = config.produ
|
|
|
349
356
|
throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget read route "${name}" must be an explicitly scoped read-only GET action with valid resource arguments in the connector allowlist or hidden-read profiles.`);
|
|
350
357
|
}
|
|
351
358
|
}
|
|
359
|
+
for (const [name, argumentMap] of Object.entries(profile?.widgetWriteActionArguments ?? {})) {
|
|
360
|
+
const entry = actions[name] ?? config.widgetWriteActions?.[name];
|
|
361
|
+
if (!entry ||
|
|
362
|
+
entry.http === false ||
|
|
363
|
+
entry.http?.method === "GET" ||
|
|
364
|
+
entry.readOnly === true ||
|
|
365
|
+
entry.requiresAuth === false ||
|
|
366
|
+
!entry.schema ||
|
|
367
|
+
typeof entry.schema !== "object" ||
|
|
368
|
+
!("~standard" in entry.schema) ||
|
|
369
|
+
Object.keys(argumentMap).length === 0 ||
|
|
370
|
+
!Object.values(argumentMap).some((rule) => typeof rule === "string" ||
|
|
371
|
+
(rule?.type === "actionSchemaResourceBound" &&
|
|
372
|
+
MCP_DIRECTORY_WIDGET_ARGUMENT.test(rule.resourceKey))) ||
|
|
373
|
+
Object.entries(argumentMap).some(([argumentName, argument]) => !MCP_DIRECTORY_WIDGET_ARGUMENT.test(argumentName) ||
|
|
374
|
+
(typeof argument === "string"
|
|
375
|
+
? !MCP_DIRECTORY_WIDGET_ARGUMENT.test(argument)
|
|
376
|
+
: argument?.type === "actionSchema"
|
|
377
|
+
? Object.keys(argument).length !== 1 ||
|
|
378
|
+
!entry.schema ||
|
|
379
|
+
typeof entry.schema !== "object" ||
|
|
380
|
+
!("~standard" in entry.schema)
|
|
381
|
+
: argument?.type === "actionSchemaResourceBound"
|
|
382
|
+
? Object.keys(argument).length !== 2 ||
|
|
383
|
+
!MCP_DIRECTORY_WIDGET_ARGUMENT.test(argument.resourceKey) ||
|
|
384
|
+
!entry.schema ||
|
|
385
|
+
typeof entry.schema !== "object" ||
|
|
386
|
+
!("~standard" in entry.schema)
|
|
387
|
+
: !argument ||
|
|
388
|
+
argument.type !== "integerRange" ||
|
|
389
|
+
Object.keys(argument).length !== 3 ||
|
|
390
|
+
!Number.isSafeInteger(argument.min) ||
|
|
391
|
+
!Number.isSafeInteger(argument.max) ||
|
|
392
|
+
argument.min < 0 ||
|
|
393
|
+
argument.max < argument.min ||
|
|
394
|
+
argument.max > 5_000))) {
|
|
395
|
+
throw new McpDirectoryProfileValidationError(`[agent-native] MCP directory widget write action "${name}" must be an authenticated mutation action with an exact resource ID binding and valid argument allowlist.`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
352
398
|
}
|
|
353
399
|
export function validateMcpDirectoryWidgetDomain(domain) {
|
|
354
400
|
let validWidgetDomain = false;
|
|
@@ -746,36 +792,59 @@ function mcpAppEmbedOpenLinkMeta(result, resource, meta) {
|
|
|
746
792
|
: {}),
|
|
747
793
|
};
|
|
748
794
|
}
|
|
795
|
+
function mcpDirectoryWidgetSourceMeta(toolName, embedMeta) {
|
|
796
|
+
const embedStart = metadataObject(embedMeta["agent-native/embedStart"]);
|
|
797
|
+
if (typeof embedStart.startUrl !== "string") {
|
|
798
|
+
return { "agent-native/widgetSource": { toolName } };
|
|
799
|
+
}
|
|
800
|
+
try {
|
|
801
|
+
const url = new URL(embedStart.startUrl, "https://agent-native.invalid");
|
|
802
|
+
const sourceTicket = url.pathname.includes("/_agent-native/embed/start")
|
|
803
|
+
? url.searchParams.get("ticket")
|
|
804
|
+
: null;
|
|
805
|
+
return {
|
|
806
|
+
"agent-native/widgetSource": {
|
|
807
|
+
toolName,
|
|
808
|
+
...(sourceTicket ? { sourceTicket } : {}),
|
|
809
|
+
},
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
return { "agent-native/widgetSource": { toolName } };
|
|
814
|
+
}
|
|
815
|
+
}
|
|
749
816
|
async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl, directoryWidget) {
|
|
750
817
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
751
818
|
return result;
|
|
752
819
|
}
|
|
753
820
|
const out = result;
|
|
754
|
-
const
|
|
821
|
+
const restrictDirectoryWidgetCapability = directoryWidget !== undefined;
|
|
755
822
|
const resultWithoutExistingEmbedTicket = { ...out };
|
|
756
|
-
if (
|
|
823
|
+
if (restrictDirectoryWidgetCapability) {
|
|
757
824
|
delete resultWithoutExistingEmbedTicket.embedStartUrl;
|
|
758
825
|
delete resultWithoutExistingEmbedTicket.embedTargetPath;
|
|
759
826
|
delete resultWithoutExistingEmbedTicket.embedExpiresAt;
|
|
760
827
|
}
|
|
761
828
|
if (out.embed === false ||
|
|
762
|
-
(!
|
|
763
|
-
|
|
829
|
+
(!restrictDirectoryWidgetCapability &&
|
|
830
|
+
out.embed !== true &&
|
|
831
|
+
!directoryLinkUrl)) {
|
|
832
|
+
return restrictDirectoryWidgetCapability
|
|
764
833
|
? resultWithoutExistingEmbedTicket
|
|
765
834
|
: result;
|
|
766
835
|
}
|
|
767
|
-
if (!
|
|
836
|
+
if (!restrictDirectoryWidgetCapability &&
|
|
768
837
|
typeof out.embedStartUrl === "string" &&
|
|
769
838
|
out.embedStartUrl.trim()) {
|
|
770
839
|
return result;
|
|
771
840
|
}
|
|
772
|
-
if (!
|
|
841
|
+
if (!restrictDirectoryWidgetCapability &&
|
|
773
842
|
typeof out.url === "string" &&
|
|
774
843
|
out.url.trim() &&
|
|
775
844
|
isEmbedStartUrl(out.url)) {
|
|
776
845
|
return result;
|
|
777
846
|
}
|
|
778
|
-
const candidates =
|
|
847
|
+
const candidates = restrictDirectoryWidgetCapability
|
|
779
848
|
? [directoryWidget.targetPath]
|
|
780
849
|
: out.embed === true
|
|
781
850
|
? [out.url, out.path, out.deepLinkUrl, directoryLinkUrl]
|
|
@@ -783,10 +852,10 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
|
|
|
783
852
|
const candidate = candidates.find((value) => {
|
|
784
853
|
if (typeof value !== "string" || value.trim().length === 0)
|
|
785
854
|
return false;
|
|
786
|
-
return !
|
|
855
|
+
return !restrictDirectoryWidgetCapability || !isEmbedStartUrl(value);
|
|
787
856
|
});
|
|
788
857
|
if (!candidate) {
|
|
789
|
-
return
|
|
858
|
+
return restrictDirectoryWidgetCapability
|
|
790
859
|
? resultWithoutExistingEmbedTicket
|
|
791
860
|
: result;
|
|
792
861
|
}
|
|
@@ -794,19 +863,19 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
|
|
|
794
863
|
const isPath = trimmed.startsWith("/") && !trimmed.startsWith("//");
|
|
795
864
|
const isAbsoluteHttp = /^https?:\/\//i.test(trimmed);
|
|
796
865
|
if (!isPath && !isAbsoluteHttp) {
|
|
797
|
-
return
|
|
866
|
+
return restrictDirectoryWidgetCapability
|
|
798
867
|
? resultWithoutExistingEmbedTicket
|
|
799
868
|
: result;
|
|
800
869
|
}
|
|
801
870
|
if (isAbsoluteHttp && !meta?.origin) {
|
|
802
|
-
return
|
|
871
|
+
return restrictDirectoryWidgetCapability
|
|
803
872
|
? resultWithoutExistingEmbedTicket
|
|
804
873
|
: result;
|
|
805
874
|
}
|
|
806
875
|
const ctx = getRequestContext();
|
|
807
876
|
const ownerEmail = ctx?.userEmail?.trim();
|
|
808
877
|
if (!ownerEmail) {
|
|
809
|
-
return
|
|
878
|
+
return restrictDirectoryWidgetCapability
|
|
810
879
|
? resultWithoutExistingEmbedTicket
|
|
811
880
|
: result;
|
|
812
881
|
}
|
|
@@ -814,30 +883,56 @@ async function withServerMintedMcpAppEmbedStart(result, meta, directoryLinkUrl,
|
|
|
814
883
|
const { buildEmbedStartPath } = await import("../server/embed-route.js");
|
|
815
884
|
const targetPath = normalizeEmbedTargetPath(withMcpChatBridgeParam(trimmed), meta?.origin);
|
|
816
885
|
if (!targetPath) {
|
|
817
|
-
return
|
|
886
|
+
return restrictDirectoryWidgetCapability
|
|
818
887
|
? resultWithoutExistingEmbedTicket
|
|
819
888
|
: result;
|
|
820
889
|
}
|
|
821
|
-
|
|
822
|
-
|
|
890
|
+
if (directoryWidget?.capability.mode === "write" &&
|
|
891
|
+
(directoryWidget.capability.value.userEmail.toLowerCase() !==
|
|
892
|
+
ownerEmail.toLowerCase() ||
|
|
893
|
+
(directoryWidget.capability.value.orgId ?? undefined) !==
|
|
894
|
+
(ctx?.orgId ?? undefined))) {
|
|
895
|
+
throw new Error("The widget write grant must match the authenticated MCP user and organization.");
|
|
896
|
+
}
|
|
897
|
+
const scope = restrictDirectoryWidgetCapability
|
|
898
|
+
? directoryWidget.capability.mode === "write"
|
|
899
|
+
? createMcpDirectoryWidgetWriteCapability(directoryWidget.capability.value)
|
|
900
|
+
: createMcpDirectoryWidgetReadCapability(directoryWidget.capability.value)
|
|
823
901
|
: typeof out.chrome === "string"
|
|
824
902
|
? out.chrome
|
|
825
903
|
: null;
|
|
826
|
-
if (
|
|
827
|
-
throw new Error("Could not create a valid
|
|
904
|
+
if (restrictDirectoryWidgetCapability && !scope) {
|
|
905
|
+
throw new Error("Could not create a valid scoped capability for this MCP directory widget.");
|
|
906
|
+
}
|
|
907
|
+
const revocationAnchorCandidate = ctx?.mcpCredentialIssuedAtMs;
|
|
908
|
+
const revocationAnchorCreatedAtMs = typeof revocationAnchorCandidate === "number" &&
|
|
909
|
+
Number.isSafeInteger(revocationAnchorCandidate)
|
|
910
|
+
? revocationAnchorCandidate
|
|
911
|
+
: undefined;
|
|
912
|
+
if (restrictDirectoryWidgetCapability &&
|
|
913
|
+
revocationAnchorCreatedAtMs === undefined) {
|
|
914
|
+
throw new Error("Directory widget sessions require a trusted MCP credential issue time.");
|
|
828
915
|
}
|
|
829
916
|
const ticket = await createEmbedSessionTicket({
|
|
830
917
|
ownerEmail,
|
|
831
918
|
orgId: ctx?.orgId,
|
|
832
919
|
targetPath,
|
|
833
920
|
scope,
|
|
921
|
+
...(directoryWidget?.capability.mode === "write"
|
|
922
|
+
? { ttlSeconds: 5 * 60 }
|
|
923
|
+
: {}),
|
|
924
|
+
...(restrictDirectoryWidgetCapability
|
|
925
|
+
? { revocationAnchorCreatedAtMs }
|
|
926
|
+
: {}),
|
|
834
927
|
});
|
|
835
928
|
const startPath = buildEmbedStartPath(ticket.ticket);
|
|
836
929
|
const embedStartUrl = meta?.origin
|
|
837
930
|
? new URL(startPath, meta.origin).toString()
|
|
838
931
|
: startPath;
|
|
839
932
|
return {
|
|
840
|
-
...(
|
|
933
|
+
...(restrictDirectoryWidgetCapability
|
|
934
|
+
? resultWithoutExistingEmbedTicket
|
|
935
|
+
: out),
|
|
841
936
|
embedStartUrl,
|
|
842
937
|
embedTargetPath: targetPath,
|
|
843
938
|
embedExpiresAt: ticket.expiresAt,
|
|
@@ -853,7 +948,7 @@ function withoutMcpAppEmbedTicket(result) {
|
|
|
853
948
|
delete output.embedExpiresAt;
|
|
854
949
|
return output;
|
|
855
950
|
}
|
|
856
|
-
function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, result, actions,
|
|
951
|
+
async function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, result, actions, identity) {
|
|
857
952
|
const profile = config.directoryProfile;
|
|
858
953
|
const resolveTarget = profile?.widgetTargets?.[toolName];
|
|
859
954
|
if (!resolveTarget)
|
|
@@ -877,7 +972,7 @@ function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, r
|
|
|
877
972
|
!scopedAuthenticatedRead) ||
|
|
878
973
|
(entry.readOnly !== true &&
|
|
879
974
|
!profile.widgetReadOnlyActions?.includes(actionName)) ||
|
|
880
|
-
!isActionVisibleForOAuthScope(entry, oauthScopes, profile.widgetReadOnlyActions?.includes(actionName) === true) ||
|
|
975
|
+
!isActionVisibleForOAuthScope(entry, identity?.oauthScopes, profile.widgetReadOnlyActions?.includes(actionName) === true) ||
|
|
881
976
|
entry.http === false ||
|
|
882
977
|
entry.http?.method !== "GET" ||
|
|
883
978
|
(entry.requiresAuth === false && !scopedPublicRead)) {
|
|
@@ -900,15 +995,100 @@ function mcpDirectoryWidgetCapabilityForTool(config, resource, toolName, args, r
|
|
|
900
995
|
actionArguments[actionName] = scopedArguments;
|
|
901
996
|
}
|
|
902
997
|
}
|
|
903
|
-
|
|
998
|
+
const writeActionArguments = {};
|
|
999
|
+
const targetWriteActions = new Set(target.writeActions ?? []);
|
|
1000
|
+
for (const actionName of targetWriteActions) {
|
|
1001
|
+
if (!Object.hasOwn(profile.widgetWriteActionArguments ?? {}, actionName)) {
|
|
1002
|
+
throw new Error(`MCP directory widget target references undeclared write action "${actionName}".`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
for (const [actionName, argumentMap] of Object.entries(profile.widgetWriteActionArguments ?? {})) {
|
|
1006
|
+
if (!targetWriteActions.has(actionName))
|
|
1007
|
+
continue;
|
|
1008
|
+
const entry = actions[actionName] ?? config.widgetWriteActions?.[actionName];
|
|
1009
|
+
if (!entry ||
|
|
1010
|
+
entry.http === false ||
|
|
1011
|
+
entry.http?.method === "GET" ||
|
|
1012
|
+
entry.readOnly === true ||
|
|
1013
|
+
entry.requiresAuth === false ||
|
|
1014
|
+
!isActionVisibleForOAuthScope(entry, identity?.oauthScopes)) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const scopedArguments = {};
|
|
1018
|
+
for (const [argumentName, rule] of Object.entries(argumentMap)) {
|
|
1019
|
+
if (typeof rule === "string") {
|
|
1020
|
+
const resourceId = target.resourceIds[rule];
|
|
1021
|
+
if (typeof resourceId !== "string" || !resourceId.trim())
|
|
1022
|
+
break;
|
|
1023
|
+
scopedArguments[argumentName] = resourceId;
|
|
1024
|
+
}
|
|
1025
|
+
else {
|
|
1026
|
+
scopedArguments[argumentName] = rule;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (Object.keys(scopedArguments).length === Object.keys(argumentMap).length &&
|
|
1030
|
+
Object.values(scopedArguments).some((argument) => {
|
|
1031
|
+
if (typeof argument === "string") {
|
|
1032
|
+
return Object.values(target.resourceIds).includes(argument);
|
|
1033
|
+
}
|
|
1034
|
+
if (argument.type !== "actionSchemaResourceBound")
|
|
1035
|
+
return false;
|
|
1036
|
+
const resourceId = target.resourceIds[argument.resourceKey];
|
|
1037
|
+
return typeof resourceId === "string" && resourceId.trim().length > 0;
|
|
1038
|
+
})) {
|
|
1039
|
+
writeActionArguments[actionName] = scopedArguments;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
if (Object.keys(actionArguments).length === 0 &&
|
|
1043
|
+
Object.keys(writeActionArguments).length === 0) {
|
|
904
1044
|
return undefined;
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1045
|
+
}
|
|
1046
|
+
let editorAccessAllowed = false;
|
|
1047
|
+
if (Object.keys(writeActionArguments).length > 0 &&
|
|
1048
|
+
hasVerifiedMcpUserIdentity(identity) &&
|
|
1049
|
+
profile.authorizeWidgetWrite) {
|
|
1050
|
+
try {
|
|
1051
|
+
editorAccessAllowed = await profile.authorizeWidgetWrite({
|
|
1052
|
+
toolName,
|
|
1053
|
+
args,
|
|
1054
|
+
result,
|
|
1055
|
+
target,
|
|
1056
|
+
identity,
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
catch {
|
|
1060
|
+
console.error(`[mcp] Directory widget editor authorization failed for ${toolName}; issuing a read-only capability.`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (Object.keys(writeActionArguments).length > 0 &&
|
|
1064
|
+
hasVerifiedMcpUserIdentity(identity) &&
|
|
1065
|
+
editorAccessAllowed) {
|
|
1066
|
+
const requestOrgId = getRequestContext()?.orgId;
|
|
1067
|
+
const value = {
|
|
908
1068
|
appId: config.appId ?? config.name,
|
|
909
1069
|
resourceUri: resource.uri,
|
|
910
1070
|
resourceIds: target.resourceIds,
|
|
911
|
-
|
|
1071
|
+
userEmail: identity.userEmail,
|
|
1072
|
+
...(typeof requestOrgId === "string" ? { orgId: requestOrgId } : {}),
|
|
1073
|
+
expiresAtMs: Date.now() + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS,
|
|
1074
|
+
readActionArguments: actionArguments,
|
|
1075
|
+
writeActionArguments,
|
|
1076
|
+
};
|
|
1077
|
+
return {
|
|
1078
|
+
targetPath: target.targetPath,
|
|
1079
|
+
capability: { mode: "write", value },
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
targetPath: target.targetPath,
|
|
1084
|
+
capability: {
|
|
1085
|
+
mode: "read",
|
|
1086
|
+
value: {
|
|
1087
|
+
appId: config.appId ?? config.name,
|
|
1088
|
+
resourceUri: resource.uri,
|
|
1089
|
+
resourceIds: target.resourceIds,
|
|
1090
|
+
actionArguments,
|
|
1091
|
+
},
|
|
912
1092
|
},
|
|
913
1093
|
};
|
|
914
1094
|
}
|
|
@@ -923,21 +1103,25 @@ function mcpDirectoryWidgetSessionTool(config) {
|
|
|
923
1103
|
}
|
|
924
1104
|
return {
|
|
925
1105
|
name: "create_embed_session",
|
|
926
|
-
description: "
|
|
1106
|
+
description: "Creates a short-lived session for the same app widget using its original server-issued result ticket.",
|
|
927
1107
|
inputSchema: {
|
|
928
1108
|
type: "object",
|
|
929
1109
|
properties: {
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1110
|
+
sourceTicket: {
|
|
1111
|
+
type: "string",
|
|
1112
|
+
description: "Original server-issued widget ticket from this result.",
|
|
1113
|
+
},
|
|
1114
|
+
renewInPlace: {
|
|
1115
|
+
type: "boolean",
|
|
1116
|
+
description: "Extend the active scoped widget session without replacing its mounted app frame.",
|
|
1117
|
+
},
|
|
934
1118
|
},
|
|
935
|
-
required: ["
|
|
1119
|
+
required: ["sourceTicket"],
|
|
936
1120
|
},
|
|
937
1121
|
_meta: { ui: { visibility: ["app"] } },
|
|
938
1122
|
};
|
|
939
1123
|
}
|
|
940
|
-
async function renewMcpDirectoryWidgetEmbedSession(config,
|
|
1124
|
+
async function renewMcpDirectoryWidgetEmbedSession(config, args, identity) {
|
|
941
1125
|
const profile = config.directoryProfile;
|
|
942
1126
|
if (!profile || config.catalogMode !== "directory") {
|
|
943
1127
|
throw new Error("Directory widget session renewal is not enabled.");
|
|
@@ -945,66 +1129,76 @@ async function renewMcpDirectoryWidgetEmbedSession(config, actions, args, identi
|
|
|
945
1129
|
if (!hasVerifiedMcpUserIdentity(identity)) {
|
|
946
1130
|
throw new Error("Widget session renewal requires a verified user caller.");
|
|
947
1131
|
}
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
});
|
|
1132
|
+
const sourceTicket = args.sourceTicket;
|
|
1133
|
+
if (typeof sourceTicket !== "string") {
|
|
1134
|
+
throw new Error("Widget session renewal requires its original result ticket.");
|
|
1135
|
+
}
|
|
1136
|
+
const { createEmbedSessionTicket, readMcpDirectoryWidgetRenewalTicket, renewMcpDirectoryWidgetSession, } = await import("../server/embed-session.js");
|
|
1137
|
+
const originalTicket = await readMcpDirectoryWidgetRenewalTicket(sourceTicket);
|
|
1138
|
+
const callerOrgId = getRequestContext()?.orgId;
|
|
1139
|
+
if (!originalTicket ||
|
|
1140
|
+
originalTicket.ownerEmail.trim().toLowerCase() !==
|
|
1141
|
+
identity.userEmail.trim().toLowerCase() ||
|
|
1142
|
+
(originalTicket.orgId ?? undefined) !== (callerOrgId ?? undefined)) {
|
|
1143
|
+
throw new Error("The original widget ticket is not available to this caller.");
|
|
1144
|
+
}
|
|
1145
|
+
const appId = config.appId ?? config.name;
|
|
1146
|
+
const renewalNow = Date.now();
|
|
1147
|
+
const capabilityExpiresAtMs = Math.min(renewalNow + MCP_DIRECTORY_WIDGET_WRITE_CAPABILITY_MAX_AGE_MS, originalTicket.renewalExpiresAtMs);
|
|
1148
|
+
const scope = renewMcpDirectoryWidgetCapabilityScope(originalTicket.scope, {
|
|
1149
|
+
appId,
|
|
1150
|
+
resourceUri: getMcpDirectoryWidgetResourceUri(appId),
|
|
1151
|
+
userEmail: identity.userEmail,
|
|
1152
|
+
orgId: callerOrgId,
|
|
1153
|
+
expiresAtMs: capabilityExpiresAtMs,
|
|
1154
|
+
readAllowed: hasMcpOAuthScope(identity.oauthScopes, "mcp:read"),
|
|
1155
|
+
writeAllowed: hasMcpOAuthScope(identity.oauthScopes, "mcp:write"),
|
|
1156
|
+
});
|
|
1157
|
+
if (!scope) {
|
|
1158
|
+
throw new Error("The original widget ticket has an invalid app capability.");
|
|
1159
|
+
}
|
|
1160
|
+
if (args.renewInPlace === true) {
|
|
1161
|
+
if (originalTicket.consumedAtMs === null ||
|
|
1162
|
+
originalTicket.sessionActiveUntilMs === null) {
|
|
1163
|
+
throw new Error("The original widget session cannot be renewed in place.");
|
|
981
1164
|
}
|
|
1165
|
+
const expiresAt = await renewMcpDirectoryWidgetSession({
|
|
1166
|
+
sourceTicket,
|
|
1167
|
+
ownerEmail: identity.userEmail,
|
|
1168
|
+
orgId: callerOrgId,
|
|
1169
|
+
expectedScope: originalTicket.scope,
|
|
1170
|
+
renewedScope: scope,
|
|
1171
|
+
});
|
|
1172
|
+
if (!expiresAt) {
|
|
1173
|
+
throw new Error("The original widget session can no longer be renewed in place.");
|
|
1174
|
+
}
|
|
1175
|
+
return { renewed: true, expiresAt };
|
|
982
1176
|
}
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
]));
|
|
987
|
-
if (distinctCandidates.size !== 1) {
|
|
988
|
-
throw new Error("Could not resolve one authorized app resource from this saved widget result.");
|
|
1177
|
+
const appOrigin = profile.widgetDomain ?? config.widgetDomain;
|
|
1178
|
+
if (!appOrigin) {
|
|
1179
|
+
throw new Error("Directory widget session renewal requires an app origin.");
|
|
989
1180
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
throw new Error("Could not create a scoped app widget session.");
|
|
1181
|
+
let appOriginUrl;
|
|
1182
|
+
try {
|
|
1183
|
+
appOriginUrl = new URL(appOrigin);
|
|
994
1184
|
}
|
|
1185
|
+
catch {
|
|
1186
|
+
throw new Error("Directory widget session renewal has an invalid origin.");
|
|
1187
|
+
}
|
|
1188
|
+
const { buildEmbedStartPath } = await import("../server/embed-route.js");
|
|
1189
|
+
const targetPath = withMcpChatBridgeParam(originalTicket.targetPath);
|
|
995
1190
|
const ticket = await createEmbedSessionTicket({
|
|
996
|
-
ownerEmail: identity.userEmail
|
|
997
|
-
orgId:
|
|
1191
|
+
ownerEmail: identity.userEmail,
|
|
1192
|
+
orgId: callerOrgId,
|
|
998
1193
|
targetPath,
|
|
999
1194
|
scope,
|
|
1195
|
+
ttlSeconds: Math.max(1, Math.ceil((capabilityExpiresAtMs - renewalNow) / 1000)),
|
|
1196
|
+
renewalExpiresAtMs: originalTicket.renewalExpiresAtMs,
|
|
1197
|
+
revocationAnchorCreatedAtMs: originalTicket.createdAtMs,
|
|
1000
1198
|
});
|
|
1001
1199
|
const startPath = buildEmbedStartPath(ticket.ticket);
|
|
1002
|
-
const appOrigin = profile.widgetDomain ?? config.widgetDomain;
|
|
1003
|
-
if (!appOrigin) {
|
|
1004
|
-
throw new Error("Widget session renewal has no configured app origin.");
|
|
1005
|
-
}
|
|
1006
1200
|
return {
|
|
1007
|
-
startUrl: new URL(startPath,
|
|
1201
|
+
startUrl: new URL(startPath, appOriginUrl).toString(),
|
|
1008
1202
|
targetPath,
|
|
1009
1203
|
expiresAt: ticket.expiresAt,
|
|
1010
1204
|
};
|
|
@@ -1111,7 +1305,7 @@ function safeUiSegment(value, fallback) {
|
|
|
1111
1305
|
return normalized || fallback;
|
|
1112
1306
|
}
|
|
1113
1307
|
const MCP_APP_RESOURCE_SHELL_VERSION = "shell-v65";
|
|
1114
|
-
const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-
|
|
1308
|
+
const MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION = "shell-v69";
|
|
1115
1309
|
export function getMcpDirectoryWidgetResourceUri(appId, fallback = "agent-native") {
|
|
1116
1310
|
const app = safeUiSegment(appId, fallback);
|
|
1117
1311
|
return (versionMcpAppResourceUri(`ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`, MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION)?.uri ?? `ui://${app}/${MCP_DIRECTORY_APP_RESOURCE_SHELL_VERSION}`);
|
|
@@ -1775,6 +1969,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
1775
1969
|
: {}),
|
|
1776
1970
|
...(requestMeta?.origin ? { requestOrigin: requestMeta.origin } : {}),
|
|
1777
1971
|
...(mcpRequestId ? { mcpRequestId } : {}),
|
|
1972
|
+
mcpCredentialIssuedAtMs: effectiveIdentity?.mcpCredentialIssuedAtMs ?? null,
|
|
1778
1973
|
}, fn);
|
|
1779
1974
|
}
|
|
1780
1975
|
async function requireMcpActionApproval(entry, name, args, ctx) {
|
|
@@ -2148,7 +2343,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2148
2343
|
name === "create_embed_session" &&
|
|
2149
2344
|
mcpDirectoryWidgetSessionTool(config)) {
|
|
2150
2345
|
try {
|
|
2151
|
-
const renewed = await renewMcpDirectoryWidgetEmbedSession(config,
|
|
2346
|
+
const renewed = await renewMcpDirectoryWidgetEmbedSession(config, metadataObject(args), effectiveIdentity);
|
|
2152
2347
|
return {
|
|
2153
2348
|
content: [
|
|
2154
2349
|
{ type: "text", text: "Widget session ready." },
|
|
@@ -2234,8 +2429,12 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2234
2429
|
});
|
|
2235
2430
|
directoryLinkUrl = linked?.url ?? undefined;
|
|
2236
2431
|
}
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2432
|
+
const trustedCredentialIssuedAtMs = effectiveIdentity?.mcpCredentialIssuedAtMs;
|
|
2433
|
+
const directoryWidget = directoryCatalog &&
|
|
2434
|
+
mcpAppResourceCandidate &&
|
|
2435
|
+
typeof trustedCredentialIssuedAtMs === "number" &&
|
|
2436
|
+
Number.isSafeInteger(trustedCredentialIssuedAtMs)
|
|
2437
|
+
? await mcpDirectoryWidgetCapabilityForTool(config, mcpAppResourceCandidate, name, args ?? {}, projectedRawResult, visibleActions, effectiveIdentity)
|
|
2239
2438
|
: undefined;
|
|
2240
2439
|
const missingDirectoryWidgetCapability = directoryCatalog &&
|
|
2241
2440
|
config.directoryProfile !== undefined &&
|
|
@@ -2261,14 +2460,15 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
|
|
|
2261
2460
|
: null;
|
|
2262
2461
|
const embedProducedNothing = !!mcpAppResourceCandidate && !mcpResultIsError && !embedHasContent;
|
|
2263
2462
|
const { block, _meta } = buildLinkArtifacts(entry, args ?? {}, actionResultForClient, requestMeta);
|
|
2463
|
+
const mcpAppOpenLinkMeta = mcpAppResource
|
|
2464
|
+
? mcpAppEmbedOpenLinkMeta(actionResultForClient, mcpAppResource, requestMeta)
|
|
2465
|
+
: {};
|
|
2264
2466
|
const responseMeta = {
|
|
2265
2467
|
...(_meta ?? {}),
|
|
2266
2468
|
...(directoryCatalog && mcpAppResource
|
|
2267
|
-
?
|
|
2268
|
-
: {}),
|
|
2269
|
-
...(mcpAppResource
|
|
2270
|
-
? mcpAppEmbedOpenLinkMeta(actionResultForClient, mcpAppResource, requestMeta)
|
|
2469
|
+
? mcpDirectoryWidgetSourceMeta(name, mcpAppOpenLinkMeta)
|
|
2271
2470
|
: {}),
|
|
2471
|
+
...mcpAppOpenLinkMeta,
|
|
2272
2472
|
...(mcpAppResource
|
|
2273
2473
|
? openAiToolResultMeta(mcpAppResource, directoryCatalog)
|
|
2274
2474
|
: {}),
|
|
@@ -2759,6 +2959,9 @@ async function verifyIssuedMcpCredential(token, resourceUrl) {
|
|
|
2759
2959
|
oauthScopes: oauth.scopes,
|
|
2760
2960
|
oauthClientId: oauth.clientId,
|
|
2761
2961
|
...(oauth.catalogScope ? { catalogScope: oauth.catalogScope } : {}),
|
|
2962
|
+
...(oauth.grantCreatedAtMs !== undefined
|
|
2963
|
+
? { grantCreatedAtMs: oauth.grantCreatedAtMs }
|
|
2964
|
+
: {}),
|
|
2762
2965
|
issuedAt: oauth.issuedAt,
|
|
2763
2966
|
};
|
|
2764
2967
|
}
|
|
@@ -2793,11 +2996,24 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
|
|
|
2793
2996
|
stored = lookup;
|
|
2794
2997
|
}
|
|
2795
2998
|
const orgId = credential.orgId !== undefined ? credential.orgId : stored?.orgId;
|
|
2999
|
+
const credentialIssuedAtMs = credential.oauthClientId !== undefined
|
|
3000
|
+
? credential.grantCreatedAtMs
|
|
3001
|
+
: typeof credential.issuedAt === "number" &&
|
|
3002
|
+
Number.isSafeInteger(credential.issuedAt)
|
|
3003
|
+
? credential.issuedAt * 1000
|
|
3004
|
+
: undefined;
|
|
3005
|
+
const mcpCredentialIssuedAtMs = credentialIssuedAtMs !== undefined &&
|
|
3006
|
+
Number.isSafeInteger(credentialIssuedAtMs)
|
|
3007
|
+
? credentialIssuedAtMs
|
|
3008
|
+
: undefined;
|
|
2796
3009
|
const admitted = await admitIssuedCredential({
|
|
2797
3010
|
authed: true,
|
|
2798
3011
|
identity: {
|
|
2799
3012
|
userEmail: credential.userEmail,
|
|
2800
3013
|
identityAssurance: stored?.kind === "service" ? "service" : "user",
|
|
3014
|
+
...(mcpCredentialIssuedAtMs !== undefined
|
|
3015
|
+
? { mcpCredentialIssuedAtMs }
|
|
3016
|
+
: {}),
|
|
2801
3017
|
...(orgId !== undefined ? { orgId } : {}),
|
|
2802
3018
|
orgDomain: credential.orgDomain,
|
|
2803
3019
|
...(credential.oauthScopes
|
|
@@ -186,6 +186,7 @@ async function signConnectToken(params) {
|
|
|
186
186
|
scope: MCP_OAUTH_DEFAULT_SCOPE,
|
|
187
187
|
resource: mcpResourceUrl(params.issuer),
|
|
188
188
|
issuer: params.issuer,
|
|
189
|
+
grantCreatedAtMs: Date.now(),
|
|
189
190
|
jti: params.jti,
|
|
190
191
|
expiresIn: params.expiresIn,
|
|
191
192
|
...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),
|