@agent-native/core 0.105.0 → 0.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +33 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/AssistantChat.tsx +4 -0
  5. package/corpus/core/src/client/MultiTabAssistantChat.tsx +5 -1
  6. package/corpus/core/src/client/composer/PromptComposer.tsx +8 -0
  7. package/corpus/core/src/client/composer/TiptapComposer.tsx +38 -1
  8. package/corpus/core/src/client/resources/McpIntegrationDialog.tsx +107 -32
  9. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +33 -0
  10. package/corpus/core/src/client/resources/use-mcp-servers.ts +1 -0
  11. package/corpus/core/src/client/use-chat-models.ts +10 -2
  12. package/corpus/core/src/connections/catalog.ts +107 -6
  13. package/corpus/core/src/localization/default-messages.ts +2 -0
  14. package/corpus/core/src/mcp/actions/call-mcp-tool.ts +18 -0
  15. package/corpus/core/src/mcp/actions/list-mcp-tools.ts +18 -0
  16. package/corpus/core/src/mcp-client/app-api.ts +136 -0
  17. package/corpus/core/src/mcp-client/index.ts +21 -0
  18. package/corpus/core/src/mcp-client/oauth-client.ts +462 -0
  19. package/corpus/core/src/mcp-client/oauth-routes.ts +307 -0
  20. package/corpus/core/src/mcp-client/remote-store.ts +93 -118
  21. package/corpus/core/src/mcp-client/remote-url.ts +97 -0
  22. package/corpus/core/src/mcp-client/routes.ts +11 -0
  23. package/corpus/core/src/oauth-tokens/store.ts +10 -4
  24. package/corpus/core/src/provider-api/index.ts +690 -55
  25. package/corpus/core/src/secrets/register-framework-secrets.ts +30 -0
  26. package/corpus/core/src/server/action-discovery.ts +2 -0
  27. package/corpus/core/src/server/core-routes-plugin.ts +9 -1
  28. package/corpus/core/src/server/workspace-provider-oauth.ts +448 -61
  29. package/corpus/core/src/templates/default/AGENTS.md +9 -7
  30. package/corpus/core/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  31. package/corpus/templates/analytics/app/lib/data-source-status.ts +2 -0
  32. package/corpus/templates/analytics/app/pages/DataSources.tsx +61 -0
  33. package/corpus/templates/analytics/server/handlers/jira.ts +7 -0
  34. package/corpus/templates/analytics/server/lib/jira.ts +14 -42
  35. package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +11 -0
  36. package/corpus/templates/dispatch/app/routes/integrations.tsx +57 -4
  37. package/dist/client/AssistantChat.d.ts +2 -0
  38. package/dist/client/AssistantChat.d.ts.map +1 -1
  39. package/dist/client/AssistantChat.js +2 -2
  40. package/dist/client/AssistantChat.js.map +1 -1
  41. package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
  42. package/dist/client/MultiTabAssistantChat.js +5 -2
  43. package/dist/client/MultiTabAssistantChat.js.map +1 -1
  44. package/dist/client/composer/PromptComposer.d.ts +2 -0
  45. package/dist/client/composer/PromptComposer.d.ts.map +1 -1
  46. package/dist/client/composer/PromptComposer.js +5 -2
  47. package/dist/client/composer/PromptComposer.js.map +1 -1
  48. package/dist/client/composer/TiptapComposer.d.ts +4 -1
  49. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  50. package/dist/client/composer/TiptapComposer.js +13 -5
  51. package/dist/client/composer/TiptapComposer.js.map +1 -1
  52. package/dist/client/resources/McpIntegrationDialog.d.ts.map +1 -1
  53. package/dist/client/resources/McpIntegrationDialog.js +58 -14
  54. package/dist/client/resources/McpIntegrationDialog.js.map +1 -1
  55. package/dist/client/resources/mcp-integration-catalog.d.ts +9 -0
  56. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  57. package/dist/client/resources/mcp-integration-catalog.js +13 -0
  58. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  59. package/dist/client/resources/use-mcp-servers.d.ts +1 -0
  60. package/dist/client/resources/use-mcp-servers.d.ts.map +1 -1
  61. package/dist/client/resources/use-mcp-servers.js.map +1 -1
  62. package/dist/client/use-chat-models.d.ts +1 -0
  63. package/dist/client/use-chat-models.d.ts.map +1 -1
  64. package/dist/client/use-chat-models.js +8 -2
  65. package/dist/client/use-chat-models.js.map +1 -1
  66. package/dist/collab/awareness.d.ts +2 -2
  67. package/dist/collab/awareness.d.ts.map +1 -1
  68. package/dist/connections/catalog.d.ts +78 -7
  69. package/dist/connections/catalog.d.ts.map +1 -1
  70. package/dist/connections/catalog.js +97 -6
  71. package/dist/connections/catalog.js.map +1 -1
  72. package/dist/localization/default-messages.d.ts +2 -0
  73. package/dist/localization/default-messages.d.ts.map +1 -1
  74. package/dist/localization/default-messages.js +2 -0
  75. package/dist/localization/default-messages.js.map +1 -1
  76. package/dist/mcp/actions/call-mcp-tool.d.ts +7 -0
  77. package/dist/mcp/actions/call-mcp-tool.d.ts.map +1 -0
  78. package/dist/mcp/actions/call-mcp-tool.js +15 -0
  79. package/dist/mcp/actions/call-mcp-tool.js.map +1 -0
  80. package/dist/mcp/actions/list-mcp-tools.d.ts +5 -0
  81. package/dist/mcp/actions/list-mcp-tools.d.ts.map +1 -0
  82. package/dist/mcp/actions/list-mcp-tools.js +16 -0
  83. package/dist/mcp/actions/list-mcp-tools.js.map +1 -0
  84. package/dist/mcp-client/app-api.d.ts +34 -0
  85. package/dist/mcp-client/app-api.d.ts.map +1 -0
  86. package/dist/mcp-client/app-api.js +94 -0
  87. package/dist/mcp-client/app-api.js.map +1 -0
  88. package/dist/mcp-client/index.d.ts +3 -1
  89. package/dist/mcp-client/index.d.ts.map +1 -1
  90. package/dist/mcp-client/index.js +3 -1
  91. package/dist/mcp-client/index.js.map +1 -1
  92. package/dist/mcp-client/oauth-client.d.ts +113 -0
  93. package/dist/mcp-client/oauth-client.d.ts.map +1 -0
  94. package/dist/mcp-client/oauth-client.js +316 -0
  95. package/dist/mcp-client/oauth-client.js.map +1 -0
  96. package/dist/mcp-client/oauth-routes.d.ts +25 -0
  97. package/dist/mcp-client/oauth-routes.d.ts.map +1 -0
  98. package/dist/mcp-client/oauth-routes.js +218 -0
  99. package/dist/mcp-client/oauth-routes.js.map +1 -0
  100. package/dist/mcp-client/remote-store.d.ts +21 -6
  101. package/dist/mcp-client/remote-store.d.ts.map +1 -1
  102. package/dist/mcp-client/remote-store.js +72 -124
  103. package/dist/mcp-client/remote-store.js.map +1 -1
  104. package/dist/mcp-client/remote-url.d.ts +8 -0
  105. package/dist/mcp-client/remote-url.d.ts.map +1 -0
  106. package/dist/mcp-client/remote-url.js +103 -0
  107. package/dist/mcp-client/remote-url.js.map +1 -0
  108. package/dist/mcp-client/routes.d.ts +1 -0
  109. package/dist/mcp-client/routes.d.ts.map +1 -1
  110. package/dist/mcp-client/routes.js +9 -0
  111. package/dist/mcp-client/routes.js.map +1 -1
  112. package/dist/oauth-tokens/store.d.ts +2 -2
  113. package/dist/oauth-tokens/store.d.ts.map +1 -1
  114. package/dist/oauth-tokens/store.js +10 -6
  115. package/dist/oauth-tokens/store.js.map +1 -1
  116. package/dist/provider-api/index.d.ts +14 -0
  117. package/dist/provider-api/index.d.ts.map +1 -1
  118. package/dist/provider-api/index.js +499 -61
  119. package/dist/provider-api/index.js.map +1 -1
  120. package/dist/resources/handlers.d.ts +1 -1
  121. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  122. package/dist/secrets/register-framework-secrets.js +28 -0
  123. package/dist/secrets/register-framework-secrets.js.map +1 -1
  124. package/dist/server/action-discovery.d.ts.map +1 -1
  125. package/dist/server/action-discovery.js +2 -0
  126. package/dist/server/action-discovery.js.map +1 -1
  127. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  128. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  129. package/dist/server/core-routes-plugin.js +9 -1
  130. package/dist/server/core-routes-plugin.js.map +1 -1
  131. package/dist/server/workspace-provider-oauth.d.ts +10 -3
  132. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  133. package/dist/server/workspace-provider-oauth.js +325 -43
  134. package/dist/server/workspace-provider-oauth.js.map +1 -1
  135. package/dist/templates/default/AGENTS.md +9 -7
  136. package/dist/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
  137. package/package.json +2 -2
  138. package/src/templates/default/AGENTS.md +9 -7
  139. package/src/templates/workspace-core/.agents/skills/external-agents/SKILL.md +46 -0
