@agent-native/core 0.105.0 → 0.106.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +39 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/application-state/index.ts +2 -0
- package/corpus/core/src/application-state/script-helpers.ts +12 -0
- package/corpus/core/src/application-state/store.ts +38 -0
- package/corpus/core/src/client/AssistantChat.tsx +4 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
- package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
- package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
- package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
- package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
- package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
- package/corpus/core/src/client/review/ReviewCommentComposer.tsx +10 -3
- package/corpus/core/src/client/use-chat-models.ts +10 -2
- package/corpus/core/src/connections/catalog.ts +107 -6
- package/corpus/core/src/localization/default-messages.ts +2 -0
- package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
- package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
- package/corpus/core/src/mcp-client/app-api.ts +136 -0
- package/corpus/core/src/mcp-client/index.ts +21 -0
- package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
- package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
- package/corpus/core/src/mcp-client/remote-store.ts +93 -118
- package/corpus/core/src/mcp-client/remote-url.ts +97 -0
- package/corpus/core/src/mcp-client/routes.ts +11 -0
- package/corpus/core/src/oauth-tokens/store.ts +10 -4
- package/corpus/core/src/provider-api/index.ts +690 -55
- package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
- package/corpus/core/src/server/action-discovery.ts +2 -0
- package/corpus/core/src/server/core-routes-plugin.ts +9 -1
- package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
- package/corpus/core/src/templates/default/AGENTS.md +9 -7
- package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
- package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
- package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
- package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
- package/corpus/templates/analytics/server/lib/jira.ts +14 -42
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
- package/corpus/templates/design/.agents/skills/visual-edit/SKILL.md +22 -0
- package/corpus/templates/design/AGENTS.md +16 -0
- package/corpus/templates/design/actions/cancel-node-rewrite-request.ts +54 -0
- package/corpus/templates/design/actions/propose-node-rewrite.ts +290 -0
- package/corpus/templates/design/actions/resolve-node-rewrite.ts +257 -0
- package/corpus/templates/design/actions/view-screen.ts +98 -2
- package/corpus/templates/design/app/components/design/CanvasContextMenu.tsx +71 -1
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +19 -0
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +33 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +194 -0
- package/corpus/templates/design/app/components/design/bridge/hit-test.bridge.ts +21 -4
- package/corpus/templates/design/app/components/design/design-canvas/iframe-events.ts +18 -0
- package/corpus/templates/design/app/components/design/multi-screen/types.ts +4 -0
- package/corpus/templates/design/app/components/visual-editor/NodeRewriteProposal.tsx +638 -0
- package/corpus/templates/design/app/components/visual-editor/ReviewCanvasPins.tsx +535 -54
- package/corpus/templates/design/app/components/visual-editor/index.ts +5 -1
- package/corpus/templates/design/app/components/visual-editor/review-canvas-state.ts +21 -3
- package/corpus/templates/design/app/i18n/ar-SA.ts +35 -0
- package/corpus/templates/design/app/i18n/de-DE.ts +35 -0
- package/corpus/templates/design/app/i18n/es-ES.ts +35 -0
- package/corpus/templates/design/app/i18n/fr-FR.ts +35 -0
- package/corpus/templates/design/app/i18n/hi-IN.ts +35 -0
- package/corpus/templates/design/app/i18n/ja-JP.ts +35 -0
- package/corpus/templates/design/app/i18n/ko-KR.ts +35 -0
- package/corpus/templates/design/app/i18n/pt-BR.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-CN.ts +35 -0
- package/corpus/templates/design/app/i18n/zh-TW.ts +32 -0
- package/corpus/templates/design/app/i18n-data.ts +32 -0
- package/corpus/templates/design/app/lib/node-reprompt.ts +110 -0
- package/corpus/templates/design/changelog/2026-07-16-design-editing-and-review-are-now-more-predictable.md +6 -0
- package/corpus/templates/design/server/lib/reprompt-action-guard.ts +107 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +9 -1
- package/corpus/templates/design/shared/code-layer.ts +15 -0
- package/corpus/templates/design/shared/node-rewrite.ts +213 -0
- package/corpus/templates/design/shared/review-anchor.ts +26 -3
- package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
- package/dist/application-state/index.d.ts +2 -2
- package/dist/application-state/index.d.ts.map +1 -1
- package/dist/application-state/index.js +2 -2
- package/dist/application-state/index.js.map +1 -1
- package/dist/application-state/script-helpers.d.ts +1 -0
- package/dist/application-state/script-helpers.d.ts.map +1 -1
- package/dist/application-state/script-helpers.js +7 -1
- package/dist/application-state/script-helpers.js.map +1 -1
- package/dist/application-state/store.d.ts +1 -0
- package/dist/application-state/store.d.ts.map +1 -1
- package/dist/application-state/store.js +28 -0
- package/dist/application-state/store.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +2 -0
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +5 -2
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/composer/PromptComposer.d.ts +2 -0
- package/dist/client/composer/PromptComposer.d.ts.map +1 -1
- package/dist/client/composer/PromptComposer.js +5 -2
- package/dist/client/composer/PromptComposer.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -1
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +13 -5
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
- package/dist/client/resources/McpIntegrationDialog.js +58 -14
- package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
- package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
- package/dist/client/resources/mcp-integration-catalog.js +13 -0
- package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
- package/dist/client/resources/use-mcp-servers.d.ts +1 -0
- package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
- package/dist/client/resources/use-mcp-servers.js.map +1 -1
- package/dist/client/review/ReviewCommentComposer.d.ts +4 -1
- package/dist/client/review/ReviewCommentComposer.d.ts.map +1 -1
- package/dist/client/review/ReviewCommentComposer.js +3 -3
- package/dist/client/review/ReviewCommentComposer.js.map +1 -1
- package/dist/client/use-chat-models.d.ts +1 -0
- package/dist/client/use-chat-models.d.ts.map +1 -1
- package/dist/client/use-chat-models.js +8 -2
- package/dist/client/use-chat-models.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/connections/catalog.d.ts +78 -7
- package/dist/connections/catalog.d.ts.map +1 -1
- package/dist/connections/catalog.js +97 -6
- package/dist/connections/catalog.js.map +1 -1
- package/dist/localization/default-messages.d.ts +2 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +2 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
- package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
- package/dist/mcp/actions/call-mcp-tool.js +15 -0
- package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
- package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
- package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
- package/dist/mcp/actions/list-mcp-tools.js +16 -0
- package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
- package/dist/mcp-client/app-api.d.ts +34 -0
- package/dist/mcp-client/app-api.d.ts.map +1 -0
- package/dist/mcp-client/app-api.js +94 -0
- package/dist/mcp-client/app-api.js.map +1 -0
- package/dist/mcp-client/index.d.ts +3 -1
- package/dist/mcp-client/index.d.ts.map +1 -1
- package/dist/mcp-client/index.js +3 -1
- package/dist/mcp-client/index.js.map +1 -1
- package/dist/mcp-client/oauth-client.d.ts +113 -0
- package/dist/mcp-client/oauth-client.d.ts.map +1 -0
- package/dist/mcp-client/oauth-client.js +316 -0
- package/dist/mcp-client/oauth-client.js.map +1 -0
- package/dist/mcp-client/oauth-routes.d.ts +25 -0
- package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
- package/dist/mcp-client/oauth-routes.js +218 -0
- package/dist/mcp-client/oauth-routes.js.map +1 -0
- package/dist/mcp-client/remote-store.d.ts +21 -6
- package/dist/mcp-client/remote-store.d.ts.map +1 -1
- package/dist/mcp-client/remote-store.js +72 -124
- package/dist/mcp-client/remote-store.js.map +1 -1
- package/dist/mcp-client/remote-url.d.ts +8 -0
- package/dist/mcp-client/remote-url.d.ts.map +1 -0
- package/dist/mcp-client/remote-url.js +103 -0
- package/dist/mcp-client/remote-url.js.map +1 -0
- package/dist/mcp-client/routes.d.ts +1 -0
- package/dist/mcp-client/routes.d.ts.map +1 -1
- package/dist/mcp-client/routes.js +9 -0
- package/dist/mcp-client/routes.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/oauth-tokens/store.d.ts +2 -2
- package/dist/oauth-tokens/store.d.ts.map +1 -1
- package/dist/oauth-tokens/store.js +10 -6
- package/dist/oauth-tokens/store.js.map +1 -1
- package/dist/observability/routes.d.ts +5 -5
- package/dist/provider-api/index.d.ts +14 -0
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +499 -61
- package/dist/provider-api/index.js.map +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +28 -0
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +2 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +9 -1
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/workspace-provider-oauth.d.ts +10 -3
- package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
- package/dist/server/workspace-provider-oauth.js +325 -43
- package/dist/server/workspace-provider-oauth.js.map +1 -1
- package/dist/templates/default/AGENTS.md +9 -7
- package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
- package/package.json +2 -2
- package/src/templates/default/AGENTS.md +9 -7
- package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 client support for remote MCP servers.
|
|
3
|
+
*
|
|
4
|
+
* MCP servers advertise their OAuth endpoints through the standard protected
|
|
5
|
+
* resource and authorization-server metadata documents. The SDK handles the
|
|
6
|
+
* protocol details; this module owns the framework-specific encrypted storage
|
|
7
|
+
* and refresh boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
auth,
|
|
12
|
+
refreshAuthorization,
|
|
13
|
+
type OAuthClientProvider,
|
|
14
|
+
} from "@modelcontextprotocol/sdk/client/auth.js";
|
|
15
|
+
import type {
|
|
16
|
+
AuthorizationServerMetadata,
|
|
17
|
+
OAuthClientInformationMixed,
|
|
18
|
+
OAuthClientMetadata,
|
|
19
|
+
OAuthProtectedResourceMetadata,
|
|
20
|
+
OAuthTokens,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
deleteOAuthTokens,
|
|
25
|
+
getOAuthTokens,
|
|
26
|
+
saveOAuthTokens,
|
|
27
|
+
} from "../oauth-tokens/store.js";
|
|
28
|
+
import { validateRemoteUrl } from "./remote-url.js";
|
|
29
|
+
|
|
30
|
+
const TOKEN_EXPIRY_SKEW_MS = 60_000;
|
|
31
|
+
const MAX_OAUTH_REDIRECTS = 5;
|
|
32
|
+
|
|
33
|
+
type GuardedFetch = (
|
|
34
|
+
url: string | URL,
|
|
35
|
+
init?: RequestInit,
|
|
36
|
+
) => Promise<Response>;
|
|
37
|
+
|
|
38
|
+
function checkedRemoteUrl(value: string | URL, label: string): URL {
|
|
39
|
+
const result = validateRemoteUrl(String(value));
|
|
40
|
+
if (!result.ok || !result.url) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`MCP OAuth ${label} is not allowed: ${result.error ?? "invalid URL"}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return result.url;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function guardedOAuthFetch(): GuardedFetch {
|
|
49
|
+
return async (url, init) => {
|
|
50
|
+
let currentUrl = checkedRemoteUrl(url, "request");
|
|
51
|
+
let currentInit: RequestInit = { ...init, redirect: "manual" };
|
|
52
|
+
|
|
53
|
+
for (let redirectCount = 0; ; redirectCount += 1) {
|
|
54
|
+
const response = await fetch(currentUrl, currentInit);
|
|
55
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) {
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
if (redirectCount >= MAX_OAUTH_REDIRECTS) {
|
|
59
|
+
throw new Error("MCP OAuth redirect limit exceeded.");
|
|
60
|
+
}
|
|
61
|
+
const location = response.headers.get("location");
|
|
62
|
+
if (!location) throw new Error("MCP OAuth redirect is missing a target.");
|
|
63
|
+
|
|
64
|
+
let nextUrl: URL;
|
|
65
|
+
try {
|
|
66
|
+
nextUrl = checkedRemoteUrl(
|
|
67
|
+
new URL(location, currentUrl),
|
|
68
|
+
"redirect target",
|
|
69
|
+
);
|
|
70
|
+
} catch {
|
|
71
|
+
throw new Error("MCP OAuth redirect target is not allowed.");
|
|
72
|
+
}
|
|
73
|
+
await response.body?.cancel().catch(() => undefined);
|
|
74
|
+
|
|
75
|
+
const nextHeaders = new Headers(currentInit.headers);
|
|
76
|
+
if (nextUrl.origin !== currentUrl.origin) {
|
|
77
|
+
nextHeaders.delete("authorization");
|
|
78
|
+
nextHeaders.delete("cookie");
|
|
79
|
+
nextHeaders.delete("proxy-authorization");
|
|
80
|
+
}
|
|
81
|
+
const nextInit: RequestInit = {
|
|
82
|
+
...currentInit,
|
|
83
|
+
headers: nextHeaders,
|
|
84
|
+
};
|
|
85
|
+
if (
|
|
86
|
+
response.status === 303 ||
|
|
87
|
+
((response.status === 301 || response.status === 302) &&
|
|
88
|
+
(currentInit.method ?? "GET").toUpperCase() === "POST")
|
|
89
|
+
) {
|
|
90
|
+
nextInit.method = "GET";
|
|
91
|
+
delete nextInit.body;
|
|
92
|
+
nextHeaders.delete("content-length");
|
|
93
|
+
nextHeaders.delete("content-type");
|
|
94
|
+
}
|
|
95
|
+
currentUrl = nextUrl;
|
|
96
|
+
currentInit = nextInit;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function validateDiscoveryUrls(state: {
|
|
102
|
+
authorizationServerUrl: string;
|
|
103
|
+
authorizationServerMetadata?: AuthorizationServerMetadata;
|
|
104
|
+
resourceMetadata?: OAuthProtectedResourceMetadata;
|
|
105
|
+
resourceMetadataUrl?: string;
|
|
106
|
+
}): void {
|
|
107
|
+
checkedRemoteUrl(state.authorizationServerUrl, "authorization server");
|
|
108
|
+
if (state.resourceMetadataUrl) {
|
|
109
|
+
checkedRemoteUrl(state.resourceMetadataUrl, "resource metadata");
|
|
110
|
+
}
|
|
111
|
+
for (const url of state.resourceMetadata?.authorization_servers ?? []) {
|
|
112
|
+
checkedRemoteUrl(url, "discovered authorization server");
|
|
113
|
+
}
|
|
114
|
+
for (const field of [
|
|
115
|
+
"authorization_endpoint",
|
|
116
|
+
"token_endpoint",
|
|
117
|
+
"registration_endpoint",
|
|
118
|
+
"introspection_endpoint",
|
|
119
|
+
"revocation_endpoint",
|
|
120
|
+
] as const) {
|
|
121
|
+
const url = (
|
|
122
|
+
state.authorizationServerMetadata as Record<string, unknown> | undefined
|
|
123
|
+
)?.[field];
|
|
124
|
+
if (typeof url === "string") {
|
|
125
|
+
checkedRemoteUrl(url, `OAuth ${field}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function credentialOwner(options: { scope: "user" | "org"; scopeId: string }) {
|
|
131
|
+
return `${options.scope}:${options.scopeId}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface McpOAuthDiscoveryState {
|
|
135
|
+
authorizationServerUrl: string;
|
|
136
|
+
authorizationServerMetadata?: AuthorizationServerMetadata;
|
|
137
|
+
resourceMetadata?: OAuthProtectedResourceMetadata;
|
|
138
|
+
resourceMetadataUrl?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface McpOAuthCredentialBundle {
|
|
142
|
+
serverUrl: string;
|
|
143
|
+
clientInformation: OAuthClientInformationMixed;
|
|
144
|
+
discoveryState?: McpOAuthDiscoveryState;
|
|
145
|
+
tokens: OAuthTokens;
|
|
146
|
+
tokenExpiresAt?: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface McpOAuthStartResult {
|
|
150
|
+
authorizationUrl: URL;
|
|
151
|
+
codeVerifier: string;
|
|
152
|
+
state: string;
|
|
153
|
+
clientInformation: OAuthClientInformationMixed;
|
|
154
|
+
discoveryState?: McpOAuthDiscoveryState;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface McpOAuthCallbackResult {
|
|
158
|
+
credentials: McpOAuthCredentialBundle;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface McpOAuthProviderOptions {
|
|
162
|
+
serverUrl: string;
|
|
163
|
+
redirectUrl: string;
|
|
164
|
+
state: string;
|
|
165
|
+
clientInformation?: OAuthClientInformationMixed;
|
|
166
|
+
codeVerifier?: string;
|
|
167
|
+
discoveryState?: McpOAuthDiscoveryState;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A small adapter around the MCP SDK's OAuth provider interface. The route
|
|
172
|
+
* stores the adapter's state in an encrypted, short-lived browser cookie; the
|
|
173
|
+
* durable credential bundle is written only after the callback succeeds.
|
|
174
|
+
*/
|
|
175
|
+
export class McpOAuthClientProvider implements OAuthClientProvider {
|
|
176
|
+
private readonly redirectUrlValue: string;
|
|
177
|
+
private readonly stateValue: string;
|
|
178
|
+
private readonly metadata: OAuthClientMetadata;
|
|
179
|
+
private clientInfo?: OAuthClientInformationMixed;
|
|
180
|
+
private savedTokens?: OAuthTokens;
|
|
181
|
+
private savedCodeVerifier?: string;
|
|
182
|
+
private savedDiscovery?: McpOAuthDiscoveryState;
|
|
183
|
+
private authorizationUrl?: URL;
|
|
184
|
+
|
|
185
|
+
constructor(options: McpOAuthProviderOptions) {
|
|
186
|
+
this.redirectUrlValue = options.redirectUrl;
|
|
187
|
+
this.stateValue = options.state;
|
|
188
|
+
this.clientInfo = options.clientInformation;
|
|
189
|
+
this.savedCodeVerifier = options.codeVerifier;
|
|
190
|
+
this.savedDiscovery = options.discoveryState;
|
|
191
|
+
this.metadata = {
|
|
192
|
+
redirect_uris: [options.redirectUrl],
|
|
193
|
+
token_endpoint_auth_method: "none",
|
|
194
|
+
grant_types: ["authorization_code"],
|
|
195
|
+
response_types: ["code"],
|
|
196
|
+
client_name: "Agent Native MCP connector",
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
get redirectUrl(): string {
|
|
201
|
+
return this.redirectUrlValue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
get clientMetadata(): OAuthClientMetadata {
|
|
205
|
+
return this.metadata;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
state(): string {
|
|
209
|
+
return this.stateValue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
clientInformation(): OAuthClientInformationMixed | undefined {
|
|
213
|
+
return this.clientInfo;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
saveClientInformation(info: OAuthClientInformationMixed): void {
|
|
217
|
+
this.clientInfo = info;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tokens(): OAuthTokens | undefined {
|
|
221
|
+
return this.savedTokens;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
saveTokens(tokens: OAuthTokens): void {
|
|
225
|
+
this.savedTokens = tokens;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
redirectToAuthorization(authorizationUrl: URL): void {
|
|
229
|
+
this.authorizationUrl = checkedRemoteUrl(
|
|
230
|
+
authorizationUrl,
|
|
231
|
+
"authorization redirect",
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
saveCodeVerifier(codeVerifier: string): void {
|
|
236
|
+
this.savedCodeVerifier = codeVerifier;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
codeVerifier(): string {
|
|
240
|
+
if (!this.savedCodeVerifier) {
|
|
241
|
+
throw new Error("MCP OAuth code verifier is missing");
|
|
242
|
+
}
|
|
243
|
+
return this.savedCodeVerifier;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
saveDiscoveryState(state: {
|
|
247
|
+
authorizationServerUrl: string;
|
|
248
|
+
authorizationServerMetadata?: AuthorizationServerMetadata;
|
|
249
|
+
resourceMetadata?: OAuthProtectedResourceMetadata;
|
|
250
|
+
resourceMetadataUrl?: string;
|
|
251
|
+
}): void {
|
|
252
|
+
validateDiscoveryUrls(state);
|
|
253
|
+
this.savedDiscovery = state;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
discoveryState(): McpOAuthDiscoveryState | undefined {
|
|
257
|
+
return this.savedDiscovery;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
get authorizationRedirect(): URL | undefined {
|
|
261
|
+
return this.authorizationUrl;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
get savedCodeVerifierValue(): string | undefined {
|
|
265
|
+
return this.savedCodeVerifier;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get savedTokensValue(): OAuthTokens | undefined {
|
|
269
|
+
return this.savedTokens;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
get savedClientInformation(): OAuthClientInformationMixed | undefined {
|
|
273
|
+
return this.clientInfo;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export async function startMcpOAuthAuthorization(
|
|
278
|
+
options: McpOAuthProviderOptions & { scope?: string },
|
|
279
|
+
): Promise<McpOAuthStartResult> {
|
|
280
|
+
checkedRemoteUrl(options.serverUrl, "server");
|
|
281
|
+
const provider = new McpOAuthClientProvider(options);
|
|
282
|
+
const result = await auth(provider, {
|
|
283
|
+
serverUrl: options.serverUrl,
|
|
284
|
+
scope: options.scope,
|
|
285
|
+
fetchFn: guardedOAuthFetch(),
|
|
286
|
+
});
|
|
287
|
+
if (result !== "REDIRECT" || !provider.authorizationRedirect) {
|
|
288
|
+
throw new Error("MCP server did not start an interactive OAuth flow");
|
|
289
|
+
}
|
|
290
|
+
const clientInformation = provider.savedClientInformation;
|
|
291
|
+
if (!clientInformation) {
|
|
292
|
+
throw new Error("MCP OAuth client registration did not complete");
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
authorizationUrl: provider.authorizationRedirect,
|
|
296
|
+
codeVerifier: provider.savedCodeVerifierValue ?? provider.codeVerifier(),
|
|
297
|
+
state: options.state,
|
|
298
|
+
clientInformation,
|
|
299
|
+
discoveryState: provider.discoveryState(),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export async function finishMcpOAuthAuthorization(
|
|
304
|
+
options: McpOAuthProviderOptions & { authorizationCode: string },
|
|
305
|
+
): Promise<McpOAuthCallbackResult> {
|
|
306
|
+
checkedRemoteUrl(options.serverUrl, "server");
|
|
307
|
+
const provider = new McpOAuthClientProvider(options);
|
|
308
|
+
const result = await auth(provider, {
|
|
309
|
+
serverUrl: options.serverUrl,
|
|
310
|
+
authorizationCode: options.authorizationCode,
|
|
311
|
+
fetchFn: guardedOAuthFetch(),
|
|
312
|
+
});
|
|
313
|
+
if (result !== "AUTHORIZED" || !provider.savedTokensValue) {
|
|
314
|
+
throw new Error("MCP OAuth token exchange did not complete");
|
|
315
|
+
}
|
|
316
|
+
const tokens = provider.savedTokensValue;
|
|
317
|
+
return {
|
|
318
|
+
credentials: {
|
|
319
|
+
serverUrl: options.serverUrl,
|
|
320
|
+
clientInformation: provider.savedClientInformation!,
|
|
321
|
+
discoveryState: provider.discoveryState(),
|
|
322
|
+
tokens,
|
|
323
|
+
tokenExpiresAt: tokenExpiresAt(tokens),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function tokenExpiresAt(tokens: OAuthTokens): number | undefined {
|
|
329
|
+
const expiresIn = Number(tokens.expires_in);
|
|
330
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) return undefined;
|
|
331
|
+
return Date.now() + expiresIn * 1_000;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export async function saveMcpOAuthCredentials(options: {
|
|
335
|
+
key: string;
|
|
336
|
+
scope: "user" | "org";
|
|
337
|
+
scopeId: string;
|
|
338
|
+
credentials: McpOAuthCredentialBundle;
|
|
339
|
+
}): Promise<void> {
|
|
340
|
+
checkedRemoteUrl(options.credentials.serverUrl, "server");
|
|
341
|
+
if (options.credentials.discoveryState) {
|
|
342
|
+
validateDiscoveryUrls(options.credentials.discoveryState);
|
|
343
|
+
}
|
|
344
|
+
await saveOAuthTokens(
|
|
345
|
+
"mcp",
|
|
346
|
+
options.key,
|
|
347
|
+
options.credentials as unknown as Record<string, unknown>,
|
|
348
|
+
`${options.scope}:${options.scopeId}`,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export async function readMcpOAuthCredentials(options: {
|
|
353
|
+
key: string;
|
|
354
|
+
scope: "user" | "org";
|
|
355
|
+
scopeId: string;
|
|
356
|
+
}): Promise<McpOAuthCredentialBundle | null> {
|
|
357
|
+
const stored = await getOAuthTokens(
|
|
358
|
+
"mcp",
|
|
359
|
+
options.key,
|
|
360
|
+
credentialOwner(options),
|
|
361
|
+
);
|
|
362
|
+
if (!stored) return null;
|
|
363
|
+
const parsed = stored as Partial<McpOAuthCredentialBundle>;
|
|
364
|
+
if (
|
|
365
|
+
typeof parsed.serverUrl !== "string" ||
|
|
366
|
+
!parsed.clientInformation ||
|
|
367
|
+
!parsed.tokens ||
|
|
368
|
+
typeof parsed.tokens.access_token !== "string"
|
|
369
|
+
) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
if (!validateRemoteUrl(parsed.serverUrl).ok) return null;
|
|
373
|
+
if (parsed.discoveryState) {
|
|
374
|
+
try {
|
|
375
|
+
validateDiscoveryUrls(parsed.discoveryState);
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return parsed as McpOAuthCredentialBundle;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export async function deleteMcpOAuthCredentials(options: {
|
|
384
|
+
key: string;
|
|
385
|
+
scope: "user" | "org";
|
|
386
|
+
scopeId: string;
|
|
387
|
+
}): Promise<boolean> {
|
|
388
|
+
return (
|
|
389
|
+
(await deleteOAuthTokens("mcp", options.key, credentialOwner(options))) > 0
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Resolve an access token for the MCP manager. Refreshing happens only when a
|
|
395
|
+
* token is near expiry, so ordinary manager reconfiguration does not perform
|
|
396
|
+
* a network request for every connector.
|
|
397
|
+
*/
|
|
398
|
+
export async function getMcpOAuthAccessToken(options: {
|
|
399
|
+
key: string;
|
|
400
|
+
scope: "user" | "org";
|
|
401
|
+
scopeId: string;
|
|
402
|
+
serverUrl: string;
|
|
403
|
+
}): Promise<string | null> {
|
|
404
|
+
if (!validateRemoteUrl(options.serverUrl).ok) return null;
|
|
405
|
+
const credentials = await readMcpOAuthCredentials(options);
|
|
406
|
+
if (!credentials || credentials.serverUrl !== options.serverUrl) return null;
|
|
407
|
+
|
|
408
|
+
const accessToken = credentials.tokens.access_token;
|
|
409
|
+
if (
|
|
410
|
+
typeof credentials.tokenExpiresAt !== "number" ||
|
|
411
|
+
credentials.tokenExpiresAt - Date.now() > TOKEN_EXPIRY_SKEW_MS
|
|
412
|
+
) {
|
|
413
|
+
return accessToken;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const refreshToken = credentials.tokens.refresh_token;
|
|
417
|
+
const discovery = credentials.discoveryState;
|
|
418
|
+
if (!refreshToken || !discovery?.authorizationServerUrl) {
|
|
419
|
+
return accessToken;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const resource = discovery.resourceMetadata?.resource
|
|
424
|
+
? checkedRemoteUrl(discovery.resourceMetadata.resource, "resource")
|
|
425
|
+
: undefined;
|
|
426
|
+
const authorizationServerUrl = checkedRemoteUrl(
|
|
427
|
+
discovery.authorizationServerUrl,
|
|
428
|
+
"authorization server",
|
|
429
|
+
);
|
|
430
|
+
const refreshed = await refreshAuthorization(authorizationServerUrl, {
|
|
431
|
+
metadata: discovery.authorizationServerMetadata,
|
|
432
|
+
clientInformation: credentials.clientInformation,
|
|
433
|
+
refreshToken,
|
|
434
|
+
resource,
|
|
435
|
+
fetchFn: guardedOAuthFetch(),
|
|
436
|
+
});
|
|
437
|
+
const nextTokens: OAuthTokens = {
|
|
438
|
+
...credentials.tokens,
|
|
439
|
+
...refreshed,
|
|
440
|
+
...(refreshed.refresh_token
|
|
441
|
+
? { refresh_token: refreshed.refresh_token }
|
|
442
|
+
: credentials.tokens.refresh_token
|
|
443
|
+
? { refresh_token: credentials.tokens.refresh_token }
|
|
444
|
+
: {}),
|
|
445
|
+
};
|
|
446
|
+
const next: McpOAuthCredentialBundle = {
|
|
447
|
+
...credentials,
|
|
448
|
+
tokens: nextTokens,
|
|
449
|
+
tokenExpiresAt: tokenExpiresAt(nextTokens),
|
|
450
|
+
};
|
|
451
|
+
await saveMcpOAuthCredentials({
|
|
452
|
+
...options,
|
|
453
|
+
credentials: next,
|
|
454
|
+
});
|
|
455
|
+
return nextTokens.access_token;
|
|
456
|
+
} catch {
|
|
457
|
+
// Keep the old token so the MCP request can return the provider's normal
|
|
458
|
+
// auth error. A single expired connector must not remove every server from
|
|
459
|
+
// the process-wide manager during a config refresh.
|
|
460
|
+
return accessToken;
|
|
461
|
+
}
|
|
462
|
+
}
|