Unified · Split
| @@ -27,6 +27,7 @@ MCP install URL generation: · 26 unmodified lines | |||
| 27 | 27 | ||
| 28 | - Derives from existing frontend origin and deployment metadata. | 28 | - Derives from existing frontend origin and deployment metadata. |
| 29 | - In local dev with no DB URL, defaults to local PGlite-backed control-plane in this Next.js app. | 29 | - In local dev with no DB URL, defaults to local PGlite-backed control-plane in this Next.js app. |
| 30 | - MCP endpoint is served from this app at `GET/POST/DELETE /v1/mcp`. | ||
| 30 | 31 | ||
| 31 | WorkOS auth setup (optional but recommended): | 32 | WorkOS auth setup (optional but recommended): |
| 32 | 33 | ||
| @@ -36,3 +37,11 @@ WorkOS auth setup (optional but recommended): · 3 unmodified lines | |||
| 36 | - `WORKOS_REDIRECT_URI` or `NEXT_PUBLIC_WORKOS_REDIRECT_URI` (for example `http://localhost:4312/callback`) | 37 | - `WORKOS_REDIRECT_URI` or `NEXT_PUBLIC_WORKOS_REDIRECT_URI` (for example `http://localhost:4312/callback`) |
| 37 | 38 | ||
| 38 | When WorkOS is configured, the app requires sign-in and the server proxy forwards the authenticated WorkOS access token to control-plane as a bearer token (`Authorization: Bearer ...`). | 39 | When WorkOS is configured, the app requires sign-in and the server proxy forwards the authenticated WorkOS access token to control-plane as a bearer token (`Authorization: Bearer ...`). |
| 40 | |||
| 41 | MCP OAuth setup (for external MCP clients): | ||
| 42 | |||
| 43 | - `MCP_AUTHORIZATION_SERVER` (or `MCP_AUTHORIZATION_SERVER_URL`, `WORKOS_AUTHKIT_ISSUER`) | ||
| 44 | - `/.well-known/oauth-protected-resource` | ||
| 45 | - `/.well-known/oauth-authorization-server` | ||
| 46 | |||
| 47 | Unauthenticated requests to `/v1/mcp` return `401` with `WWW-Authenticate` and `resource_metadata` for OAuth discovery. | ||
| @@ -0,0 +1,43 @@ | |||
| 1 | import { getMcpAuthConfig } from "../../../lib/mcp/resource-auth"; | ||
| 2 | |||
| 3 | export const dynamic = "force-dynamic"; | ||
| 4 | export const runtime = "nodejs"; | ||
| 5 | |||
| 6 | export async function GET(): Promise<Response> { | ||
| 7 | const authConfig = getMcpAuthConfig(); | ||
| 8 | if (!authConfig.enabled || !authConfig.authorizationServer) { | ||
| 9 | return Response.json( | ||
| 10 | { error: "MCP OAuth is not configured" }, | ||
| 11 | { status: 404 }, | ||
| 12 | ); | ||
| 13 | } | ||
| 14 | |||
| 15 | const upstream = new URL( | ||
| 16 | "/.well-known/oauth-authorization-server", | ||
| 17 | authConfig.authorizationServer, | ||
| 18 | ); | ||
| 19 | |||
| 20 | try { | ||
| 21 | const response = await fetch(upstream.toString(), { | ||
| 22 | headers: { accept: "application/json" }, | ||
| 23 | }); | ||
| 24 | |||
| 25 | const text = await response.text(); | ||
| 26 | return new Response(text, { | ||
| 27 | status: response.status, | ||
| 28 | headers: { | ||
| 29 | "content-type": response.headers.get("content-type") ?? "application/json", | ||
| 30 | }, | ||
| 31 | }); | ||
| 32 | } catch (error) { | ||
| 33 | return Response.json( | ||
| 34 | { | ||
| 35 | error: | ||
| 36 | error instanceof Error | ||
| 37 | ? `Failed to fetch upstream auth metadata: ${error.message}` | ||
| 38 | : "Failed to fetch upstream auth metadata", | ||
| 39 | }, | ||
| 40 | { status: 502 }, | ||
| 41 | ); | ||
| 42 | } | ||
| 43 | } | ||
| @@ -0,0 +1,62 @@ | |||
| 1 | import { externalOriginFromRequest } from "../../../lib/workos"; | ||
| 2 | import { getMcpAuthConfig } from "../../../lib/mcp/resource-auth"; | ||
| 3 | |||
| 4 | export const dynamic = "force-dynamic"; | ||
| 5 | export const runtime = "nodejs"; | ||
| 6 | |||
| 7 | const MCP_PATH = "/v1/mcp"; | ||
| 8 | |||
| 9 | const parseResourceHint = ( | ||
| 10 | requestUrl: URL, | ||
| 11 | origin: string, | ||
| 12 | ): { resourceUrl: URL } | { error: string } => { | ||
| 13 | const resourceHint = requestUrl.searchParams.get("resource")?.trim(); | ||
| 14 | if (!resourceHint) { | ||
| 15 | const fallback = new URL(MCP_PATH, origin); | ||
| 16 | for (const [key, value] of requestUrl.searchParams.entries()) { | ||
| 17 | if (key === "resource") continue; | ||
| 18 | fallback.searchParams.set(key, value); | ||
| 19 | } | ||
| 20 | return { resourceUrl: fallback }; | ||
| 21 | } | ||
| 22 | |||
| 23 | let parsed: URL; | ||
| 24 | try { | ||
| 25 | parsed = new URL(resourceHint); | ||
| 26 | } catch { | ||
| 27 | return { error: "Invalid resource hint" }; | ||
| 28 | } | ||
| 29 | |||
| 30 | if (parsed.origin !== origin) { | ||
| 31 | return { error: "resource hint origin must match this server" }; | ||
| 32 | } | ||
| 33 | |||
| 34 | if (parsed.pathname !== MCP_PATH) { | ||
| 35 | return { error: `resource hint path must be ${MCP_PATH}` }; | ||
| 36 | } | ||
| 37 | |||
| 38 | return { resourceUrl: parsed }; | ||
| 39 | }; | ||
| 40 | |||
| 41 | export async function GET(request: Request): Promise<Response> { | ||
| 42 | const authConfig = getMcpAuthConfig(); | ||
| 43 | if (!authConfig.enabled || !authConfig.authorizationServer) { | ||
| 44 | return Response.json( | ||
| 45 | { error: "MCP OAuth is not configured" }, | ||
| 46 | { status: 404 }, | ||
| 47 | ); | ||
| 48 | } | ||
| 49 | |||
| 50 | const origin = externalOriginFromRequest(request); | ||
| 51 | const requestUrl = new URL(request.url); | ||
| 52 | const parsedResource = parseResourceHint(requestUrl, origin); | ||
| 53 | if ("error" in parsedResource) { | ||
| 54 | return Response.json({ error: parsedResource.error }, { status: 400 }); | ||
| 55 | } | ||
| 56 | |||
| 57 | return Response.json({ | ||
| 58 | resource: parsedResource.resourceUrl.toString(), | ||
| 59 | authorization_servers: [authConfig.authorizationServer], | ||
| 60 | bearer_methods_supported: ["header"], | ||
| 61 | }); | ||
| 62 | } | ||
| @@ -0,0 +1,66 @@ | |||
| 1 | import { | ||
| 2 | createWorkosPrincipal, | ||
| 3 | getControlPlaneRuntime, | ||
| 4 | provisionPrincipal, | ||
| 5 | } from "../../../lib/control-plane/server"; | ||
| 6 | import { | ||
| 7 | getMcpAuthConfig, | ||
| 8 | unauthorizedMcpResponse, | ||
| 9 | verifyMcpBearerToken, | ||
| 10 | } from "../../../lib/mcp/resource-auth"; | ||
| 11 | |||
| 12 | export const dynamic = "force-dynamic"; | ||
| 13 | export const runtime = "nodejs"; | ||
| 14 | |||
| 15 | const parseWorkspaceId = (request: Request): string | null => { | ||
| 16 | const value = new URL(request.url).searchParams.get("workspaceId")?.trim(); | ||
| 17 | return value && value.length > 0 ? value : null; | ||
| 18 | }; | ||
| 19 | |||
| 20 | const handler = async (request: Request): Promise<Response> => { | ||
| 21 | const authConfig = getMcpAuthConfig(); | ||
| 22 | if (!authConfig.enabled) { | ||
| 23 | return Response.json( | ||
| 24 | { | ||
| 25 | error: | ||
| 26 | "MCP OAuth is not configured. Set MCP_AUTHORIZATION_SERVER (or WORKOS_AUTHKIT_ISSUER).", | ||
| 27 | }, | ||
| 28 | { status: 503 }, | ||
| 29 | ); | ||
| 30 | } | ||
| 31 | |||
| 32 | const verified = await verifyMcpBearerToken(request, authConfig); | ||
| 33 | if (!verified) { | ||
| 34 | return unauthorizedMcpResponse(request, "No valid bearer token provided."); | ||
| 35 | } | ||
| 36 | |||
| 37 | const workspaceId = parseWorkspaceId(request); | ||
| 38 | if (!workspaceId) { | ||
| 39 | return Response.json( | ||
| 40 | { error: "workspaceId query parameter is required" }, | ||
| 41 | { status: 400 }, | ||
| 42 | ); | ||
| 43 | } | ||
| 44 | |||
| 45 | const principal = createWorkosPrincipal({ | ||
| 46 | subject: verified.subject, | ||
| 47 | email: verified.email, | ||
| 48 | displayName: verified.displayName, | ||
| 49 | }); | ||
| 50 | |||
| 51 | if (workspaceId !== principal.workspaceId) { | ||
| 52 | return Response.json( | ||
| 53 | { error: "workspaceId does not match authenticated subject" }, | ||
| 54 | { status: 403 }, | ||
| 55 | ); | ||
| 56 | } | ||
| 57 | |||
| 58 | const controlPlaneRuntime = await getControlPlaneRuntime(); | ||
| 59 | await provisionPrincipal(controlPlaneRuntime, principal); | ||
| 60 | |||
| 61 | return controlPlaneRuntime.handleMcp(request, workspaceId); | ||
| 62 | }; | ||
| 63 | |||
| 64 | export const GET = handler; | ||
| 65 | export const POST = handler; | ||
| 66 | export const DELETE = handler; | ||
| @@ -65,13 +65,6 @@ type SourceGroup = { · 64 unmodified lines | |||
| 65 | toolCount: number; | 65 | toolCount: number; |
| 66 | }; | 66 | }; |
| 67 | 67 | ||
| 68 | const isLocalHost = (hostname: string): boolean => | ||
| 69 | hostname === "localhost" | ||
| 70 | || hostname === "127.0.0.1" | ||
| 71 | || hostname === "0.0.0.0" | ||
| 72 | || hostname === "::1" | ||
| 73 | || hostname === "[::1]"; | ||
| 74 | |||
| 75 | // --------------------------------------------------------------------------- | 68 | // --------------------------------------------------------------------------- |
| 76 | // Search helpers (ported from old executor explorer-derived.ts) | 69 | // Search helpers (ported from old executor explorer-derived.ts) |
| 77 | // --------------------------------------------------------------------------- | 70 | // --------------------------------------------------------------------------- |
| @@ -407,11 +400,7 @@ export function ToolsView(props: { · 329 unmodified lines | |||
| 407 | return `http://127.0.0.1:8788/v1/mcp?workspaceId=${encodeURIComponent(workspaceId)}`; | 400 | return `http://127.0.0.1:8788/v1/mcp?workspaceId=${encodeURIComponent(workspaceId)}`; |
| 408 | } | 401 | } |
| 409 | 402 | ||
| 410 | const baseOrigin = isLocalHost(window.location.hostname) | 403 | const url = new URL("/v1/mcp", window.location.origin); |
| 411 | ? "http://127.0.0.1:8788" | ||
| 412 | : window.location.origin; | ||
| 413 | |||
| 414 | const url = new URL("/v1/mcp", baseOrigin); | ||
| 415 | url.searchParams.set("workspaceId", workspaceId); | 404 | url.searchParams.set("workspaceId", workspaceId); |
| 416 | return url.toString(); | 405 | return url.toString(); |
| 417 | }, [props.mcpBaseUrl, workspaceId]); | 406 | }, [props.mcpBaseUrl, workspaceId]); |
| @@ -8,6 +8,18 @@ import { · 7 unmodified lines | |||
| 8 | makeSourceCatalogService, | 8 | makeSourceCatalogService, |
| 9 | makeSourceManagerService, | 9 | makeSourceManagerService, |
| 10 | } from "@executor-v2/management-api"; | 10 | } from "@executor-v2/management-api"; |
| 11 | import { | ||
| 12 | createRunExecutor, | ||
| 13 | createSourceToolRegistry, | ||
| 14 | defaultExecuteToolExposureMode, | ||
| 15 | makeGraphqlToolProvider, | ||
| 16 | makeMcpToolProvider, | ||
| 17 | makeOpenApiToolProvider, | ||
| 18 | makeRuntimeAdapterRegistry, | ||
| 19 | makeToolProviderRegistry, | ||
| 20 | parseExecuteToolExposureMode, | ||
| 21 | type ExecuteToolExposureMode, | ||
| 22 | } from "@executor-v2/engine"; | ||
| 11 | import { | 23 | import { |
| 12 | makeSqlControlPlanePersistence, | 24 | makeSqlControlPlanePersistence, |
| 13 | type SqlControlPlanePersistence, | 25 | type SqlControlPlanePersistence, |
| @@ -22,14 +34,22 @@ import { · 8 unmodified lines | |||
| 22 | type OrganizationMembership, | 34 | type OrganizationMembership, |
| 23 | type Workspace, | 35 | type Workspace, |
| 24 | } from "@executor-v2/schema"; | 36 | } from "@executor-v2/schema"; |
| 37 | import { makeCloudflareWorkerLoaderRuntimeAdapter } from "@executor-v2/runtime-cloudflare-worker-loader"; | ||
| 38 | import { makeDenoSubprocessRuntimeAdapter } from "@executor-v2/runtime-deno-subprocess"; | ||
| 39 | import { makeLocalInProcessRuntimeAdapter } from "@executor-v2/runtime-local-inproc"; | ||
| 25 | import * as Effect from "effect/Effect"; | 40 | import * as Effect from "effect/Effect"; |
| 26 | import * as Layer from "effect/Layer"; | 41 | import * as Layer from "effect/Layer"; |
| 27 | 42 | ||
| 28 | import { PmActorLive } from "../../../pm/src/actor"; | 43 | import { PmActorLive } from "../../../pm/src/actor"; |
| 29 | import { createPmApprovalsService } from "../../../pm/src/approvals-service"; | 44 | import { |
| 45 | createPmApprovalsService, | ||
| 46 | createPmPersistentToolApprovalPolicy, | ||
| 47 | } from "../../../pm/src/approvals-service"; | ||
| 30 | import { createPmCredentialsService } from "../../../pm/src/credentials-service"; | 48 | import { createPmCredentialsService } from "../../../pm/src/credentials-service"; |
| 49 | import { createPmMcpHandler } from "../../../pm/src/mcp-handler"; | ||
| 31 | import { createPmOrganizationsService } from "../../../pm/src/organizations-service"; | 50 | import { createPmOrganizationsService } from "../../../pm/src/organizations-service"; |
| 32 | import { createPmPoliciesService } from "../../../pm/src/policies-service"; | 51 | import { createPmPoliciesService } from "../../../pm/src/policies-service"; |
| 52 | import { createPmExecuteRuntimeRun } from "../../../pm/src/runtime-execution-port"; | ||
| 33 | import { createPmStorageService } from "../../../pm/src/storage-service"; | 53 | import { createPmStorageService } from "../../../pm/src/storage-service"; |
| 34 | import { createPmToolsService } from "../../../pm/src/tools-service"; | 54 | import { createPmToolsService } from "../../../pm/src/tools-service"; |
| 35 | import { createPmWorkspacesService } from "../../../pm/src/workspaces-service"; | 55 | import { createPmWorkspacesService } from "../../../pm/src/workspaces-service"; |
| @@ -90,6 +110,7 @@ type ControlPlaneRuntime = { · 54 unmodified lines | |||
| 90 | toolArtifactStore: ToolArtifactStore; | 110 | toolArtifactStore: ToolArtifactStore; |
| 91 | fetchOpenApiDocument: typeof fetchOpenApiDocument; | 111 | fetchOpenApiDocument: typeof fetchOpenApiDocument; |
| 92 | handleControlPlane: (request: Request) => Promise<Response>; | 112 | handleControlPlane: (request: Request) => Promise<Response>; |
| 113 | handleMcp: (request: Request, workspaceId: string) => Promise<Response>; | ||
| 93 | dispose: () => Promise<void>; | 114 | dispose: () => Promise<void>; |
| 94 | }; | 115 | }; |
| 95 | 116 | ||
| @@ -127,6 +148,22 @@ const resolveControlPlaneDataDir = (): string => · 31 unmodified lines | |||
| 127 | const resolveStateRootDir = (): string => | 148 | const resolveStateRootDir = (): string => |
| 128 | defaultControlPlaneStateRootDir; | 149 | defaultControlPlaneStateRootDir; |
| 129 | 150 | ||
| 151 | const readConfiguredRuntimeKind = (value: string | undefined): string | undefined => { | ||
| 152 | const normalized = value?.trim(); | ||
| 153 | return normalized && normalized.length > 0 ? normalized : undefined; | ||
| 154 | }; | ||
| 155 | |||
| 156 | const readBooleanFlag = (value: string | undefined): boolean => { | ||
| 157 | const normalized = value?.trim().toLowerCase(); | ||
| 158 | return normalized === "1" || normalized === "true" || normalized === "yes"; | ||
| 159 | }; | ||
| 160 | |||
| 161 | const readConfiguredToolExposureMode = ( | ||
| 162 | value: string | undefined, | ||
| 163 | ): ExecuteToolExposureMode => | ||
| 164 | parseExecuteToolExposureMode(value ?? undefined) ?? | ||
| 165 | defaultExecuteToolExposureMode; | ||
| 166 | |||
| 130 | const openApiSyncRetryDelayMs = 300; | 167 | const openApiSyncRetryDelayMs = 300; |
| 131 | 168 | ||
| 132 | const formatCause = (cause: unknown): string => { | 169 | const formatCause = (cause: unknown): string => { |
| @@ -256,6 +293,35 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 123 unmodified lines | |||
| 256 | 293 | ||
| 257 | const sourceStore = persistence.sourceStore; | 294 | const sourceStore = persistence.sourceStore; |
| 258 | const toolArtifactStore = persistence.toolArtifactStore; | 295 | const toolArtifactStore = persistence.toolArtifactStore; |
| 296 | const runtimeAdapterList = [ | ||
| 297 | makeLocalInProcessRuntimeAdapter(), | ||
| 298 | makeDenoSubprocessRuntimeAdapter(), | ||
| 299 | makeCloudflareWorkerLoaderRuntimeAdapter(), | ||
| 300 | ]; | ||
| 301 | const runtimeAdapters = makeRuntimeAdapterRegistry(runtimeAdapterList); | ||
| 302 | const defaultRuntimeKind = | ||
| 303 | readConfiguredRuntimeKind(process.env.PM_RUNTIME_KIND) | ||
| 304 | ?? runtimeAdapterList[0]?.kind | ||
| 305 | ?? "local-inproc"; | ||
| 306 | const requireToolApprovals = readBooleanFlag(process.env.PM_REQUIRE_TOOL_APPROVALS); | ||
| 307 | const defaultToolExposureMode = readConfiguredToolExposureMode( | ||
| 308 | process.env.PM_TOOL_EXPOSURE_MODE, | ||
| 309 | ); | ||
| 310 | const toolProviderRegistry = makeToolProviderRegistry([ | ||
| 311 | makeOpenApiToolProvider(), | ||
| 312 | makeMcpToolProvider(), | ||
| 313 | makeGraphqlToolProvider(), | ||
| 314 | ]); | ||
| 315 | const persistentApprovalPolicy = createPmPersistentToolApprovalPolicy( | ||
| 316 | persistence.rows, | ||
| 317 | { | ||
| 318 | requireApprovals: requireToolApprovals, | ||
| 319 | }, | ||
| 320 | ); | ||
| 321 | const mcpHandlers = new Map< | ||
| 322 | string, | ||
| 323 | (request: Request) => Promise<Response> | ||
| 324 | >(); | ||
| 259 | 325 | ||
| 260 | const sourceCatalog = makeSourceCatalogService(sourceStore); | 326 | const sourceCatalog = makeSourceCatalogService(sourceStore); |
| 261 | const sourceManager = makeSourceManagerService(toolArtifactStore); | 327 | const sourceManager = makeSourceManagerService(toolArtifactStore); |
| @@ -373,12 +439,44 @@ const createControlPlaneRuntime = async (): Promise<ControlPlaneRuntime> => { · 111 unmodified lines | |||
| 373 | PmActorLive(persistence.rows), | 439 | PmActorLive(persistence.rows), |
| 374 | ); | 440 | ); |
| 375 | 441 | ||
| 442 | const resolveMcpHandler = (workspaceId: string): ((request: Request) => Promise<Response>) => { | ||
| 443 | const existing = mcpHandlers.get(workspaceId); | ||
| 444 | if (existing) { | ||
| 445 | return existing; | ||
| 446 | } | ||
| 447 | |||
| 448 | const toolRegistry = createSourceToolRegistry({ | ||
| 449 | workspaceId, | ||
| 450 | sourceStore, | ||
| 451 | toolArtifactStore, | ||
| 452 | toolProviderRegistry, | ||
| 453 | approvalPolicy: persistentApprovalPolicy, | ||
| 454 | }); | ||
| 455 | const executeRuntimeRun = createPmExecuteRuntimeRun({ | ||
| 456 | defaultRuntimeKind, | ||
| 457 | runtimeAdapters, | ||
| 458 | toolRegistry, | ||
| 459 | }); | ||
| 460 | const runExecutor = createRunExecutor(executeRuntimeRun); | ||
| 461 | const next = createPmMcpHandler(runExecutor.executeRun, { | ||
| 462 | toolRegistry, | ||
| 463 | defaultToolExposureMode, | ||
| 464 | }); | ||
| 465 | |||
| 466 | mcpHandlers.set(workspaceId, next); | ||
| 467 | return next; | ||
| 468 | }; | ||
| 469 | |||
| 376 | return { | 470 | return { |
| 377 | persistence, | 471 | persistence, |
| 378 | sourceStore, | 472 | sourceStore, |
| 379 | toolArtifactStore, | 473 | toolArtifactStore, |
| 380 | fetchOpenApiDocument, | 474 | fetchOpenApiDocument, |
| 381 | handleControlPlane: controlPlaneWebHandler.handler, | 475 | handleControlPlane: controlPlaneWebHandler.handler, |
| 476 | handleMcp: async (request, workspaceId) => { | ||
| 477 | const handler = resolveMcpHandler(workspaceId); | ||
| 478 | return handler(request); | ||
| 479 | }, | ||
| 382 | dispose: async () => { | 480 | dispose: async () => { |
| 383 | await controlPlaneWebHandler.dispose(); | 481 | await controlPlaneWebHandler.dispose(); |
| 384 | await persistence.close(); | 482 | await persistence.close(); |
| @@ -0,0 +1,181 @@ | |||
| 1 | import { createRemoteJWKSet, jwtVerify } from "jose"; | ||
| 2 | |||
| 3 | import { externalOriginFromRequest } from "../workos"; | ||
| 4 | |||
| 5 | type McpAuthConfig = { | ||
| 6 | enabled: boolean; | ||
| 7 | authorizationServer: string | null; | ||
| 8 | jwks: ReturnType<typeof createRemoteJWKSet> | null; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export type VerifiedMcpToken = { | ||
| 12 | subject: string; | ||
| 13 | email: string | null; | ||
| 14 | displayName: string | null; | ||
| 15 | }; | ||
| 16 | |||
| 17 | const trim = (value: string | undefined): string | undefined => { | ||
| 18 | const candidate = value?.trim(); | ||
| 19 | return candidate && candidate.length > 0 ? candidate : undefined; | ||
| 20 | }; | ||
| 21 | |||
| 22 | const toAuthorizationServerUrl = (value: string): string | null => { | ||
| 23 | const candidate = | ||
| 24 | value.startsWith("http://") || value.startsWith("https://") | ||
| 25 | ? value | ||
| 26 | : `https://${value}`; | ||
| 27 | |||
| 28 | try { | ||
| 29 | return new URL(candidate).origin; | ||
| 30 | } catch { | ||
| 31 | return null; | ||
| 32 | } | ||
| 33 | }; | ||
| 34 | |||
| 35 | const resolveAuthorizationServer = (): string | null => { | ||
| 36 | const candidates = [ | ||
| 37 | process.env.MCP_AUTHORIZATION_SERVER, | ||
| 38 | process.env.MCP_AUTHORIZATION_SERVER_URL, | ||
| 39 | process.env.WORKOS_AUTHKIT_ISSUER, | ||
| 40 | process.env.WORKOS_AUTHKIT_DOMAIN, | ||
| 41 | ]; | ||
| 42 | |||
| 43 | for (const candidate of candidates) { | ||
| 44 | const normalized = trim(candidate); | ||
| 45 | if (!normalized) { | ||
| 46 | continue; | ||
| 47 | } | ||
| 48 | |||
| 49 | const parsed = toAuthorizationServerUrl(normalized); | ||
| 50 | if (parsed) { | ||
| 51 | return parsed; | ||
| 52 | } | ||
| 53 | } | ||
| 54 | |||
| 55 | return null; | ||
| 56 | }; | ||
| 57 | |||
| 58 | let cachedAuthConfig: McpAuthConfig | null = null; | ||
| 59 | |||
| 60 | export const getMcpAuthConfig = (): McpAuthConfig => { | ||
| 61 | const authorizationServer = resolveAuthorizationServer(); | ||
| 62 | |||
| 63 | if ( | ||
| 64 | cachedAuthConfig | ||
| 65 | && cachedAuthConfig.authorizationServer === authorizationServer | ||
| 66 | ) { | ||
| 67 | return cachedAuthConfig; | ||
| 68 | } | ||
| 69 | |||
| 70 | if (!authorizationServer) { | ||
| 71 | cachedAuthConfig = { | ||
| 72 | enabled: false, | ||
| 73 | authorizationServer: null, | ||
| 74 | jwks: null, | ||
| 75 | }; | ||
| 76 | return cachedAuthConfig; | ||
| 77 | } | ||
| 78 | |||
| 79 | cachedAuthConfig = { | ||
| 80 | enabled: true, | ||
| 81 | authorizationServer, | ||
| 82 | jwks: createRemoteJWKSet(new URL("/oauth2/jwks", authorizationServer)), | ||
| 83 | }; | ||
| 84 | |||
| 85 | return cachedAuthConfig; | ||
| 86 | }; | ||
| 87 | |||
| 88 | const parseBearerToken = (request: Request): string | null => { | ||
| 89 | const header = request.headers.get("authorization"); | ||
| 90 | if (!header || !header.startsWith("Bearer ")) { | ||
| 91 | return null; | ||
| 92 | } | ||
| 93 | |||
| 94 | const token = header.slice("Bearer ".length).trim(); | ||
| 95 | return token.length > 0 ? token : null; | ||
| 96 | }; | ||
| 97 | |||
| 98 | export const resourceMetadataUrl = (request: Request): string => { | ||
| 99 | const url = new URL(request.url); | ||
| 100 | const origin = externalOriginFromRequest(request); | ||
| 101 | const metadata = new URL( | ||
| 102 | "/.well-known/oauth-protected-resource", | ||
| 103 | origin, | ||
| 104 | ); | ||
| 105 | metadata.search = url.search; | ||
| 106 | |||
| 107 | const resource = new URL(url.pathname, origin); | ||
| 108 | resource.search = url.search; | ||
| 109 | metadata.searchParams.set("resource", resource.toString()); | ||
| 110 | |||
| 111 | return metadata.toString(); | ||
| 112 | }; | ||
| 113 | |||
| 114 | export const unauthorizedMcpResponse = ( | ||
| 115 | request: Request, | ||
| 116 | message: string, | ||
| 117 | ): Response => { | ||
| 118 | const challenge = [ | ||
| 119 | 'Bearer error="unauthorized"', | ||
| 120 | 'error_description="Authorization needed"', | ||
| 121 | `resource_metadata="${resourceMetadataUrl(request)}"`, | ||
| 122 | ].join(", "); | ||
| 123 | |||
| 124 | return Response.json( | ||
| 125 | { error: message }, | ||
| 126 | { | ||
| 127 | status: 401, | ||
| 128 | headers: { | ||
| 129 | "WWW-Authenticate": challenge, | ||
| 130 | }, | ||
| 131 | }, | ||
| 132 | ); | ||
| 133 | }; | ||
| 134 | |||
| 135 | const readDisplayName = (payload: Record<string, unknown>): string | null => { | ||
| 136 | const direct = payload.name; | ||
| 137 | if (typeof direct === "string" && direct.trim().length > 0) { | ||
| 138 | return direct.trim(); | ||
| 139 | } | ||
| 140 | |||
| 141 | const given = typeof payload.given_name === "string" ? payload.given_name.trim() : ""; | ||
| 142 | const family = typeof payload.family_name === "string" ? payload.family_name.trim() : ""; | ||
| 143 | const combined = [given, family].filter((part) => part.length > 0).join(" "); | ||
| 144 | |||
| 145 | return combined.length > 0 ? combined : null; | ||
| 146 | }; | ||
| 147 | |||
| 148 | export const verifyMcpBearerToken = async ( | ||
| 149 | request: Request, | ||
| 150 | config: McpAuthConfig, | ||
| 151 | ): Promise<VerifiedMcpToken | null> => { | ||
| 152 | if (!config.enabled || !config.authorizationServer || !config.jwks) { | ||
| 153 | return null; | ||
| 154 | } | ||
| 155 | |||
| 156 | const token = parseBearerToken(request); | ||
| 157 | if (!token) { | ||
| 158 | return null; | ||
| 159 | } | ||
| 160 | |||
| 161 | try { | ||
| 162 | const { payload } = await jwtVerify(token, config.jwks, { | ||
| 163 | issuer: config.authorizationServer, | ||
| 164 | }); | ||
| 165 | |||
| 166 | if (typeof payload.sub !== "string" || payload.sub.length === 0) { | ||
| 167 | return null; | ||
| 168 | } | ||
| 169 | |||
| 170 | const email = typeof payload.email === "string" ? payload.email : null; | ||
| 171 | const displayName = readDisplayName(payload); | ||
| 172 | |||
| 173 | return { | ||
| 174 | subject: payload.sub, | ||
| 175 | email, | ||
| 176 | displayName, | ||
| 177 | }; | ||
| 178 | } catch { | ||
| 179 | return null; | ||
| 180 | } | ||
| 181 | }; | ||
| @@ -11,9 +11,13 @@ · 10 unmodified lines | |||
| 11 | "dependencies": { | 11 | "dependencies": { |
| 12 | "@effect-atom/atom": "^0.4.11", | 12 | "@effect-atom/atom": "^0.4.11", |
| 13 | "@effect-atom/atom-react": "^0.4.6", | 13 | "@effect-atom/atom-react": "^0.4.6", |
| 14 | "@executor-v2/engine": "workspace:*", | ||
| 14 | "@executor-v2/management-api": "workspace:*", | 15 | "@executor-v2/management-api": "workspace:*", |
| 15 | "@executor-v2/persistence-ports": "workspace:*", | 16 | "@executor-v2/persistence-ports": "workspace:*", |
| 16 | "@executor-v2/persistence-sql": "workspace:*", | 17 | "@executor-v2/persistence-sql": "workspace:*", |
| 18 | "@executor-v2/runtime-cloudflare-worker-loader": "workspace:*", | ||
| 19 | "@executor-v2/runtime-deno-subprocess": "workspace:*", | ||
| 20 | "@executor-v2/runtime-local-inproc": "workspace:*", | ||
| 17 | "@executor-v2/schema": "workspace:*", | 21 | "@executor-v2/schema": "workspace:*", |
| 18 | "@modelcontextprotocol/sdk": "^1.27.1", | 22 | "@modelcontextprotocol/sdk": "^1.27.1", |
| 19 | "@streamdown/code": "^1.0.3", | 23 | "@streamdown/code": "^1.0.3", |
| @@ -21,6 +25,7 @@ · 1 unmodified line | |||
| 21 | "class-variance-authority": "^0.7.1", | 25 | "class-variance-authority": "^0.7.1", |
| 22 | "clsx": "^2.1.1", | 26 | "clsx": "^2.1.1", |
| 23 | "effect": "catalog:", | 27 | "effect": "catalog:", |
| 28 | "jose": "^6.1.3", | ||
| 24 | "next": "^15.4.6", | 29 | "next": "^15.4.6", |
| 25 | "next-themes": "^0.4.6", | 30 | "next-themes": "^0.4.6", |
| 26 | "react": "^19.1.1", | 31 | "react": "^19.1.1", |
| @@ -47,9 +47,13 @@ · 46 unmodified lines | |||
| 47 | "dependencies": { | 47 | "dependencies": { |
| 48 | "@effect-atom/atom": "^0.4.11", | 48 | "@effect-atom/atom": "^0.4.11", |
| 49 | "@effect-atom/atom-react": "^0.4.6", | 49 | "@effect-atom/atom-react": "^0.4.6", |
| 50 | "@executor-v2/engine": "workspace:*", | ||
| 50 | "@executor-v2/management-api": "workspace:*", | 51 | "@executor-v2/management-api": "workspace:*", |
| 51 | "@executor-v2/persistence-ports": "workspace:*", | 52 | "@executor-v2/persistence-ports": "workspace:*", |
| 52 | "@executor-v2/persistence-sql": "workspace:*", | 53 | "@executor-v2/persistence-sql": "workspace:*", |
| 54 | "@executor-v2/runtime-cloudflare-worker-loader": "workspace:*", | ||
| 55 | "@executor-v2/runtime-deno-subprocess": "workspace:*", | ||
| 56 | "@executor-v2/runtime-local-inproc": "workspace:*", | ||
| 53 | "@executor-v2/schema": "workspace:*", | 57 | "@executor-v2/schema": "workspace:*", |
| 54 | "@modelcontextprotocol/sdk": "^1.27.1", | 58 | "@modelcontextprotocol/sdk": "^1.27.1", |
| 55 | "@streamdown/code": "^1.0.3", | 59 | "@streamdown/code": "^1.0.3", |
| @@ -57,6 +61,7 @@ · 1 unmodified line | |||
| 57 | "class-variance-authority": "^0.7.1", | 61 | "class-variance-authority": "^0.7.1", |
| 58 | "clsx": "^2.1.1", | 62 | "clsx": "^2.1.1", |
| 59 | "effect": "catalog:", | 63 | "effect": "catalog:", |
| 64 | "jose": "^6.1.3", | ||
| 60 | "next": "^15.4.6", | 65 | "next": "^15.4.6", |
| 61 | "next-themes": "^0.4.6", | 66 | "next-themes": "^0.4.6", |
| 62 | "react": "^19.1.1", | 67 | "react": "^19.1.1", |
| @@ -1472,7 +1477,7 @@ · 1409 unmodified lines | |||
| 1472 | 1477 | ||
| 1473 | "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], | 1478 | "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], |
| 1474 | 1479 | ||
| 1475 | "jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], | 1480 | "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], |
| 1476 | 1481 | ||
| 1477 | "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], | 1482 | "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], |
| 1478 | 1483 | ||
| @@ -2348,8 +2353,6 @@ · 869 unmodified lines | |||
| 2348 | 2353 | ||
| 2349 | "@mapbox/node-pre-gyp/tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="], | 2354 | "@mapbox/node-pre-gyp/tar": ["tar@7.5.9", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg=="], |
| 2350 | 2355 | ||
| 2351 | "@modelcontextprotocol/sdk/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], | ||
| 2352 | |||
| 2353 | "@neondatabase/serverless/@types/node": ["@types/node@22.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw=="], | 2356 | "@neondatabase/serverless/@types/node": ["@types/node@22.19.13", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw=="], |
| 2354 | 2357 | ||
| 2355 | "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], | 2358 | "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], |
| @@ -2430,7 +2433,7 @@ · 74 unmodified lines | |||
| 2430 | 2433 | ||
| 2431 | "@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="], | 2434 | "@vercel/static-config/ajv": ["ajv@8.6.3", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw=="], |
| 2432 | 2435 | ||
| 2433 | "@workos-inc/node/jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], | 2436 | "@workos-inc/authkit-nextjs/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], |
| 2434 | 2437 | ||
| 2435 | "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], | 2438 | "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], |
| 2436 | 2439 | ||
| @@ -2522,6 +2525,8 @@ · 85 unmodified lines | |||
| 2522 | 2525 | ||
| 2523 | "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], | 2526 | "unstorage/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], |
| 2524 | 2527 | ||
| 2528 | "vercel/jose": ["jose@5.9.6", "", {}, "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ=="], | ||
| 2529 | |||
| 2525 | "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], | 2530 | "vscode-json-languageservice/jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], |
| 2526 | 2531 | ||
| 2527 | "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], | 2532 | "widest-line/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], |
| @@ -12,8 +12,13 @@ const readWasmBytes = async (): Promise<Uint8Array> => { · 11 unmodified lines | |||
| 12 | const candidates: string[] = []; | 12 | const candidates: string[] = []; |
| 13 | 13 | ||
| 14 | try { | 14 | try { |
| 15 | const wasmModuleUrl = new URL( | ||
| 16 | "./openapi-extractor-wasm/openapi_extractor_bg.wasm", | ||
| 17 | import.meta.url, | ||
| 18 | ).toString(); | ||
| 19 | |||
| 15 | candidates.push( | 20 | candidates.push( |
| 16 | fileURLToPath(new URL("./openapi-extractor-wasm/openapi_extractor_bg.wasm", import.meta.url)), | 21 | fileURLToPath(wasmModuleUrl), |
| 17 | ); | 22 | ); |
| 18 | } catch { | 23 | } catch { |
| 19 | // Next.js serverless bundling can provide non-URL import.meta.url values. | 24 | // Next.js serverless bundling can provide non-URL import.meta.url values. |
| @@ -131,7 +131,7 @@ const defaultDenoExecutable = (): string => { · 130 unmodified lines | |||
| 131 | }; | 131 | }; |
| 132 | 132 | ||
| 133 | const workerScriptPath = fileURLToPath( | 133 | const workerScriptPath = fileURLToPath( |
| 134 | new URL("./deno-subprocess-worker.mjs", import.meta.url), | 134 | new URL("./deno-subprocess-worker.mjs", import.meta.url).toString(), |
| 135 | ); | 135 | ); |
| 136 | 136 | ||
| 137 | const writeMessage = ( | 137 | const writeMessage = ( |