@@ -0,0 +1,218 @@
1
+ import crypto from "node:crypto";
2
+ import { deleteCookie, defineEventHandler, getCookie, getMethod, getQuery, setCookie, setResponseStatus, } from "h3";
3
+ import { getOrgContext } from "../org/context.js";
4
+ import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
5
+ import { getSession, safeReturnPath } from "../server/auth.js";
6
+ import { getH3App } from "../server/framework-request-handler.js";
7
+ import { getAppUrl, resolveOAuthRedirectUri } from "../server/google-oauth.js";
8
+ import { runWithRequestContext } from "../server/request-context.js";
9
+ import { finishMcpOAuthAuthorization, startMcpOAuthAuthorization, } from "./oauth-client.js";
10
+ import { addOAuthRemoteServer, normalizeServerName, validateRemoteUrl, } from "./remote-store.js";
11
+ const FLOW_COOKIE = "an_mcp_oauth_flow";
12
+ const FLOW_TTL_SECONDS = 10 * 60;
13
+ export function mountMcpOAuthRoutes(nitroApp, options) {
14
+ const mountedApps = (globalThis.__agentNativeMcpOAuthMountedApps ??= new WeakSet());
15
+ if (mountedApps.has(nitroApp))
16
+ return;
17
+ mountedApps.add(nitroApp);
18
+ getH3App(nitroApp).use("/_agent-native/mcp/servers/oauth", defineEventHandler(async (event) => {
19
+ const method = getMethod(event);
20
+ const pathname = (event.url?.pathname || "")
21
+ .replace(/^\/+/, "")
22
+ .replace(/\/+$/, "");
23
+ const parts = pathname ? pathname.split("/") : [];
24
+ if (method !== "GET") {
25
+ setResponseStatus(event, 405);
26
+ return { error: "Method not allowed" };
27
+ }
28
+ if (parts.length === 1 && parts[0] === "start") {
29
+ return handleMcpOAuthStart(event);
30
+ }
31
+ if (parts.length === 1 && parts[0] === "callback") {
32
+ return handleMcpOAuthCallback(event, options);
33
+ }
34
+ setResponseStatus(event, 404);
35
+ return { error: "Not found" };
36
+ }));
37
+ }
38
+ async function handleMcpOAuthStart(event) {
39
+ const session = await getSession(event).catch(() => null);
40
+ if (!session?.email)
41
+ return unauthorized(event);
42
+ const query = getQuery(event);
43
+ const rawUrl = text(query.url);
44
+ const rawName = text(query.name);
45
+ const returnUrl = text(query.return);
46
+ if (!rawUrl || !rawName) {
47
+ setResponseStatus(event, 400);
48
+ return { error: "MCP OAuth requires a server name and URL." };
49
+ }
50
+ const urlCheck = validateRemoteUrl(rawUrl);
51
+ if (!urlCheck.ok) {
52
+ setResponseStatus(event, 400);
53
+ return { error: urlCheck.error ?? "MCP server URL is not allowed." };
54
+ }
55
+ const name = normalizeServerName(rawName);
56
+ if (!name) {
57
+ setResponseStatus(event, 400);
58
+ return { error: "MCP server name is invalid." };
59
+ }
60
+ const requestedScope = query.scope === "org" ? "org" : "user";
61
+ const requestedOrgId = text(query.orgId);
62
+ const org = requestedScope === "org"
63
+ ? await getOrgContext(event).catch(() => null)
64
+ : null;
65
+ const scope = requestedScope;
66
+ const scopeId = scope === "user" ? session.email : (org?.orgId ?? "");
67
+ if (scope === "org" && requestedOrgId && requestedOrgId !== scopeId) {
68
+ setResponseStatus(event, 403);
69
+ return {
70
+ error: "The selected organization is not the active organization.",
71
+ };
72
+ }
73
+ if (scope === "org" && (!scopeId || !isOrgAdmin(org?.role))) {
74
+ setResponseStatus(event, scopeId ? 403 : 400);
75
+ return {
76
+ error: scopeId
77
+ ? "Only organization owners and admins can connect an org MCP server."
78
+ : "Join an organization before connecting an org MCP server.",
79
+ };
80
+ }
81
+ const redirectUri = resolveOAuthRedirectUri(event, "/_agent-native/mcp/servers/oauth/callback");
82
+ if (!redirectUri) {
83
+ setResponseStatus(event, 400);
84
+ return { error: "Invalid MCP OAuth redirect URI." };
85
+ }
86
+ const state = crypto.randomUUID();
87
+ const safeReturnUrl = returnUrl ? safeReturnPath(returnUrl) : undefined;
88
+ try {
89
+ const started = await runWithRequestContext({ userEmail: session.email, orgId: org?.orgId ?? undefined }, () => startMcpOAuthAuthorization({
90
+ serverUrl: urlCheck.url.toString(),
91
+ redirectUrl: redirectUri,
92
+ state,
93
+ }));
94
+ const flow = {
95
+ name,
96
+ url: urlCheck.url.toString(),
97
+ description: text(query.description),
98
+ scope,
99
+ scopeId,
100
+ owner: session.email,
101
+ ...(org?.orgId ? { orgId: org.orgId } : {}),
102
+ redirectUri,
103
+ state: started.state,
104
+ codeVerifier: started.codeVerifier,
105
+ clientInformation: started.clientInformation,
106
+ ...(started.discoveryState
107
+ ? { discoveryState: started.discoveryState }
108
+ : {}),
109
+ ...(safeReturnUrl ? { returnUrl: safeReturnUrl } : {}),
110
+ expiresAt: Date.now() + FLOW_TTL_SECONDS * 1_000,
111
+ };
112
+ setCookie(event, FLOW_COOKIE, encryptSecretValue(JSON.stringify(flow)), {
113
+ httpOnly: true,
114
+ secure: redirectUri.startsWith("https://"),
115
+ sameSite: "lax",
116
+ path: "/",
117
+ maxAge: FLOW_TTL_SECONDS,
118
+ });
119
+ return Response.redirect(started.authorizationUrl.href, 302);
120
+ }
121
+ catch {
122
+ setResponseStatus(event, 400);
123
+ return {
124
+ error: "This MCP server could not start OAuth. It may not support standard MCP OAuth discovery or dynamic client registration.",
125
+ };
126
+ }
127
+ }
128
+ async function handleMcpOAuthCallback(event, options) {
129
+ const session = await getSession(event).catch(() => null);
130
+ if (!session?.email)
131
+ return unauthorized(event);
132
+ const query = getQuery(event);
133
+ const code = text(query.code);
134
+ const state = text(query.state);
135
+ const providerError = text(query.error);
136
+ const flow = readFlow(event);
137
+ deleteCookie(event, FLOW_COOKIE, { path: "/" });
138
+ if (providerError || !code || !state) {
139
+ setResponseStatus(event, 400);
140
+ return { error: "MCP OAuth authorization was not completed." };
141
+ }
142
+ const org = flow?.scope === "org" ? await getOrgContext(event).catch(() => null) : null;
143
+ if (!flow ||
144
+ !isValidMcpOAuthFlow(flow, session.email, org?.orgId ?? undefined, state)) {
145
+ setResponseStatus(event, 400);
146
+ return { error: "MCP OAuth state is invalid or expired." };
147
+ }
148
+ if (flow.scope === "org" && !isOrgAdmin(org?.role)) {
149
+ setResponseStatus(event, 403);
150
+ return {
151
+ error: "Only organization owners and admins can connect an org MCP server.",
152
+ };
153
+ }
154
+ try {
155
+ const finished = await runWithRequestContext({ userEmail: session.email, orgId: org?.orgId ?? undefined }, () => finishMcpOAuthAuthorization({
156
+ serverUrl: flow.url,
157
+ redirectUrl: flow.redirectUri,
158
+ state: flow.state,
159
+ clientInformation: flow.clientInformation,
160
+ codeVerifier: flow.codeVerifier,
161
+ discoveryState: flow.discoveryState,
162
+ authorizationCode: code,
163
+ }));
164
+ const result = await addOAuthRemoteServer(flow.scope, flow.scopeId, {
165
+ name: flow.name,
166
+ url: flow.url,
167
+ description: flow.description,
168
+ credentials: finished.credentials,
169
+ });
170
+ if (!result.ok) {
171
+ setResponseStatus(event, 400);
172
+ return { error: result.error };
173
+ }
174
+ await options.reconfigure();
175
+ const returnPath = flow.returnUrl ??
176
+ `/settings/connections?connected=mcp-${encodeURIComponent(flow.name)}`;
177
+ return Response.redirect(getAppUrl(event, returnPath), 302);
178
+ }
179
+ catch {
180
+ setResponseStatus(event, 400);
181
+ return { error: "MCP OAuth authorization could not be completed." };
182
+ }
183
+ }
184
+ function readFlow(event) {
185
+ const encrypted = getCookie(event, FLOW_COOKIE);
186
+ if (!encrypted)
187
+ return null;
188
+ try {
189
+ const parsed = JSON.parse(decryptSecretValue(encrypted));
190
+ return parsed && typeof parsed === "object" ? parsed : null;
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ export function isValidMcpOAuthFlow(flow, email, orgId, state) {
197
+ const scopeMatches = flow.scope === "user"
198
+ ? flow.scopeId === email && !flow.orgId
199
+ : flow.scope === "org" && flow.scopeId === orgId && flow.orgId === orgId;
200
+ return (flow.expiresAt >= Date.now() &&
201
+ flow.owner === email &&
202
+ flow.state === state &&
203
+ scopeMatches &&
204
+ typeof flow.scopeId === "string" &&
205
+ typeof flow.redirectUri === "string" &&
206
+ flow.redirectUri.includes("/_agent-native/mcp/servers/oauth/callback"));
207
+ }
208
+ function isOrgAdmin(role) {
209
+ return role === "owner" || role === "admin";
210
+ }
211
+ function text(value) {
212
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
213
+ }
214
+ function unauthorized(event) {
215
+ setResponseStatus(event, 401);
216
+ return { error: "Authentication required" };
217
+ }
218
+ //# sourceMappingURL=oauth-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-routes.js","sourceRoot":"","sources":["../../src/mcp-client/oauth-routes.ts"],"names":[],"mappings":"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","sourcesContent":["import crypto from \"node:crypto\";\n\nimport type { OAuthClientInformationMixed } from \"@modelcontextprotocol/sdk/shared/auth.js\";\nimport {\n deleteCookie,\n defineEventHandler,\n getCookie,\n getMethod,\n getQuery,\n setCookie,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\n\nimport { getOrgContext } from \"../org/context.js\";\nimport { decryptSecretValue, encryptSecretValue } from \"../secrets/crypto.js\";\nimport { getSession, safeReturnPath } from \"../server/auth.js\";\nimport { getH3App } from \"../server/framework-request-handler.js\";\nimport { getAppUrl, resolveOAuthRedirectUri } from \"../server/google-oauth.js\";\nimport { runWithRequestContext } from \"../server/request-context.js\";\nimport {\n finishMcpOAuthAuthorization,\n startMcpOAuthAuthorization,\n type McpOAuthDiscoveryState,\n} from \"./oauth-client.js\";\nimport {\n addOAuthRemoteServer,\n normalizeServerName,\n validateRemoteUrl,\n type RemoteMcpScope,\n} from \"./remote-store.js\";\n\nconst FLOW_COOKIE = \"an_mcp_oauth_flow\";\nconst FLOW_TTL_SECONDS = 10 * 60;\n\nexport interface McpOAuthFlow {\n name: string;\n url: string;\n description?: string;\n scope: RemoteMcpScope;\n scopeId: string;\n owner: string;\n orgId?: string;\n redirectUri: string;\n state: string;\n codeVerifier: string;\n clientInformation: OAuthClientInformationMixed;\n discoveryState?: McpOAuthDiscoveryState;\n returnUrl?: string;\n expiresAt: number;\n}\n\nexport interface McpOAuthRoutesOptions {\n reconfigure: () => Promise<void>;\n}\n\nexport function mountMcpOAuthRoutes(\n nitroApp: any,\n options: McpOAuthRoutesOptions,\n): void {\n const mountedApps: WeakSet<object> = ((\n globalThis as any\n ).__agentNativeMcpOAuthMountedApps ??= new WeakSet<object>());\n if (mountedApps.has(nitroApp)) return;\n mountedApps.add(nitroApp);\n\n getH3App(nitroApp).use(\n \"/_agent-native/mcp/servers/oauth\",\n defineEventHandler(async (event: H3Event) => {\n const method = getMethod(event);\n const pathname = (event.url?.pathname || \"\")\n .replace(/^\\/+/, \"\")\n .replace(/\\/+$/, \"\");\n const parts = pathname ? pathname.split(\"/\") : [];\n if (method !== \"GET\") {\n setResponseStatus(event, 405);\n return { error: \"Method not allowed\" };\n }\n if (parts.length === 1 && parts[0] === \"start\") {\n return handleMcpOAuthStart(event);\n }\n if (parts.length === 1 && parts[0] === \"callback\") {\n return handleMcpOAuthCallback(event, options);\n }\n setResponseStatus(event, 404);\n return { error: \"Not found\" };\n }),\n );\n}\n\nasync function handleMcpOAuthStart(\n event: H3Event,\n): Promise<Response | Record<string, unknown>> {\n const session = await getSession(event).catch(() => null);\n if (!session?.email) return unauthorized(event);\n\n const query = getQuery(event);\n const rawUrl = text(query.url);\n const rawName = text(query.name);\n const returnUrl = text(query.return);\n if (!rawUrl || !rawName) {\n setResponseStatus(event, 400);\n return { error: \"MCP OAuth requires a server name and URL.\" };\n }\n const urlCheck = validateRemoteUrl(rawUrl);\n if (!urlCheck.ok) {\n setResponseStatus(event, 400);\n return { error: urlCheck.error ?? \"MCP server URL is not allowed.\" };\n }\n const name = normalizeServerName(rawName);\n if (!name) {\n setResponseStatus(event, 400);\n return { error: \"MCP server name is invalid.\" };\n }\n\n const requestedScope = query.scope === \"org\" ? \"org\" : \"user\";\n const requestedOrgId = text(query.orgId);\n const org =\n requestedScope === \"org\"\n ? await getOrgContext(event).catch(() => null)\n : null;\n const scope: RemoteMcpScope = requestedScope;\n const scopeId = scope === \"user\" ? session.email : (org?.orgId ?? \"\");\n if (scope === \"org\" && requestedOrgId && requestedOrgId !== scopeId) {\n setResponseStatus(event, 403);\n return {\n error: \"The selected organization is not the active organization.\",\n };\n }\n if (scope === \"org\" && (!scopeId || !isOrgAdmin(org?.role))) {\n setResponseStatus(event, scopeId ? 403 : 400);\n return {\n error: scopeId\n ? \"Only organization owners and admins can connect an org MCP server.\"\n : \"Join an organization before connecting an org MCP server.\",\n };\n }\n\n const redirectUri = resolveOAuthRedirectUri(\n event,\n \"/_agent-native/mcp/servers/oauth/callback\",\n );\n if (!redirectUri) {\n setResponseStatus(event, 400);\n return { error: \"Invalid MCP OAuth redirect URI.\" };\n }\n\n const state = crypto.randomUUID();\n const safeReturnUrl = returnUrl ? safeReturnPath(returnUrl) : undefined;\n try {\n const started = await runWithRequestContext(\n { userEmail: session.email, orgId: org?.orgId ?? undefined },\n () =>\n startMcpOAuthAuthorization({\n serverUrl: urlCheck.url!.toString(),\n redirectUrl: redirectUri,\n state,\n }),\n );\n const flow: McpOAuthFlow = {\n name,\n url: urlCheck.url!.toString(),\n description: text(query.description),\n scope,\n scopeId,\n owner: session.email,\n ...(org?.orgId ? { orgId: org.orgId } : {}),\n redirectUri,\n state: started.state,\n codeVerifier: started.codeVerifier,\n clientInformation: started.clientInformation,\n ...(started.discoveryState\n ? { discoveryState: started.discoveryState }\n : {}),\n ...(safeReturnUrl ? { returnUrl: safeReturnUrl } : {}),\n expiresAt: Date.now() + FLOW_TTL_SECONDS * 1_000,\n };\n setCookie(event, FLOW_COOKIE, encryptSecretValue(JSON.stringify(flow)), {\n httpOnly: true,\n secure: redirectUri.startsWith(\"https://\"),\n sameSite: \"lax\",\n path: \"/\",\n maxAge: FLOW_TTL_SECONDS,\n });\n return Response.redirect(started.authorizationUrl.href, 302);\n } catch {\n setResponseStatus(event, 400);\n return {\n error:\n \"This MCP server could not start OAuth. It may not support standard MCP OAuth discovery or dynamic client registration.\",\n };\n }\n}\n\nasync function handleMcpOAuthCallback(\n event: H3Event,\n options: McpOAuthRoutesOptions,\n): Promise<Response | Record<string, unknown>> {\n const session = await getSession(event).catch(() => null);\n if (!session?.email) return unauthorized(event);\n\n const query = getQuery(event);\n const code = text(query.code);\n const state = text(query.state);\n const providerError = text(query.error);\n const flow = readFlow(event);\n deleteCookie(event, FLOW_COOKIE, { path: \"/\" });\n if (providerError || !code || !state) {\n setResponseStatus(event, 400);\n return { error: \"MCP OAuth authorization was not completed.\" };\n }\n const org =\n flow?.scope === \"org\" ? await getOrgContext(event).catch(() => null) : null;\n if (\n !flow ||\n !isValidMcpOAuthFlow(flow, session.email, org?.orgId ?? undefined, state)\n ) {\n setResponseStatus(event, 400);\n return { error: \"MCP OAuth state is invalid or expired.\" };\n }\n if (flow.scope === \"org\" && !isOrgAdmin(org?.role)) {\n setResponseStatus(event, 403);\n return {\n error:\n \"Only organization owners and admins can connect an org MCP server.\",\n };\n }\n\n try {\n const finished = await runWithRequestContext(\n { userEmail: session.email, orgId: org?.orgId ?? undefined },\n () =>\n finishMcpOAuthAuthorization({\n serverUrl: flow.url,\n redirectUrl: flow.redirectUri,\n state: flow.state,\n clientInformation: flow.clientInformation,\n codeVerifier: flow.codeVerifier,\n discoveryState: flow.discoveryState,\n authorizationCode: code,\n }),\n );\n const result = await addOAuthRemoteServer(flow.scope, flow.scopeId, {\n name: flow.name,\n url: flow.url,\n description: flow.description,\n credentials: finished.credentials,\n });\n if (!result.ok) {\n setResponseStatus(event, 400);\n return { error: result.error };\n }\n await options.reconfigure();\n const returnPath =\n flow.returnUrl ??\n `/settings/connections?connected=mcp-${encodeURIComponent(flow.name)}`;\n return Response.redirect(getAppUrl(event, returnPath), 302);\n } catch {\n setResponseStatus(event, 400);\n return { error: \"MCP OAuth authorization could not be completed.\" };\n }\n}\n\nfunction readFlow(event: H3Event): McpOAuthFlow | null {\n const encrypted = getCookie(event, FLOW_COOKIE);\n if (!encrypted) return null;\n try {\n const parsed = JSON.parse(decryptSecretValue(encrypted)) as McpOAuthFlow;\n return parsed && typeof parsed === \"object\" ? parsed : null;\n } catch {\n return null;\n }\n}\n\nexport function isValidMcpOAuthFlow(\n flow: McpOAuthFlow,\n email: string,\n orgId: string | undefined,\n state: string,\n): boolean {\n const scopeMatches =\n flow.scope === \"user\"\n ? flow.scopeId === email && !flow.orgId\n : flow.scope === \"org\" && flow.scopeId === orgId && flow.orgId === orgId;\n return (\n flow.expiresAt >= Date.now() &&\n flow.owner === email &&\n flow.state === state &&\n scopeMatches &&\n typeof flow.scopeId === \"string\" &&\n typeof flow.redirectUri === \"string\" &&\n flow.redirectUri.includes(\"/_agent-native/mcp/servers/oauth/callback\")\n );\n}\n\nfunction isOrgAdmin(role: unknown): boolean {\n return role === \"owner\" || role === \"admin\";\n}\n\nfunction text(value: unknown): string | undefined {\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction unauthorized(event: H3Event) {\n setResponseStatus(event, 401);\n return { error: \"Authentication required\" };\n}\n"]}
@@ -20,6 +20,8 @@
20
20
  * re-saved to migrate.
21
21
  */
22
22
  import type { McpHttpServerConfig } from "./config.js";
23
+ import { type McpOAuthCredentialBundle } from "./oauth-client.js";
24
+ export { validateRemoteUrl } from "./remote-url.js";
23
25
  export type RemoteMcpScope = "user" | "org";
24
26
  export interface StoredRemoteMcpServer {
25
27
  /** Stable unique id — used for removal / URLs. */
@@ -43,6 +45,8 @@ export interface StoredRemoteMcpServer {
43
45
  * supplied (or for legacy cleartext rows).
44
46
  */
45
47
  headerSecretKey?: string;
48
+ /** Reference to the encrypted OAuth credential bundle for this server. */
49
+ oauthSecretKey?: string;
46
50
  /**
47
51
  * Trusted first-party Agent-Native app. Only framework-controlled
48
52
  * registrations should set this; management routes intentionally do not
@@ -72,12 +76,6 @@ export declare function normalizeServerName(input: string): string;
72
76
  * name don't collide in the global MCP manager.
73
77
  */
74
78
  export declare function hashEmail(email: string): string;
75
- /** Reject obviously-wrong URLs. Allows http only for localhost. */
76
- export declare function validateRemoteUrl(raw: string): {
77
- ok: boolean;
78
- url?: URL;
79
- error?: string;
80
- };
81
79
  export declare function listRemoteServers(scope: RemoteMcpScope, scopeId: string): Promise<StoredRemoteMcpServer[]>;
82
80
  export declare function addRemoteServer(scope: RemoteMcpScope, scopeId: string, input: {
83
81
  name: string;
@@ -92,6 +90,23 @@ export declare function addRemoteServer(scope: RemoteMcpScope, scopeId: string,
92
90
  ok: false;
93
91
  error: string;
94
92
  }>;
93
+ /**
94
+ * Persist a remote MCP server whose authorization is managed by the MCP
95
+ * server's OAuth 2.1 metadata and token endpoints. OAuth credentials are
96
+ * stored separately from the settings row and never returned to clients.
97
+ */
98
+ export declare function addOAuthRemoteServer(scope: RemoteMcpScope, scopeId: string, input: {
99
+ name: string;
100
+ url: string;
101
+ description?: string;
102
+ credentials: McpOAuthCredentialBundle;
103
+ }): Promise<{
104
+ ok: true;
105
+ server: StoredRemoteMcpServer;
106
+ } | {
107
+ ok: false;
108
+ error: string;
109
+ }>;
95
110
  export declare function addFirstPartyRemoteServer(orgId: string, input: {
96
111
  appId: string;
97
112
  name: string;
@@ -1 +1 @@
1
- {"version":3,"file":"remote-store.d.ts","sourceRoot":"","sources":["../../src/mcp-client/remote-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"remote-store.d.ts","sourceRoot":"","sources":["../../src/mcp-client/remote-store.ts"],"names":[],"mappings":"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"}
@@ -23,6 +23,9 @@ import { createHash } from "node:crypto";
23
23
  import { writeAppSecret, readAppSecret, deleteAppSecret, } from "../secrets/storage.js";
24
24
  import { getOrgSetting, putOrgSetting, deleteOrgSetting, } from "../settings/org-settings.js";
25
25
  import { getUserSetting, putUserSetting, deleteUserSetting, } from "../settings/user-settings.js";
26
+ import { deleteMcpOAuthCredentials, getMcpOAuthAccessToken, saveMcpOAuthCredentials, } from "./oauth-client.js";
27
+ import { validateRemoteUrl } from "./remote-url.js";
28
+ export { validateRemoteUrl } from "./remote-url.js";
26
29
  const SETTINGS_KEY = "mcp-servers-remote";
27
30
  function toSecretScope(scope) {
28
31
  return scope === "user" ? "user" : "workspace";
@@ -103,129 +106,6 @@ export function hashEmail(email) {
103
106
  function sanitiseOrgId(orgId) {
104
107
  return orgId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
105
108
  }
106
- /**
107
- * Hostname patterns that are always rejected to prevent SSRF via DNS rebinding
108
- * or well-known internal service names.
109
- */
110
- const BLOCKED_HOSTNAME_PATTERNS = [
111
- /^localhost$/i,
112
- /\.localhost$/i,
113
- /\.local$/i,
114
- /\.internal$/i,
115
- // DNS rebind services
116
- /\.nip\.io$/i,
117
- /\.sslip\.io$/i,
118
- /\.xip\.io$/i,
119
- /\.localtest\.me$/i,
120
- /\.lvh\.me$/i,
121
- // Cloud metadata hosts
122
- /^metadata\.google\.internal$/i,
123
- /^instance-data$/i,
124
- ];
125
- /** Literal IPs that are always rejected (cloud metadata endpoints). */
126
- const BLOCKED_IPS = new Set([
127
- "169.254.169.254", // AWS/GCP/Azure IMDS
128
- "100.100.100.200", // Alibaba Cloud metadata
129
- "::1",
130
- ]);
131
- /** Parse a dotted-decimal IPv4 string to a 32-bit integer for range checks. */
132
- function ipv4ToInt(ip) {
133
- const parts = ip.split(".");
134
- if (parts.length !== 4)
135
- return null;
136
- let n = 0;
137
- for (const part of parts) {
138
- const byte = parseInt(part, 10);
139
- if (isNaN(byte) || byte < 0 || byte > 255)
140
- return null;
141
- n = (n << 8) | byte;
142
- }
143
- return n >>> 0;
144
- }
145
- function isPrivateIpv4(hostname) {
146
- const n = ipv4ToInt(hostname);
147
- if (n === null)
148
- return false;
149
- // 10.0.0.0/8
150
- if ((n & 0xff000000) >>> 0 === 0x0a000000)
151
- return true;
152
- // 172.16.0.0/12
153
- if ((n & 0xfff00000) >>> 0 === 0xac100000)
154
- return true;
155
- // 192.168.0.0/16
156
- if ((n & 0xffff0000) >>> 0 === 0xc0a80000)
157
- return true;
158
- // 169.254.0.0/16 (link-local, includes AWS/GCP metadata)
159
- if ((n & 0xffff0000) >>> 0 === 0xa9fe0000)
160
- return true;
161
- // 127.0.0.0/8 (loopback)
162
- if ((n & 0xff000000) >>> 0 === 0x7f000000)
163
- return true;
164
- return false;
165
- }
166
- function isBlockedHostname(hostname) {
167
- const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
168
- if (BLOCKED_IPS.has(normalized))
169
- return true;
170
- if (isPrivateIpv4(normalized))
171
- return true;
172
- // IPv6 ULA (fc00::/7) and link-local (fe80::/10)
173
- if (normalized === "::1")
174
- return true;
175
- if (normalized.startsWith("fc") || normalized.startsWith("fd"))
176
- return true;
177
- if (normalized.startsWith("fe80"))
178
- return true;
179
- if (normalized.startsWith("::ffff:")) {
180
- const mapped = normalized.slice("::ffff:".length);
181
- if (isPrivateIpv4(mapped))
182
- return true;
183
- const hexMatch = /^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(mapped);
184
- if (hexMatch) {
185
- const value = (parseInt(hexMatch[1], 16) << 16) | parseInt(hexMatch[2], 16);
186
- const dotted = [
187
- (value >>> 24) & 0xff,
188
- (value >>> 16) & 0xff,
189
- (value >>> 8) & 0xff,
190
- value & 0xff,
191
- ].join(".");
192
- if (isPrivateIpv4(dotted))
193
- return true;
194
- }
195
- }
196
- for (const pattern of BLOCKED_HOSTNAME_PATTERNS) {
197
- if (pattern.test(normalized))
198
- return true;
199
- }
200
- return false;
201
- }
202
- /** Reject obviously-wrong URLs. Allows http only for localhost. */
203
- export function validateRemoteUrl(raw) {
204
- let url;
205
- try {
206
- url = new URL(raw);
207
- }
208
- catch {
209
- return { ok: false, error: "Not a valid URL" };
210
- }
211
- if (url.protocol === "https:") {
212
- if (isBlockedHostname(url.hostname)) {
213
- return {
214
- ok: false,
215
- error: `Host "${url.hostname}" is not allowed (private/internal address)`,
216
- };
217
- }
218
- return { ok: true, url };
219
- }
220
- if (url.protocol === "http:") {
221
- const host = url.hostname;
222
- if (host === "localhost" || host === "127.0.0.1") {
223
- return { ok: true, url };
224
- }
225
- return { ok: false, error: "Plain http is only allowed for localhost" };
226
- }
227
- return { ok: false, error: `Unsupported protocol: ${url.protocol}` };
228
- }
229
109
  async function readList(scope, scopeId) {
230
110
  const raw = scope === "user"
231
111
  ? await getUserSetting(scopeId, SETTINGS_KEY)
@@ -254,6 +134,47 @@ export async function addRemoteServer(scope, scopeId, input) {
254
134
  }
255
135
  return addRemoteServerInternal(scope, scopeId, input);
256
136
  }
137
+ /**
138
+ * Persist a remote MCP server whose authorization is managed by the MCP
139
+ * server's OAuth 2.1 metadata and token endpoints. OAuth credentials are
140
+ * stored separately from the settings row and never returned to clients.
141
+ */
142
+ export async function addOAuthRemoteServer(scope, scopeId, input) {
143
+ const oauthSecretKey = `mcp_oauth:${shortId()}`;
144
+ try {
145
+ await saveMcpOAuthCredentials({
146
+ key: oauthSecretKey,
147
+ scope,
148
+ scopeId,
149
+ credentials: input.credentials,
150
+ });
151
+ const result = await addRemoteServerInternal(scope, scopeId, {
152
+ name: input.name,
153
+ url: input.url,
154
+ description: input.description,
155
+ oauthSecretKey,
156
+ });
157
+ if (!result.ok) {
158
+ await deleteMcpOAuthCredentials({
159
+ key: oauthSecretKey,
160
+ scope,
161
+ scopeId,
162
+ });
163
+ }
164
+ return result;
165
+ }
166
+ catch (err) {
167
+ await deleteMcpOAuthCredentials({
168
+ key: oauthSecretKey,
169
+ scope,
170
+ scopeId,
171
+ }).catch(() => { });
172
+ return {
173
+ ok: false,
174
+ error: `Failed to save MCP OAuth credentials: ${err?.message ?? err}`,
175
+ };
176
+ }
177
+ }
257
178
  export async function addFirstPartyRemoteServer(orgId, input) {
258
179
  const trust = await isFirstPartyRemoteEndpointTrusted(orgId, input.appId, input.url);
259
180
  if (!trust.ok)
@@ -313,6 +234,7 @@ async function addRemoteServerInternal(scope, scopeId, input) {
313
234
  ...(input.firstPartyAppId
314
235
  ? { firstPartyAppId: input.firstPartyAppId }
315
236
  : {}),
237
+ ...(input.oauthSecretKey ? { oauthSecretKey: input.oauthSecretKey } : {}),
316
238
  description: input.description?.trim() || undefined,
317
239
  createdAt: Date.now(),
318
240
  };
@@ -416,6 +338,19 @@ export async function removeRemoteServer(scope, scopeId, id) {
416
338
  console.warn(`[mcp-client] Failed to delete MCP header secret ${removed.headerSecretKey}: ${err?.message ?? err}`);
417
339
  }
418
340
  }
341
+ if (removed?.oauthSecretKey) {
342
+ try {
343
+ await deleteMcpOAuthCredentials({
344
+ key: removed.oauthSecretKey,
345
+ scope,
346
+ scopeId,
347
+ });
348
+ }
349
+ catch (err) {
350
+ // eslint-disable-next-line no-console
351
+ console.warn(`[mcp-client] Failed to delete MCP OAuth credentials ${removed.oauthSecretKey}: ${err?.message ?? err}`);
352
+ }
353
+ }
419
354
  return true;
420
355
  }
421
356
  /**
@@ -485,10 +420,23 @@ export function toHttpServerConfig(stored) {
485
420
  * secrets).
486
421
  */
487
422
  export async function toHttpServerConfigAsync(scope, scopeId, stored) {
423
+ let headers = await materializeHeaders(scope, scopeId, stored);
424
+ if (stored.oauthSecretKey) {
425
+ const accessToken = await getMcpOAuthAccessToken({
426
+ key: stored.oauthSecretKey,
427
+ scope,
428
+ scopeId,
429
+ serverUrl: stored.url,
430
+ });
431
+ if (accessToken) {
432
+ headers ??= {};
433
+ headers.Authorization = `Bearer ${accessToken}`;
434
+ }
435
+ }
488
436
  return {
489
437
  type: "http",
490
438
  url: stored.url,
491
- headers: await materializeHeaders(scope, scopeId, stored),
439
+ headers,
492
440
  ...(stored.firstParty ? { firstParty: true } : {}),
493
441
  ...(stored.firstPartyAppId
494
442
  ? { firstPartyAppId: stored.firstPartyAppId }