@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.
- package/dist/action.d.ts +6 -2
- package/dist/agent/engine/builder-engine.js +6 -1
- package/dist/agent/engine/tool-call-journal-seed.d.ts +2 -0
- package/dist/agent/engine/tool-call-journal-seed.js +14 -0
- package/dist/agent/harness/store.js +14 -0
- package/dist/agent/production-agent.js +257 -17
- package/dist/agent/reaped-run-thread.js +6 -0
- package/dist/agent/run-manager.js +6 -0
- package/dist/agent/tool-call-journal.d.ts +4 -0
- package/dist/agent/tool-call-journal.js +12 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/app-config/agent.d.ts +3 -0
- package/dist/app-config/agent.js +7 -0
- package/dist/app-config/schema.d.ts +3 -0
- package/dist/application-state/handlers.d.ts +3 -3
- package/dist/application-state/handlers.js +6 -1
- package/dist/application-state/index.d.ts +3 -2
- package/dist/application-state/index.js +3 -2
- package/dist/application-state/last-used-tab.d.ts +11 -0
- package/dist/application-state/last-used-tab.js +43 -0
- package/dist/application-state/script-helpers.d.ts +26 -0
- package/dist/application-state/script-helpers.js +83 -15
- package/dist/application-state/store.d.ts +8 -0
- package/dist/application-state/store.js +16 -0
- package/dist/cli/agent.js +7 -3
- package/dist/cli/code-agent-executor.d.ts +1 -0
- package/dist/cli/code-agent-executor.js +37 -2
- package/dist/cli/code.js +3 -1
- package/dist/client/agent-sidebar-state.d.ts +2 -0
- package/dist/client/browser-tab-use.d.ts +9 -0
- package/dist/client/browser-tab-use.js +112 -0
- package/dist/client/chat/runtime.js +3 -1
- package/dist/client/error-capture.js +33 -1
- package/dist/client/navigation/index.d.ts +1 -0
- package/dist/client/navigation/index.js +1 -0
- package/dist/client/resources/mcp-integration-logos.js +16 -0
- package/dist/client/review/use-review.d.ts +28 -0
- package/dist/client/review/use-review.js +41 -12
- package/dist/client/route-state.js +2 -0
- package/dist/client/sse-event-processor.d.ts +1 -0
- package/dist/client/sse-event-processor.js +7 -1
- package/dist/client/use-db-sync.d.ts +8 -1
- package/dist/client/use-db-sync.js +6 -5
- package/dist/coding-tools/run-code.js +12 -2
- package/dist/coding-tools/sandbox/executions-store.d.ts +1 -1
- package/dist/coding-tools/sandbox/executions-store.js +9 -3
- package/dist/collab/routes.d.ts +1 -1
- package/dist/data-programs/execute.js +1 -1
- package/dist/db/schema-snapshot.generated.js +17 -3
- package/dist/extensions/url-safety.d.ts +1 -3
- package/dist/extensions/url-safety.js +23 -23
- package/dist/file-upload/actions/upload-image.d.ts +5 -5
- package/dist/file-upload/index.d.ts +1 -1
- package/dist/file-upload/index.js +1 -1
- package/dist/file-upload/s3-client.d.ts +1 -0
- package/dist/file-upload/s3-client.js +70 -13
- package/dist/file-upload/s3.d.ts +9 -0
- package/dist/file-upload/s3.js +14 -1
- package/dist/framework-package-versions.json +8 -8
- package/dist/jobs/run-history.js +10 -3
- package/dist/jobs/scheduler.js +5 -36
- package/dist/localization/core-messages/supplemental/ar-SA.js +35 -0
- package/dist/localization/core-messages/supplemental/de-DE.js +35 -0
- package/dist/localization/core-messages/supplemental/en-US.js +35 -0
- package/dist/localization/core-messages/supplemental/es-ES.js +35 -0
- package/dist/localization/core-messages/supplemental/fr-FR.js +35 -0
- package/dist/localization/core-messages/supplemental/hi-IN.js +35 -0
- package/dist/localization/core-messages/supplemental/ja-JP.js +35 -0
- package/dist/localization/core-messages/supplemental/ko-KR.js +35 -0
- package/dist/localization/core-messages/supplemental/pt-BR.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-CN.js +35 -0
- package/dist/localization/core-messages/supplemental/zh-TW.js +35 -0
- package/dist/localization/default-messages.d.ts +35 -0
- package/dist/localization/default-messages.js +35 -0
- package/dist/localization/mcp-settings-messages.d.ts +35 -0
- package/dist/mcp/actions/list-mcp-client-connections.d.ts +8 -0
- package/dist/mcp/actions/list-mcp-client-connections.js +39 -0
- package/dist/mcp/browser-tab-target.d.ts +16 -0
- package/dist/mcp/browser-tab-target.js +58 -0
- package/dist/mcp/build-server.d.ts +1 -0
- package/dist/mcp/build-server.js +38 -7
- package/dist/mcp/call-attribution.d.ts +4 -1
- package/dist/mcp/call-attribution.js +3 -1
- package/dist/mcp/client-connections.d.ts +6 -0
- package/dist/mcp/client-connections.js +91 -0
- package/dist/mcp/connect-route.js +12 -2
- package/dist/mcp/connect-store.d.ts +3 -0
- package/dist/mcp/connect-store.js +10 -1
- package/dist/mcp/direct-action-call.js +1 -0
- package/dist/mcp/oauth-store.d.ts +6 -0
- package/dist/mcp/oauth-store.js +23 -0
- package/dist/mcp/oauth-token.d.ts +1 -0
- package/dist/mcp/oauth-token.js +1 -0
- package/dist/mcp/server.js +9 -20
- package/dist/mcp-client/config.d.ts +2 -0
- package/dist/mcp-client/manager.js +23 -0
- package/dist/mcp-client/workspace-servers.js +42 -10
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/org/handlers.d.ts +1 -1
- package/dist/org/handlers.js +21 -17
- package/dist/provider-api/actions/custom-provider-registration.d.ts +12 -12
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.js +17 -36
- package/dist/review/actions/create-review-comment.d.ts +7 -0
- package/dist/review/actions/create-review-comment.js +29 -3
- package/dist/review/actions/delete-review-comment.js +2 -2
- package/dist/review/actions/list-review-comments.d.ts +3 -0
- package/dist/review/actions/list-review-comments.js +41 -17
- package/dist/review/actions/react-to-review-comment.js +2 -2
- package/dist/review/actions/reply-review-comment.d.ts +4 -0
- package/dist/review/actions/reply-review-comment.js +15 -6
- package/dist/review/actions/update-review-comment-anchor.d.ts +6 -0
- package/dist/review/actions/update-review-comment-anchor.js +39 -10
- package/dist/review/actions/update-review-comment.d.ts +3 -0
- package/dist/review/actions/update-review-comment.js +28 -3
- package/dist/review/errors.d.ts +33 -0
- package/dist/review/errors.js +51 -0
- package/dist/review/index.d.ts +5 -4
- package/dist/review/index.js +3 -2
- package/dist/review/registry.d.ts +51 -1
- package/dist/review/registry.js +117 -0
- package/dist/review/store.d.ts +59 -14
- package/dist/review/store.js +622 -101
- package/dist/review/suggestions/actions.js +65 -10
- package/dist/review/suggestions/actor-kind.d.ts +5 -0
- package/dist/review/suggestions/actor-kind.js +20 -0
- package/dist/review/suggestions/store.js +16 -2
- package/dist/review/suggestions/types.d.ts +13 -0
- package/dist/review/types.d.ts +89 -0
- package/dist/scripts/db/dev-query-proxy.d.ts +2 -0
- package/dist/scripts/db/dev-query-proxy.js +4 -1
- package/dist/scripts/db/query.js +3 -0
- package/dist/scripts/dev/shell.js +4 -0
- package/dist/scripts/dev-session.d.ts +31 -0
- package/dist/scripts/dev-session.js +58 -0
- package/dist/scripts/resources/list.js +53 -4
- package/dist/scripts/runner.js +22 -4
- package/dist/search/query.d.ts +11 -5
- package/dist/search/query.js +52 -15
- package/dist/search/tokenize.d.ts +1 -0
- package/dist/search/tokenize.js +3 -0
- package/dist/secrets/api-keys.d.ts +4 -0
- package/dist/secrets/api-keys.js +5 -0
- package/dist/secrets/register.d.ts +2 -0
- package/dist/secrets/routes.d.ts +2 -2
- package/dist/secrets/routes.js +85 -6
- package/dist/secrets/storage.d.ts +2 -0
- package/dist/secrets/storage.js +34 -9
- package/dist/secrets/substitution.d.ts +3 -2
- package/dist/secrets/substitution.js +56 -7
- package/dist/server/action-discovery.js +5 -0
- package/dist/server/action-routes.js +8 -6
- package/dist/server/agent-chat/prompt-resources.js +117 -79
- package/dist/server/agent-chat/script-entries.js +15 -8
- package/dist/server/agent-chat-plugin.js +9 -4
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-engine-ollama-models-route.d.ts +1 -1
- package/dist/server/agents-bundle.d.ts +3 -1
- package/dist/server/agents-bundle.js +4 -4
- package/dist/server/csrf.js +1 -0
- package/dist/server/dev-action-bridge.d.ts +2 -0
- package/dist/server/dev-action-bridge.js +34 -5
- package/dist/server/first-touch-route.d.ts +1 -1
- package/dist/server/http-response-telemetry.js +4 -0
- package/dist/server/loopback.js +3 -1
- package/dist/server/request-context.d.ts +26 -0
- package/dist/shared/app-state-routes.d.ts +7 -0
- package/dist/shared/app-state-routes.js +7 -0
- package/dist/shared/mcp-client-connections.d.ts +41 -0
- package/dist/shared/mcp-client-connections.js +18 -0
- package/dist/shared/mcp-connect-content.d.ts +18 -0
- package/dist/shared/mcp-connect-content.js +200 -172
- package/dist/shared/runtime-config.js +5 -1
- package/dist/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/context-awareness/SKILL.md +13 -0
- package/dist/tracking/providers.d.ts +7 -0
- package/dist/tracking/providers.js +39 -17
- package/dist/triggers/condition-evaluator.d.ts +2 -0
- package/dist/triggers/condition-evaluator.js +93 -3
- package/dist/triggers/dispatcher.js +24 -6
- package/docs/AGENTS.md +16 -16
- package/docs/content/create-with-an-agent.mdx +10 -4
- package/docs/content/durable-resume.mdx +11 -2
- package/docs/content/toolkit-comments-review.mdx +54 -1
- package/package.json +5 -4
- package/src/templates/chat/.agents/skills/context-awareness/SKILL.md +13 -0
- package/src/templates/default/.agents/skills/context-awareness/SKILL.md +13 -0
- 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. */
|
package/dist/mcp/build-server.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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(
|
|
3269
|
-
fullSurface: !!(
|
|
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,
|
|
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
|
|
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,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:
|
|
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:
|
|
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>;
|
package/dist/mcp/oauth-store.js
CHANGED
|
@@ -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[];
|
package/dist/mcp/oauth-token.js
CHANGED
|
@@ -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,
|