@agent-native/core 0.227.0 → 0.228.0-nightly-20261111004142

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 (190) hide show
  1. package/dist/action.d.ts +6 -2
  2. package/dist/agent/engine/builder-engine.js +6 -1
  3. package/dist/agent/engine/tool-call-journal-seed.d.ts +2 -0
  4. package/dist/agent/engine/tool-call-journal-seed.js +14 -0
  5. package/dist/agent/harness/store.js +14 -0
  6. package/dist/agent/production-agent.js +257 -17
  7. package/dist/agent/reaped-run-thread.js +6 -0
  8. package/dist/agent/run-manager.js +6 -0
  9. package/dist/agent/tool-call-journal.d.ts +4 -0
  10. package/dist/agent/tool-call-journal.js +12 -0
  11. package/dist/agent/types.d.ts +7 -1
  12. package/dist/app-config/agent.d.ts +3 -0
  13. package/dist/app-config/agent.js +7 -0
  14. package/dist/app-config/schema.d.ts +3 -0
  15. package/dist/application-state/handlers.d.ts +3 -3
  16. package/dist/application-state/handlers.js +6 -1
  17. package/dist/application-state/index.d.ts +3 -2
  18. package/dist/application-state/index.js +3 -2
  19. package/dist/application-state/last-used-tab.d.ts +11 -0
  20. package/dist/application-state/last-used-tab.js +43 -0
  21. package/dist/application-state/script-helpers.d.ts +26 -0
  22. package/dist/application-state/script-helpers.js +83 -15
  23. package/dist/application-state/store.d.ts +8 -0
  24. package/dist/application-state/store.js +16 -0
  25. package/dist/cli/agent.js +7 -3
  26. package/dist/cli/code-agent-executor.d.ts +1 -0
  27. package/dist/cli/code-agent-executor.js +37 -2
  28. package/dist/cli/code.js +3 -1
  29. package/dist/client/agent-sidebar-state.d.ts +2 -0
  30. package/dist/client/browser-tab-use.d.ts +9 -0
  31. package/dist/client/browser-tab-use.js +112 -0
  32. package/dist/client/chat/runtime.js +3 -1
  33. package/dist/client/error-capture.js +33 -1
  34. package/dist/client/navigation/index.d.ts +1 -0
  35. package/dist/client/navigation/index.js +1 -0
  36. package/dist/client/resources/mcp-integration-logos.js +16 -0
  37. package/dist/client/review/use-review.d.ts +28 -0
  38. package/dist/client/review/use-review.js +41 -12
  39. package/dist/client/route-state.js +2 -0
  40. package/dist/client/sse-event-processor.d.ts +1 -0
  41. package/dist/client/sse-event-processor.js +7 -1
  42. package/dist/client/use-db-sync.d.ts +8 -1
  43. package/dist/client/use-db-sync.js +6 -5
  44. package/dist/coding-tools/run-code.js +12 -2
  45. package/dist/coding-tools/sandbox/executions-store.d.ts +1 -1
  46. package/dist/coding-tools/sandbox/executions-store.js +9 -3
  47. package/dist/collab/routes.d.ts +1 -1
  48. package/dist/data-programs/execute.js +1 -1
  49. package/dist/db/schema-snapshot.generated.js +17 -3
  50. package/dist/extensions/url-safety.d.ts +1 -3
  51. package/dist/extensions/url-safety.js +23 -23
  52. package/dist/file-upload/actions/upload-image.d.ts +5 -5
  53. package/dist/file-upload/index.d.ts +1 -1
  54. package/dist/file-upload/index.js +1 -1
  55. package/dist/file-upload/s3-client.d.ts +1 -0
  56. package/dist/file-upload/s3-client.js +70 -13
  57. package/dist/file-upload/s3.d.ts +9 -0
  58. package/dist/file-upload/s3.js +14 -1
  59. package/dist/framework-package-versions.json +8 -8
  60. package/dist/jobs/run-history.js +10 -3
  61. package/dist/jobs/scheduler.js +5 -36
  62. package/dist/localization/core-messages/supplemental/ar-SA.js +35 -0
  63. package/dist/localization/core-messages/supplemental/de-DE.js +35 -0
  64. package/dist/localization/core-messages/supplemental/en-US.js +35 -0
  65. package/dist/localization/core-messages/supplemental/es-ES.js +35 -0
  66. package/dist/localization/core-messages/supplemental/fr-FR.js +35 -0
  67. package/dist/localization/core-messages/supplemental/hi-IN.js +35 -0
  68. package/dist/localization/core-messages/supplemental/ja-JP.js +35 -0
  69. package/dist/localization/core-messages/supplemental/ko-KR.js +35 -0
  70. package/dist/localization/core-messages/supplemental/pt-BR.js +35 -0
  71. package/dist/localization/core-messages/supplemental/zh-CN.js +35 -0
  72. package/dist/localization/core-messages/supplemental/zh-TW.js +35 -0
  73. package/dist/localization/default-messages.d.ts +35 -0
  74. package/dist/localization/default-messages.js +35 -0
  75. package/dist/localization/mcp-settings-messages.d.ts +35 -0
  76. package/dist/mcp/actions/list-mcp-client-connections.d.ts +8 -0
  77. package/dist/mcp/actions/list-mcp-client-connections.js +39 -0
  78. package/dist/mcp/browser-tab-target.d.ts +16 -0
  79. package/dist/mcp/browser-tab-target.js +58 -0
  80. package/dist/mcp/build-server.d.ts +1 -0
  81. package/dist/mcp/build-server.js +38 -7
  82. package/dist/mcp/call-attribution.d.ts +4 -1
  83. package/dist/mcp/call-attribution.js +3 -1
  84. package/dist/mcp/client-connections.d.ts +6 -0
  85. package/dist/mcp/client-connections.js +91 -0
  86. package/dist/mcp/connect-route.js +12 -2
  87. package/dist/mcp/connect-store.d.ts +3 -0
  88. package/dist/mcp/connect-store.js +10 -1
  89. package/dist/mcp/direct-action-call.js +1 -0
  90. package/dist/mcp/oauth-store.d.ts +6 -0
  91. package/dist/mcp/oauth-store.js +23 -0
  92. package/dist/mcp/oauth-token.d.ts +1 -0
  93. package/dist/mcp/oauth-token.js +1 -0
  94. package/dist/mcp/server.js +9 -20
  95. package/dist/mcp-client/config.d.ts +2 -0
  96. package/dist/mcp-client/manager.js +23 -0
  97. package/dist/mcp-client/workspace-servers.js +42 -10
  98. package/dist/notifications/routes.d.ts +3 -3
  99. package/dist/org/handlers.d.ts +1 -1
  100. package/dist/org/handlers.js +21 -17
  101. package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
  102. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  103. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  104. package/dist/provider-api/index.js +17 -36
  105. package/dist/review/actions/create-review-comment.d.ts +7 -0
  106. package/dist/review/actions/create-review-comment.js +29 -3
  107. package/dist/review/actions/delete-review-comment.js +2 -2
  108. package/dist/review/actions/list-review-comments.d.ts +3 -0
  109. package/dist/review/actions/list-review-comments.js +41 -17
  110. package/dist/review/actions/react-to-review-comment.js +2 -2
  111. package/dist/review/actions/reply-review-comment.d.ts +4 -0
  112. package/dist/review/actions/reply-review-comment.js +15 -6
  113. package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
  114. package/dist/review/actions/update-review-comment-anchor.js +39 -10
  115. package/dist/review/actions/update-review-comment.d.ts +3 -0
  116. package/dist/review/actions/update-review-comment.js +28 -3
  117. package/dist/review/errors.d.ts +33 -0
  118. package/dist/review/errors.js +51 -0
  119. package/dist/review/index.d.ts +5 -4
  120. package/dist/review/index.js +3 -2
  121. package/dist/review/registry.d.ts +51 -1
  122. package/dist/review/registry.js +117 -0
  123. package/dist/review/store.d.ts +59 -14
  124. package/dist/review/store.js +622 -101
  125. package/dist/review/suggestions/actions.js +65 -10
  126. package/dist/review/suggestions/actor-kind.d.ts +5 -0
  127. package/dist/review/suggestions/actor-kind.js +20 -0
  128. package/dist/review/suggestions/store.js +16 -2
  129. package/dist/review/suggestions/types.d.ts +13 -0
  130. package/dist/review/types.d.ts +89 -0
  131. package/dist/scripts/db/dev-query-proxy.d.ts +2 -0
  132. package/dist/scripts/db/dev-query-proxy.js +4 -1
  133. package/dist/scripts/db/query.js +3 -0
  134. package/dist/scripts/dev/shell.js +4 -0
  135. package/dist/scripts/dev-session.d.ts +31 -0
  136. package/dist/scripts/dev-session.js +58 -0
  137. package/dist/scripts/resources/list.js +53 -4
  138. package/dist/scripts/runner.js +22 -4
  139. package/dist/search/query.d.ts +11 -5
  140. package/dist/search/query.js +52 -15
  141. package/dist/search/tokenize.d.ts +1 -0
  142. package/dist/search/tokenize.js +3 -0
  143. package/dist/secrets/api-keys.d.ts +4 -0
  144. package/dist/secrets/api-keys.js +5 -0
  145. package/dist/secrets/register.d.ts +2 -0
  146. package/dist/secrets/routes.d.ts +2 -2
  147. package/dist/secrets/routes.js +85 -6
  148. package/dist/secrets/storage.d.ts +2 -0
  149. package/dist/secrets/storage.js +34 -9
  150. package/dist/secrets/substitution.d.ts +3 -2
  151. package/dist/secrets/substitution.js +56 -7
  152. package/dist/server/action-discovery.js +5 -0
  153. package/dist/server/action-routes.js +8 -6
  154. package/dist/server/agent-chat/prompt-resources.js +117 -79
  155. package/dist/server/agent-chat/script-entries.js +15 -8
  156. package/dist/server/agent-chat-plugin.js +9 -4
  157. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  158. package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
  159. package/dist/server/agents-bundle.d.ts +3 -1
  160. package/dist/server/agents-bundle.js +4 -4
  161. package/dist/server/csrf.js +1 -0
  162. package/dist/server/dev-action-bridge.d.ts +2 -0
  163. package/dist/server/dev-action-bridge.js +34 -5
  164. package/dist/server/first-touch-route.d.ts +1 -1
  165. package/dist/server/http-response-telemetry.js +4 -0
  166. package/dist/server/loopback.js +3 -1
  167. package/dist/server/request-context.d.ts +26 -0
  168. package/dist/shared/app-state-routes.d.ts +7 -0
  169. package/dist/shared/app-state-routes.js +7 -0
  170. package/dist/shared/mcp-client-connections.d.ts +41 -0
  171. package/dist/shared/mcp-client-connections.js +18 -0
  172. package/dist/shared/mcp-connect-content.d.ts +18 -0
  173. package/dist/shared/mcp-connect-content.js +200 -172
  174. package/dist/shared/runtime-config.js +5 -1
  175. package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  176. package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  177. package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
  178. package/dist/tracking/providers.d.ts +7 -0
  179. package/dist/tracking/providers.js +39 -17
  180. package/dist/triggers/condition-evaluator.d.ts +2 -0
  181. package/dist/triggers/condition-evaluator.js +93 -3
  182. package/dist/triggers/dispatcher.js +24 -6
  183. package/docs/AGENTS.md +16 -16
  184. package/docs/content/create-with-an-agent.mdx +10 -4
  185. package/docs/content/durable-resume.mdx +11 -2
  186. package/docs/content/toolkit-comments-review.mdx +54 -1
  187. package/package.json +5 -4
  188. package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
  189. package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
  190. package/src/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
@@ -136,6 +136,41 @@ const messages = {
136
136
  mcpClientSetup: "Connect an AI host",
137
137
  mcpClientSetupDescription: "Choose a host for step-by-step setup, or paste the URL into any MCP-compatible client.",
138
138
  mcpChooseAssistant: "Choose your AI assistant",
139
+ mcpCopyAndOpen: "Copy URL and open {{host}}",
140
+ mcpManualSetup: "Manual setup",
141
+ mcpTryPrompt: "Try it in {{host}}",
142
+ mcpHelpConnecting: "Need help connecting?",
143
+ mcpLocalAddressHint: "Local address: {{host}} needs a publicly reachable HTTPS server URL.",
144
+ mcpAuthentication: "Authentication",
145
+ mcpTestPrompt: "What tools are available from the {{app}} connector?",
146
+ mcpSetupIntro: "Use Claude, ChatGPT, or another AI app to read and update data in {{app}}.",
147
+ mcpStageOpen: "Open {{host}}",
148
+ mcpStageAdd: "Add a custom connector",
149
+ mcpStageDetails: "Enter connection details",
150
+ mcpStageAuthorize: "Authorize and test in {{host}}",
151
+ mcpStageEnable: "Enable and test in {{host}}",
152
+ mcpNameLabel: "Name",
153
+ mcpHostBenefit: "Use your Agent-Native tools directly in {{host}}.",
154
+ mcpManualBenefit: "Use your Agent-Native tools in any MCP-compatible app.",
155
+ mcpStageOpenSettings: "Open connector settings",
156
+ mcpReadyToTry: "Test the connection",
157
+ mcpTestUnavailable: "Available once connected.",
158
+ mcpCopyTestPrompt: "Copy prompt",
159
+ mcpConnectionDocs: "Setup guide",
160
+ mcpServerUrl: "Server URL",
161
+ mcpCopyUrl: "Copy URL",
162
+ mcpStageAddServer: "Add the MCP server",
163
+ mcpSetupSteps: "Setup",
164
+ mcpOpensNewTab: "Opens in a new tab",
165
+ mcpOpensDesktopApp: "Opens the desktop app",
166
+ mcpConnected: "Connected",
167
+ mcpNotConnected: "Not connected",
168
+ mcpConnectionUnavailable: "Status unavailable",
169
+ mcpStatusCheckFailed: "Couldn't check MCP connections.",
170
+ mcpLastUsedLabel: "Last used",
171
+ mcpLastUsed: "Last used {{time}}",
172
+ mcpNoActivity: "No activity recorded",
173
+ mcpLegacyStatusUnknown: "Older credentials do not include enough information to verify their current status.",
139
174
  mcpCommand: "Command",
140
175
  mcpConfig: "MCP config",
141
176
  mcpCopy: "Copy",
@@ -69,6 +69,41 @@ export interface McpSettingsMessages {
69
69
  mcpClientSetup: string;
70
70
  mcpClientSetupDescription: string;
71
71
  mcpChooseAssistant: string;
72
+ mcpCopyAndOpen: string;
73
+ mcpManualSetup: string;
74
+ mcpTryPrompt: string;
75
+ mcpHelpConnecting: string;
76
+ mcpLocalAddressHint: string;
77
+ mcpAuthentication: string;
78
+ mcpTestPrompt: string;
79
+ mcpSetupIntro: string;
80
+ mcpStageOpen: string;
81
+ mcpStageAdd: string;
82
+ mcpStageDetails: string;
83
+ mcpStageAuthorize: string;
84
+ mcpStageEnable: string;
85
+ mcpNameLabel: string;
86
+ mcpHostBenefit: string;
87
+ mcpManualBenefit: string;
88
+ mcpStageOpenSettings: string;
89
+ mcpReadyToTry: string;
90
+ mcpTestUnavailable: string;
91
+ mcpCopyTestPrompt: string;
92
+ mcpConnectionDocs: string;
93
+ mcpServerUrl: string;
94
+ mcpCopyUrl: string;
95
+ mcpStageAddServer: string;
96
+ mcpSetupSteps: string;
97
+ mcpOpensNewTab: string;
98
+ mcpOpensDesktopApp: string;
99
+ mcpConnected: string;
100
+ mcpNotConnected: string;
101
+ mcpConnectionUnavailable: string;
102
+ mcpStatusCheckFailed: string;
103
+ mcpLastUsedLabel: string;
104
+ mcpLastUsed: string;
105
+ mcpNoActivity: string;
106
+ mcpLegacyStatusUnknown: string;
72
107
  mcpCommand: string;
73
108
  mcpConfig: string;
74
109
  mcpCopy: string;
@@ -0,0 +1,8 @@
1
+ declare const _default: import("../../action.js").ActionDefinition<Record<string, never>, {
2
+ connections: {
3
+ client: "chatgpt" | "claude" | "claude-code" | "codex" | "cursor" | "grok" | "other" | "vscode";
4
+ status: "connected" | "disconnected" | "unknown";
5
+ lastUsedAt: number | null;
6
+ }[];
7
+ }>;
8
+ export default _default;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { defineAction, defineActionErrors } from "../../action.js";
3
+ import { getRequestContext } from "../../server/request-context.js";
4
+ import { mcpClientConnectionsSchema } from "../../shared/mcp-client-connections.js";
5
+ import { listMcpClientConnections } from "../client-connections.js";
6
+ import { resolveMcpOAuthIssuer } from "../oauth-route.js";
7
+ const errors = defineActionErrors("unauthorized", "mcp_connections_unavailable");
8
+ export default defineAction({
9
+ description: "List the current user's external MCP client authorizations for this app and active organization. Returns per-client connected, disconnected, or unknown status and the last server-recorded activity timestamp. Connected means authorization remains valid, not that the client is currently online. Never returns tokens. Unknown means older credentials lack enough metadata to verify their current validity.",
10
+ schema: z.object({}),
11
+ outputSchema: mcpClientConnectionsSchema,
12
+ errors,
13
+ requiresAuth: true,
14
+ readOnly: true,
15
+ parallelSafe: true,
16
+ persistInBrowser: false,
17
+ http: { method: "GET" },
18
+ run: async (_args, ctx) => {
19
+ const email = ctx?.userEmail?.trim().toLowerCase();
20
+ if (!email)
21
+ return errors.fail("Sign in to view MCP connections.", "unauthorized", {
22
+ statusCode: 401,
23
+ });
24
+ const issuer = resolveMcpOAuthIssuer(getRequestContext()?.requestOrigin);
25
+ if (!issuer)
26
+ return errors.fail("Unable to resolve this app's MCP resource.", "mcp_connections_unavailable", { statusCode: 503 });
27
+ try {
28
+ return await listMcpClientConnections({
29
+ email,
30
+ orgId: ctx?.orgId ?? null,
31
+ issuer,
32
+ });
33
+ }
34
+ catch (error) {
35
+ console.error("[mcp-connections] Status lookup failed", error);
36
+ return errors.fail("MCP connection status is unavailable. Try again.", "mcp_connections_unavailable", { statusCode: 503 });
37
+ }
38
+ },
39
+ });
@@ -0,0 +1,16 @@
1
+ import { type BrowserTabTarget, type BrowserTabTargetSlot } from "../server/request-context.js";
2
+ /**
3
+ * An MCP tool call comes from no browser tab, so tab-scoped app state
4
+ * (navigation, navigate commands, selection) would otherwise fall back to
5
+ * account-wide rows no tab keeps current. This slot points the call at the
6
+ * tab of this app the user used last. The lookup runs only when the call
7
+ * first reads or writes tab-scoped state.
8
+ */
9
+ export declare function createBrowserTabTargetSlot(): BrowserTabTargetSlot;
10
+ /**
11
+ * Tells the calling agent which tab a call's tab-scoped reads and writes
12
+ * reached, since nothing in the call itself says so.
13
+ */
14
+ export declare function browserTabTargetNote(target: BrowserTabTarget, now?: number): string;
15
+ /** Adds the tab note to a successful tool result that used tab-scoped state. */
16
+ export declare function withBrowserTabTargetNote<T>(result: T, slot: BrowserTabTargetSlot | undefined): T;
@@ -0,0 +1,58 @@
1
+ import { findLastUsedBrowserTab, LAST_USED_BROWSER_TAB_MAX_AGE_MS, } from "../application-state/last-used-tab.js";
2
+ import { getRequestUserEmail, } from "../server/request-context.js";
3
+ /**
4
+ * An MCP tool call comes from no browser tab, so tab-scoped app state
5
+ * (navigation, navigate commands, selection) would otherwise fall back to
6
+ * account-wide rows no tab keeps current. This slot points the call at the
7
+ * tab of this app the user used last. The lookup runs only when the call
8
+ * first reads or writes tab-scoped state.
9
+ */
10
+ export function createBrowserTabTargetSlot() {
11
+ let pending;
12
+ const slot = {
13
+ resolve() {
14
+ pending ??= (async () => {
15
+ const userEmail = getRequestUserEmail();
16
+ const target = userEmail
17
+ ? await findLastUsedBrowserTab(userEmail)
18
+ : { kind: "unavailable" };
19
+ slot.resolved = target;
20
+ return target;
21
+ })();
22
+ return pending;
23
+ },
24
+ };
25
+ return slot;
26
+ }
27
+ function usedAgo(usedAt, now) {
28
+ const minutes = Math.floor(Math.max(0, now - usedAt) / 60_000);
29
+ if (minutes < 1)
30
+ return "less than a minute ago";
31
+ if (minutes < 60)
32
+ return `${minutes} min ago`;
33
+ return `${Math.floor(minutes / 60)} h ago`;
34
+ }
35
+ /**
36
+ * Tells the calling agent which tab a call's tab-scoped reads and writes
37
+ * reached, since nothing in the call itself says so.
38
+ */
39
+ export function browserTabTargetNote(target, now = Date.now()) {
40
+ if (target.kind === "last-used") {
41
+ return `Browser tab: this call used the user's most recently used tab of this app (last used ${usedAgo(target.usedAt, now)}). A navigate command reaches that tab only while it stays open, and this result doesn't confirm that it moved.`;
42
+ }
43
+ const hours = Math.round(LAST_USED_BROWSER_TAB_MAX_AGE_MS / 3_600_000);
44
+ return `Browser tab: no tab of this app was used in the last ${hours} hours, so there was no on-screen state to read and a navigate command won't move any tab. Ask the user to open the app in a browser tab, then retry.`;
45
+ }
46
+ /** Adds the tab note to a successful tool result that used tab-scoped state. */
47
+ export function withBrowserTabTargetNote(result, slot) {
48
+ const target = slot?.resolved;
49
+ if (!target || !result || typeof result !== "object")
50
+ return result;
51
+ const { content, isError } = result;
52
+ if (isError === true || !Array.isArray(content))
53
+ return result;
54
+ return {
55
+ ...result,
56
+ content: [...content, { type: "text", text: browserTabTargetNote(target) }],
57
+ };
58
+ }
@@ -367,6 +367,7 @@ export type VerifyAuthResult = {
367
367
  */
368
368
  export declare function verifyAuth(authHeader: string | undefined, ownerEmailHeader?: string, options?: {
369
369
  allowDevOpen?: boolean;
370
+ allowLocalOwnerHint?: boolean;
370
371
  resourceUrl?: string | string[];
371
372
  event?: unknown;
372
373
  /** This app's public origin; federated orgs need it for the membership check. */
@@ -24,6 +24,7 @@ import { mcpApprovalPrompt, mcpApprovalRelativeLinkNote, mcpApprovalReviewUrl, }
24
24
  import { mcpApprovalAppId } from "./approval-store.js";
25
25
  export { isAutoReadExcludedActionName } from "./action-eligibility.js";
26
26
  import { isMcpAppsInlineEnabled } from "./app-cards-lab.js";
27
+ import { createBrowserTabTargetSlot, withBrowserTabTargetNote, } from "./browser-tab-target.js";
27
28
  import { getBuiltinCrossAppTools } from "./builtin-tools.js";
28
29
  import { MCP_CONNECT_OAUTH_CLIENT_ID, MCP_CONNECT_SCOPE, } from "./connect-store.js";
29
30
  import { createDirectActionCall } from "./direct-action-call.js";
@@ -2159,7 +2160,7 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2159
2160
  callerAuthUserId ??= import("../server/better-auth-instance.js").then(({ findBetterAuthUserIdForEmail }) => findBetterAuthUserIdForEmail(email));
2160
2161
  return callerAuthUserId;
2161
2162
  }
2162
- async function withCallerContext(fn, mcpRequestId, authUserId) {
2163
+ async function withCallerContext(fn, mcpRequestId, authUserId, run) {
2163
2164
  const orgId = await orgIdPromise;
2164
2165
  return runWithRequestContext({
2165
2166
  userEmail: effectiveIdentity?.userEmail,
@@ -2178,10 +2179,16 @@ export async function createMCPServerForRequest(config, identity, requestMeta) {
2178
2179
  : {}),
2179
2180
  ...(mcpRequestId ? { mcpRequestId } : {}),
2180
2181
  mcpCredentialIssuedAtMs: effectiveIdentity?.mcpCredentialIssuedAtMs ?? null,
2182
+ ...(run ? { run } : {}),
2181
2183
  }, fn);
2182
2184
  }
2183
2185
  async function withToolCallContext(fn, mcpRequestId) {
2184
- return withCallerContext(fn, mcpRequestId, await resolveCallerAuthUserId());
2186
+ // Only a signed-in person has browser tabs for a call to act on.
2187
+ const browserTabTarget = hasVerifiedMcpUserIdentity(effectiveIdentity)
2188
+ ? createBrowserTabTargetSlot()
2189
+ : undefined;
2190
+ const result = await withCallerContext(fn, mcpRequestId, await resolveCallerAuthUserId(), browserTabTarget ? { browserTabTarget } : undefined);
2191
+ return withBrowserTabTargetNote(result, browserTabTarget);
2185
2192
  }
2186
2193
  const actionCallContext = {
2187
2194
  appId: config.appId,
@@ -3063,6 +3070,7 @@ async function verifyIssuedMcpCredential(token, resourceUrl) {
3063
3070
  : {}),
3064
3071
  oauthScopes: oauth.scopes,
3065
3072
  oauthClientId: oauth.clientId,
3073
+ oauthResource: oauth.resource,
3066
3074
  ...(oauth.catalogScope ? { catalogScope: oauth.catalogScope } : {}),
3067
3075
  ...(oauth.grantCreatedAtMs !== undefined
3068
3076
  ? { grantCreatedAtMs: oauth.grantCreatedAtMs }
@@ -3137,6 +3145,23 @@ async function admitIssuedMcpCredential(credential, requestOrigin) {
3137
3145
  if (admitted.authed && credential.connect) {
3138
3146
  await markConnectTokenUsed(credential.connect.jti);
3139
3147
  }
3148
+ else if (admitted.authed &&
3149
+ credential.oauthClientId &&
3150
+ credential.oauthResource) {
3151
+ try {
3152
+ const { recordOAuthClientActivity } = await import("./oauth-store.js");
3153
+ await recordOAuthClientActivity({
3154
+ clientId: credential.oauthClientId,
3155
+ email: credential.userEmail,
3156
+ orgId: orgId ?? null,
3157
+ resource: credential.oauthResource,
3158
+ });
3159
+ }
3160
+ catch (error) {
3161
+ // Activity metadata must not turn a successfully admitted request into an authentication failure.
3162
+ console.warn("[mcp] Could not record client activity", error);
3163
+ }
3164
+ }
3140
3165
  return admitted;
3141
3166
  }
3142
3167
  /**
@@ -3241,6 +3266,9 @@ async function checkIssuedCredential(result, requestOrigin, storedConnectToken,
3241
3266
  */
3242
3267
  export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
3243
3268
  const accessTokens = getAccessTokens();
3269
+ const trustedOwnerHeader = options.allowLocalOwnerHint
3270
+ ? ownerEmailHeader
3271
+ : undefined;
3244
3272
  const hasA2ASecret = !!readDeployCredentialEnv("A2A_SECRET")?.trim();
3245
3273
  const token = getBearerToken(authHeader);
3246
3274
  const orgMemberDelegation = token
@@ -3250,7 +3278,7 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
3250
3278
  if (!orgMemberDelegation) {
3251
3279
  const issued = await verifyIssuedMcpCredential(token, options.resourceUrl);
3252
3280
  if (issued === "unverified") {
3253
- return ((await matchStaticAccessToken(token, accessTokens, ownerEmailHeader)) ?? {
3281
+ return ((await matchStaticAccessToken(token, accessTokens, trustedOwnerHeader)) ?? {
3254
3282
  authed: false,
3255
3283
  refusal: "invalid",
3256
3284
  });
@@ -3265,8 +3293,8 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
3265
3293
  }
3266
3294
  return {
3267
3295
  authed: true,
3268
- identity: deriveStaticTokenIdentity(ownerEmailHeader),
3269
- fullSurface: !!(ownerEmailHeader && ownerEmailHeader.trim()),
3296
+ identity: deriveStaticTokenIdentity(trustedOwnerHeader),
3297
+ fullSurface: !!(trustedOwnerHeader && trustedOwnerHeader.trim()),
3270
3298
  };
3271
3299
  }
3272
3300
  if (!token)
@@ -3332,7 +3360,7 @@ export async function verifyAuth(authHeader, ownerEmailHeader, options = {}) {
3332
3360
  }
3333
3361
  // A supplied bearer that failed JWT verification must not fall through to
3334
3362
  // dev-open auth or reuse a forwarded owner-email hint.
3335
- return ((await matchStaticAccessToken(token, accessTokens, ownerEmailHeader)) ?? {
3363
+ return ((await matchStaticAccessToken(token, accessTokens, trustedOwnerHeader)) ?? {
3336
3364
  authed: false,
3337
3365
  refusal: "invalid",
3338
3366
  });
@@ -3357,9 +3385,12 @@ async function matchStaticAccessToken(token, accessTokens, ownerEmailHeader) {
3357
3385
  });
3358
3386
  if (!matched)
3359
3387
  return null;
3388
+ const identity = deriveStaticTokenIdentity(ownerEmailHeader);
3389
+ if (!identity)
3390
+ return { authed: false, refusal: "invalid" };
3360
3391
  return {
3361
3392
  authed: true,
3362
- identity: deriveStaticTokenIdentity(ownerEmailHeader),
3393
+ identity,
3363
3394
  fullSurface: true,
3364
3395
  };
3365
3396
  }
@@ -5,10 +5,13 @@ export interface McpCallAttributionInput {
5
5
  /** `ctx.mcpReq.envelope`: the reserved `_meta` keys the SDK lifted out. */
6
6
  envelope: unknown;
7
7
  oauthClientId: string | undefined;
8
+ /** The request's User-Agent header. */
9
+ userAgent?: string | undefined;
8
10
  }
9
11
  /**
10
12
  * The client facts the audit trail records for one MCP `tools/call`. Only
11
13
  * `oauthClientId` comes from a credential this server issued. The name and
12
- * version are whatever the client sent as `clientInfo`.
14
+ * version are whatever the client sent as `clientInfo`, and the User-Agent
15
+ * whatever it sent as that header.
13
16
  */
14
17
  export declare function mcpCallAttribution(input: McpCallAttributionInput): Pick<ActionRunContext, "mcpClient">;
@@ -2,7 +2,8 @@ import { readClientInfoFromRequest } from "./analytics.js";
2
2
  /**
3
3
  * The client facts the audit trail records for one MCP `tools/call`. Only
4
4
  * `oauthClientId` comes from a credential this server issued. The name and
5
- * version are whatever the client sent as `clientInfo`.
5
+ * version are whatever the client sent as `clientInfo`, and the User-Agent
6
+ * whatever it sent as that header.
6
7
  */
7
8
  export function mcpCallAttribution(input) {
8
9
  // 2026-07-28 requests carry clientInfo in the envelope, which the SDK
@@ -18,6 +19,7 @@ export function mcpCallAttribution(input) {
18
19
  const mcpClient = {
19
20
  ...(info.clientName ? { name: info.clientName } : {}),
20
21
  ...(info.clientVersion ? { version: info.clientVersion } : {}),
22
+ ...(input.userAgent ? { userAgent: input.userAgent } : {}),
21
23
  ...(input.oauthClientId ? { oauthClientId: input.oauthClientId } : {}),
22
24
  };
23
25
  return Object.keys(mcpClient).length > 0 ? { mcpClient } : {};
@@ -0,0 +1,6 @@
1
+ import type { McpClientConnections } from "../shared/mcp-client-connections.js";
2
+ export declare function listMcpClientConnections({ email, orgId, issuer, }: {
3
+ email: string;
4
+ orgId: string | null;
5
+ issuer: string;
6
+ }): Promise<McpClientConnections>;
@@ -0,0 +1,91 @@
1
+ import { z } from "zod";
2
+ import { getDbExec } from "../db/client.js";
3
+ import { MCP_CONNECT_GUIDES, resolveMcpConnectGuideId, } from "../shared/mcp-connect-content.js";
4
+ import { ensureConnectTables } from "./connect-store.js";
5
+ import { ensureOAuthTables } from "./oauth-store.js";
6
+ import { MCP_DIRECTORY_ROUTE_PREFIX, MCP_LEGACY_ROUTE_PREFIX, MCP_PUBLIC_ROUTE_PREFIX, } from "./route-paths.js";
7
+ const timestamp = z.coerce.number().int().nonnegative().safe().nullable();
8
+ const oauthRow = z.object({
9
+ client_name: z.string().nullable(),
10
+ active: z.boolean(),
11
+ unknown: z.boolean(),
12
+ last_used_at: timestamp,
13
+ });
14
+ const tokenRow = z.object({
15
+ client_name: z.string().nullable(),
16
+ active: z.boolean(),
17
+ unknown: z.boolean(),
18
+ last_used_at: timestamp,
19
+ });
20
+ const MAX_CONNECTIONS = 200;
21
+ export async function listMcpClientConnections({ email, orgId, issuer, }) {
22
+ await Promise.all([ensureConnectTables(), ensureOAuthTables()]);
23
+ const owner = email.trim().toLowerCase();
24
+ const now = Date.now();
25
+ const resources = [
26
+ MCP_PUBLIC_ROUTE_PREFIX,
27
+ MCP_LEGACY_ROUTE_PREFIX,
28
+ MCP_DIRECTORY_ROUTE_PREFIX,
29
+ ].map((path) => `${issuer.replace(/\/+$/, "")}${path}`);
30
+ const db = getDbExec();
31
+ const [oauth, tokens] = await Promise.all([
32
+ db.execute({
33
+ sql: `SELECT c.client_name,
34
+ BOOL_OR(r.revoked_at IS NULL AND (r.expires_at IS NULL OR r.expires_at > ?)
35
+ AND COALESCE(LOWER(r.issued_for_email) = ?, FALSE)) AS active,
36
+ BOOL_OR(r.revoked_at IS NULL AND (r.expires_at IS NULL OR r.expires_at > ?)
37
+ AND r.issued_for_email IS NULL) AS unknown,
38
+ MAX(r.last_used_at) AS last_used_at
39
+ FROM mcp_oauth_refresh_tokens r LEFT JOIN mcp_oauth_clients c ON c.client_id = r.client_id
40
+ WHERE LOWER(r.owner_email) = ? AND r.org_id IS NOT DISTINCT FROM ?
41
+ AND r.resource IN (?, ?, ?)
42
+ GROUP BY r.client_id, c.client_name
43
+ ORDER BY r.client_id LIMIT ?`,
44
+ args: [now, owner, now, owner, orgId, ...resources, MAX_CONNECTIONS + 1],
45
+ }),
46
+ db.execute({
47
+ sql: `SELECT client_name,
48
+ COALESCE(BOOL_OR(revoked_at IS NULL AND expires_at > ? AND resource IS NOT NULL), FALSE) AS active,
49
+ COALESCE(BOOL_OR(revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)
50
+ AND (expires_at IS NULL OR resource IS NULL)), FALSE) AS unknown,
51
+ MAX(last_used_at) AS last_used_at
52
+ FROM mcp_connect_tokens
53
+ WHERE LOWER(owner_email) = ? AND org_id IS NOT DISTINCT FROM ? AND kind = 'personal'
54
+ AND (resource IN (?, ?, ?) OR resource IS NULL)
55
+ GROUP BY client_name ORDER BY client_name LIMIT ?`,
56
+ args: [now, now, owner, orgId, ...resources, MAX_CONNECTIONS + 1],
57
+ }),
58
+ ]);
59
+ if (oauth.rows.length > MAX_CONNECTIONS ||
60
+ tokens.rows.length > MAX_CONNECTIONS)
61
+ throw new Error("MCP connection metadata exceeds the supported result limit.");
62
+ const summaries = new Map(MCP_CONNECT_GUIDES.map((guide) => [
63
+ guide.id,
64
+ {
65
+ client: guide.id,
66
+ status: "disconnected",
67
+ lastUsedAt: null,
68
+ },
69
+ ]));
70
+ const add = (name, status, lastUsedAt) => {
71
+ const client = resolveMcpConnectGuideId(name || "other");
72
+ const summary = summaries.get(client);
73
+ if (!summary)
74
+ throw new Error("Unknown MCP client group.");
75
+ if (status === "connected" ||
76
+ (status === "unknown" && summary.status !== "connected"))
77
+ summary.status = status;
78
+ if (lastUsedAt !== null &&
79
+ (summary.lastUsedAt === null || lastUsedAt > summary.lastUsedAt))
80
+ summary.lastUsedAt = lastUsedAt;
81
+ };
82
+ for (const raw of oauth.rows) {
83
+ const row = oauthRow.parse(raw);
84
+ add(row.client_name, row.active ? "connected" : row.unknown ? "unknown" : "disconnected", row.last_used_at);
85
+ }
86
+ for (const raw of tokens.rows) {
87
+ const row = tokenRow.parse(raw);
88
+ add(row.client_name, row.active ? "connected" : row.unknown ? "unknown" : "disconnected", row.last_used_at);
89
+ }
90
+ return { connections: [...summaries.values()] };
91
+ }
@@ -144,12 +144,13 @@ async function mintConnectToken(params) {
144
144
  requestOrigin: params.requestOrigin,
145
145
  }, async (tx) => {
146
146
  const jti = randomUUID();
147
+ const expiresAt = (Math.floor(Date.now() / 1000) + params.ttlDays * 86_400) * 1000;
147
148
  const token = await signConnectToken({
148
149
  ownerEmail: params.email,
149
150
  orgId: params.orgId,
150
151
  orgDomain,
151
152
  issuer: params.issuer,
152
- expiresIn: `${params.ttlDays}d`,
153
+ expiresIn: expiresAt / 1000,
153
154
  jti,
154
155
  ...(params.catalogScope === "full" ? { catalogScope: "full" } : {}),
155
156
  });
@@ -158,6 +159,9 @@ async function mintConnectToken(params) {
158
159
  ownerEmail: params.email,
159
160
  orgId: params.orgId ?? null,
160
161
  label: params.label,
162
+ clientName: params.clientName,
163
+ resource: mcpResourceUrl(params.issuer),
164
+ expiresAt,
161
165
  }, tx);
162
166
  return { token, jti };
163
167
  });
@@ -1321,6 +1325,7 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1321
1325
  orgId: defaultOrganizationId,
1322
1326
  label,
1323
1327
  ttlDays,
1328
+ clientName: typeof body.client === "string" ? body.client.slice(0, 120) : null,
1324
1329
  issuer,
1325
1330
  requestOrigin: origin,
1326
1331
  ...(catalogScope ? { catalogScope } : {}),
@@ -1496,12 +1501,14 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1496
1501
  }),
1497
1502
  });
1498
1503
  }
1504
+ const expiresAt = (Math.floor(Date.now() / 1000) + DEFAULT_TOKEN_TTL_DAYS * 86_400) *
1505
+ 1000;
1499
1506
  const token = await signConnectToken({
1500
1507
  ownerEmail: claimed.ownerEmail,
1501
1508
  orgId: claimed.orgId,
1502
1509
  orgDomain,
1503
1510
  issuer,
1504
- expiresIn: `${DEFAULT_TOKEN_TTL_DAYS}d`,
1511
+ expiresIn: expiresAt / 1000,
1505
1512
  jti,
1506
1513
  ...(claimed.catalogScope
1507
1514
  ? { catalogScope: claimed.catalogScope }
@@ -1512,6 +1519,9 @@ export async function handleMcpConnect(event, subpath, options = {}) {
1512
1519
  ownerEmail: claimed.ownerEmail,
1513
1520
  orgId: claimed.orgId,
1514
1521
  label: "Device connection",
1522
+ clientName: claimed.client,
1523
+ resource: mcpResourceUrl(issuer),
1524
+ expiresAt,
1515
1525
  }, tx);
1516
1526
  if (!(await finishDeviceCodeMint(deviceCode, jti, tx))) {
1517
1527
  throw new McpCredentialIssuanceError("unavailable");
@@ -54,6 +54,9 @@ export declare function recordMintedToken(params: {
54
54
  ownerEmail: string;
55
55
  orgId?: string | null;
56
56
  label?: string | null;
57
+ clientName?: string | null;
58
+ resource?: string | null;
59
+ expiresAt?: number | null;
57
60
  kind?: "personal" | "service";
58
61
  serviceName?: string | null;
59
62
  createdBy?: string | null;
@@ -74,6 +74,12 @@ export const mcpConnectStore = defineStore({
74
74
  await ensureColumnExists("mcp_device_codes", "purpose", `ALTER TABLE mcp_device_codes ADD COLUMN IF NOT EXISTS purpose TEXT`);
75
75
  },
76
76
  },
77
+ {
78
+ name: "client-connection-metadata",
79
+ sql: `ALTER TABLE mcp_connect_tokens ADD COLUMN IF NOT EXISTS client_name TEXT,
80
+ ADD COLUMN IF NOT EXISTS resource TEXT,
81
+ ADD COLUMN IF NOT EXISTS expires_at BIGINT`,
82
+ },
77
83
  ],
78
84
  });
79
85
  export function ensureTable() {
@@ -111,7 +117,7 @@ export async function recordMintedToken(params, exec) {
111
117
  const client = exec ?? getDbExec();
112
118
  const id = randomUUID();
113
119
  const result = await client.execute({
114
- sql: `INSERT INTO mcp_connect_tokens (id, jti, owner_email, org_id, label, kind, service_name, created_by, created_at, last_used_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
120
+ sql: `INSERT INTO mcp_connect_tokens (id, jti, owner_email, org_id, label, kind, service_name, created_by, created_at, last_used_at, revoked_at, client_name, resource, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
115
121
  args: [
116
122
  id,
117
123
  params.jti,
@@ -124,6 +130,9 @@ export async function recordMintedToken(params, exec) {
124
130
  Date.now(),
125
131
  null,
126
132
  null,
133
+ params.clientName ?? null,
134
+ params.resource ?? null,
135
+ params.expiresAt ?? null,
127
136
  ],
128
137
  });
129
138
  if (result.rowsAffected !== 1) {
@@ -122,6 +122,7 @@ export function createDirectActionCall(scope) {
122
122
  request,
123
123
  envelope: ctx.mcpReq.envelope,
124
124
  oauthClientId: effectiveIdentity?.oauthClientId,
125
+ userAgent: requestMeta?.clientName,
125
126
  }),
126
127
  approval: {
127
128
  state: () => ctx.mcpReq.requestState() ?? undefined,
@@ -118,3 +118,9 @@ export declare function touchOAuthRefreshToken(refreshToken: string, expectedOwn
118
118
  * Idempotent: an already-revoked token keeps its first timestamp.
119
119
  */
120
120
  export declare function revokeOAuthRefreshToken(refreshToken: string, expectedOwnerEmail?: string): Promise<void>;
121
+ export declare function recordOAuthClientActivity(params: {
122
+ clientId: string;
123
+ email: string;
124
+ orgId: string | null;
125
+ resource: string;
126
+ }): Promise<void>;
@@ -115,6 +115,10 @@ export const mcpOauthStore = defineStore({
115
115
  await ensureColumnExists("mcp_oauth_refresh_tokens", "issued_for_email", `ALTER TABLE mcp_oauth_refresh_tokens ADD COLUMN IF NOT EXISTS issued_for_email TEXT`);
116
116
  },
117
117
  },
118
+ {
119
+ name: "client-activity-lookup-index",
120
+ sql: `CREATE INDEX IF NOT EXISTS mcp_oauth_refresh_activity_idx ON mcp_oauth_refresh_tokens (client_id, LOWER(owner_email), LOWER(issued_for_email), org_id, resource) WHERE revoked_at IS NULL`,
121
+ },
118
122
  ],
119
123
  });
120
124
  export function ensureTable() {
@@ -571,3 +575,22 @@ export async function revokeOAuthRefreshToken(refreshToken, expectedOwnerEmail)
571
575
  if (result.rowsAffected !== 0 && result.rowsAffected !== 1)
572
576
  throw new Error("Refresh-token revocation returned an invalid row count");
573
577
  }
578
+ export async function recordOAuthClientActivity(params) {
579
+ await ensureTable();
580
+ const now = Date.now();
581
+ await getDbExec().execute({
582
+ sql: `UPDATE mcp_oauth_refresh_tokens SET last_used_at = ?
583
+ WHERE client_id = ? AND LOWER(owner_email) = ? AND LOWER(issued_for_email) = ?
584
+ AND org_id IS NOT DISTINCT FROM ? AND resource = ?
585
+ AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`,
586
+ args: [
587
+ now,
588
+ params.clientId,
589
+ params.email.toLowerCase(),
590
+ params.email.toLowerCase(),
591
+ params.orgId,
592
+ params.resource,
593
+ now,
594
+ ],
595
+ });
596
+ }
@@ -47,6 +47,7 @@ export declare function signMcpOAuthAccessToken(params: {
47
47
  }): Promise<string>;
48
48
  export declare function verifyMcpOAuthAccessToken(token: string, resource: string | string[] | undefined): Promise<{
49
49
  userEmail: string;
50
+ resource: string;
50
51
  orgId?: string | null;
51
52
  orgDomain?: string;
52
53
  scopes: string[];
@@ -173,6 +173,7 @@ export async function verifyMcpOAuthAccessToken(token, resource) {
173
173
  }
174
174
  return {
175
175
  userEmail: payload.sub,
176
+ resource: embeddedResource,
176
177
  orgId: orgIdClaim.orgId,
177
178
  orgDomain: typeof payload.org_domain === "string" ? payload.org_domain : undefined,
178
179
  scopes,