Use the Convex /mcp endpoint directly in local and managed runtime paths, and update setup/docs/env wiring to remove localhost gateway-specific configuration.
Unified · Split
| @@ -25,8 +25,6 @@ CONVEX_SITE_URL=https://your-deployment.convex.site · 24 unmodified lines | ||
| 25 | 25 | # EXECUTOR_BACKEND_PORT=5410 |
| 26 | 26 | # EXECUTOR_BACKEND_SITE_PORT=5411 |
| 27 | 27 | # EXECUTOR_WEB_PORT=5312 |
| 28 | −# EXECUTOR_MCP_GATEWAY_PORT=5313 | |
| 29 | −# NEXT_PUBLIC_LOCAL_MCP_ORIGIN=http://localhost:4313 | |
| 30 | 28 | |
| 31 | 29 | # ── Discord bot (optional — omit to skip bot startup) ── |
| 32 | 30 | # DISCORD_BOT_TOKEN=... |
| @@ -63,7 +63,7 @@ bun run dev · 62 unmodified lines | ||
| 63 | 63 | |
| 64 | 64 | - Convex dev watcher for `executor/` |
| 65 | 65 | - Executor web app (`http://localhost:4312`) |
| 66 | −- Executor MCP gateway (`http://localhost:4313/mcp`) | |
| 66 | +- Executor MCP endpoint (`<CONVEX_SITE_URL>/mcp`) | |
| 67 | 67 | - Assistant server (`http://localhost:3000`) |
| 68 | 68 | - Discord bot (only if `DISCORD_BOT_TOKEN` is set) |
| 69 | 69 | |
| @@ -8,9 +8,8 @@ · 7 unmodified lines | ||
| 8 | 8 | * Starts: |
| 9 | 9 | * 1. Convex cloud dev function watcher ─┐ |
| 10 | 10 | * 2. Executor web UI (port 4312) ├─ all started concurrently |
| 11 | − * 3. Executor MCP gateway (port 4313) │ | |
| 12 | − * 4. Assistant server (port 3000) │ | |
| 13 | − * 5. Discord bot ─┘ | |
| 11 | + * 3. Assistant server (port 3000) │ | |
| 12 | + * 4. Discord bot ─┘ | |
| 14 | 13 | * |
| 15 | 14 | * All processes are killed when this script exits (Ctrl+C). |
| 16 | 15 | * PIDs are written to .dev.pids for use with `bun run kill:all`. |
| @@ -24,7 +23,6 @@ const PID_FILE = join(import.meta.dir, ".dev.pids"); · 7 unmodified lines | ||
| 24 | 23 | const colors = { |
| 25 | 24 | convex: "\x1b[36m", // cyan |
| 26 | 25 | web: "\x1b[34m", // blue |
| 27 | − mcp: "\x1b[33m", // yellow | |
| 28 | 26 | assistant: "\x1b[32m", // green |
| 29 | 27 | bot: "\x1b[35m", // magenta |
| 30 | 28 | reset: "\x1b[0m", |
| @@ -119,8 +117,7 @@ process.on("SIGTERM", shutdown); · 88 unmodified lines | ||
| 119 | 117 | // ── Kill stale processes from a previous run ── |
| 120 | 118 | |
| 121 | 119 | const EXECUTOR_WEB_PORT = Number(Bun.env.EXECUTOR_WEB_PORT ?? 4312); |
| 122 | −const EXECUTOR_MCP_PORT = Number(Bun.env.EXECUTOR_MCP_GATEWAY_PORT ?? 4313); | |
| 123 | −const DEV_PORTS = [3000, EXECUTOR_WEB_PORT, EXECUTOR_MCP_PORT]; | |
| 120 | +const DEV_PORTS = [3000, EXECUTOR_WEB_PORT]; | |
| 124 | 121 | |
| 125 | 122 | async function killStaleProcesses() { |
| 126 | 123 | let killed = 0; |
| @@ -180,16 +177,6 @@ spawnService("web", ["bun", "run", "dev"], { · 53 unmodified lines | ||
| 180 | 177 | cwd: "./executor/apps/web", |
| 181 | 178 | }); |
| 182 | 179 | |
| 183 | −spawnService("mcp", ["bun", "run", "dev:mcp-gateway"], { | |
| 184 | − cwd: "./executor", | |
| 185 | − env: { | |
| 186 | − EXECUTOR_MCP_GATEWAY_PORT: String(EXECUTOR_MCP_PORT), | |
| 187 | − MCP_GATEWAY_REQUIRE_AUTH: "0", | |
| 188 | − MCP_AUTHORIZATION_SERVER: "", | |
| 189 | − MCP_AUTHORIZATION_SERVER_URL: "", | |
| 190 | − }, | |
| 191 | −}); | |
| 192 | − | |
| 193 | 180 | spawnService("assistant", ["bun", "run", "dev"], { |
| 194 | 181 | cwd: "./assistant/packages/server", |
| 195 | 182 | env: { |
| @@ -4,7 +4,7 @@ Executor is a Convex-native execution platform for MCP-driven agents. It provide · 3 unmodified lines | ||
| 4 | 4 | |
| 5 | 5 | - task execution (`run_code`) with tool invocation and approval gates |
| 6 | 6 | - workspace-scoped policy, credentials, and tool source management |
| 7 | −- MCP endpoints (Convex HTTP routes and a standalone Bun gateway) | |
| 7 | +- MCP endpoints (Convex HTTP routes) | |
| 8 | 8 | - a Next.js web app for tasks, approvals, tools, members, and billing |
| 9 | 9 | - a binary-first install flow for local self-hosted runtime |
| 10 | 10 | |
| @@ -16,7 +16,6 @@ Core components: · 5 unmodified lines | ||
| 16 | 16 | - `convex/http.ts`: HTTP routes for `/mcp`, OAuth discovery metadata, and internal runtime callbacks. |
| 17 | 17 | - `convex/executorNode.ts`: task runner action (`runTask`) and tool invocation plumbing. |
| 18 | 18 | - `lib/`: runtime engine, typechecker, tool discovery, external source adapters (MCP/OpenAPI/GraphQL), credential provider resolvers. |
| 19 | −- `mcp-gateway.ts`: standalone stateful MCP gateway with anonymous OAuth and optional WorkOS token verification. | |
| 20 | 19 | - `apps/web/`: operator UI (dashboard, tasks, approvals, tools, onboarding, org settings). |
| 21 | 20 | - `executor.ts`: CLI entrypoint used by local source scripts and compiled binary releases. |
| 22 | 21 | |
| @@ -50,16 +49,11 @@ bun run dev:executor:convex · 27 unmodified lines | ||
| 50 | 49 | |
| 51 | 50 | # Terminal 2: Web UI |
| 52 | 51 | bun run dev:executor:web |
| 53 | − | |
| 54 | −# Terminal 3 (optional but recommended for stateful MCP transport) | |
| 55 | −bun run --cwd executor dev:mcp-gateway | |
| 56 | 52 | ``` |
| 57 | 53 | |
| 58 | 54 | Default source-dev endpoints: |
| 59 | 55 | |
| 60 | 56 | - Web UI: `http://localhost:4312` |
| 61 | −- MCP gateway: `http://localhost:4313/mcp` | |
| 62 | −- Gateway health: `http://localhost:4313/health` | |
| 63 | 57 | - Convex HTTP MCP route: `<CONVEX_SITE_URL>/mcp` |
| 64 | 58 | |
| 65 | 59 | ## Binary Install (No Global Bun/Node/Convex Required) |
| @@ -83,7 +77,6 @@ executor doctor · 17 unmodified lines | ||
| 83 | 77 | executor up |
| 84 | 78 | executor backend --help |
| 85 | 79 | executor web |
| 86 | −executor gateway | |
| 87 | 80 | ``` |
| 88 | 81 | |
| 89 | 82 | Uninstall: |
| @@ -97,7 +90,6 @@ Default managed-runtime ports: · 7 unmodified lines | ||
| 97 | 90 | - backend API: `5410` |
| 98 | 91 | - backend site proxy: `5411` |
| 99 | 92 | - packaged web app: `5312` |
| 100 | −- MCP gateway: `5313` | |
| 101 | 93 | |
| 102 | 94 | ## CLI Commands (executor/package.json) |
| 103 | 95 | |
| @@ -108,7 +100,6 @@ bun run doctor · 4 unmodified lines | ||
| 108 | 100 | bun run up |
| 109 | 101 | bun run backend -- --help |
| 110 | 102 | bun run web |
| 111 | −bun run gateway | |
| 112 | 103 | bun run codegen |
| 113 | 104 | bun run deploy |
| 114 | 105 | bun run build:binary |
| @@ -122,24 +113,15 @@ Notes: · 7 unmodified lines | ||
| 122 | 113 | |
| 123 | 114 | ## MCP and OAuth Surface |
| 124 | 115 | |
| 125 | −Gateway routes (`mcp-gateway.ts`): | |
| 116 | +Convex HTTP routes (`convex/http.ts`) expose: | |
| 126 | 117 | |
| 127 | −- `/mcp` (GET/POST/DELETE) | |
| 118 | +- `/mcp` (direct Convex MCP transport) | |
| 128 | 119 | - `/.well-known/oauth-protected-resource` |
| 129 | 120 | - `/.well-known/oauth-authorization-server` |
| 130 | −- `/oauth2/jwks` | |
| 131 | −- `/register` | |
| 132 | −- `/authorize` | |
| 133 | −- `/token` | |
| 134 | −- `/health` | |
| 135 | − | |
| 136 | −Convex HTTP routes (`convex/http.ts`) also expose: | |
| 137 | − | |
| 138 | −- `/mcp` (direct Convex MCP transport) | |
| 139 | 121 | - `/internal/runs/:runId/tool-call` |
| 140 | 122 | - `/internal/runs/:runId/output` |
| 141 | 123 | |
| 142 | −Anonymous OAuth is always available on the standalone gateway. WorkOS token verification is enabled when `MCP_AUTHORIZATION_SERVER` (or `MCP_AUTHORIZATION_SERVER_URL`) is configured. | |
| 124 | +MCP bearer-token verification is enabled when `MCP_AUTHORIZATION_SERVER` (or `MCP_AUTHORIZATION_SERVER_URL`) is configured. | |
| 143 | 125 | |
| 144 | 126 | ## Configuration Reference |
| 145 | 127 | |
| @@ -159,14 +141,11 @@ Important env vars (see root `.env.example` for the base template): · 13 unmodified lines | ||
| 159 | 141 | - `STRIPE_PRICE_ID` |
| 160 | 142 | - MCP auth integration: |
| 161 | 143 | - `MCP_AUTHORIZATION_SERVER` or `MCP_AUTHORIZATION_SERVER_URL` |
| 162 | −- Managed runtime and gateway: | |
| 144 | +- Managed runtime: | |
| 163 | 145 | - `EXECUTOR_RUNTIME_DIR` |
| 164 | 146 | - `EXECUTOR_BACKEND_PORT` |
| 165 | 147 | - `EXECUTOR_BACKEND_SITE_PORT` |
| 166 | 148 | - `EXECUTOR_WEB_PORT` |
| 167 | − - `EXECUTOR_MCP_GATEWAY_PORT` | |
| 168 | − - `EXECUTOR_INTERNAL_TOKEN` (recommended for Convex-backed anonymous OAuth key/client persistence) | |
| 169 | − - `NEXT_PUBLIC_LOCAL_MCP_ORIGIN` | |
| 170 | 149 | |
| 171 | 150 | ## Credential Providers |
| 172 | 151 | |
| @@ -198,17 +177,15 @@ bun run typecheck:executor · 25 unmodified lines | ||
| 198 | 177 | executor/ |
| 199 | 178 | |- apps/web/ # Next.js operator UI |
| 200 | 179 | |- convex/ # Convex functions, schema, auth, HTTP routes |
| 201 | −|- lib/ # runtime, tool loading/discovery, gateway helpers | |
| 180 | +|- lib/ # runtime, tool loading/discovery, helper modules | |
| 202 | 181 | |- scripts/build-release.ts # release artifact builder |
| 203 | 182 | |- executor.ts # CLI entrypoint (compiled into binary) |
| 204 | −|- mcp-gateway.ts # standalone MCP + OAuth gateway | |
| 205 | 183 | |- install # curl install script |
| 206 | 184 | `- uninstall # uninstall script |
| 207 | 185 | ``` |
| 208 | 186 | |
| 209 | 187 | ## Troubleshooting |
| 210 | 188 | |
| 211 | −- `401` on `/mcp`: the gateway expects OAuth bearer tokens; complete MCP OAuth client flow first. | |
| 189 | +- `401` on `/mcp`: verify your bearer token issuer matches `MCP_AUTHORIZATION_SERVER` (or disable MCP OAuth in local dev). | |
| 212 | 190 | - Web UI cannot load data: verify `CONVEX_URL` / `CONVEX_SITE_URL` and that Convex dev is running. |
| 213 | −- Gateway OAuth state resets between restarts: set `EXECUTOR_INTERNAL_TOKEN` so gateway can persist keys/clients in Convex. | |
| 214 | 191 | - Release build missing web archive for your platform: run `bun run build:release` on that target platform. |
| @@ -1,5 +1,15 @@ | ||
| 1 | 1 | import type { NextConfig } from "next"; |
| 2 | 2 | |
| 3 | +function toSiteUrl(convexUrl?: string): string | undefined { | |
| 4 | + if (!convexUrl) { | |
| 5 | + return undefined; | |
| 6 | + } | |
| 7 | + if (convexUrl.includes(".convex.cloud")) { | |
| 8 | + return convexUrl.replace(".convex.cloud", ".convex.site"); | |
| 9 | + } | |
| 10 | + return convexUrl; | |
| 11 | +} | |
| 12 | + | |
| 3 | 13 | const appShellRewriteExclusions = [ |
| 4 | 14 | "api(?:/|$)", |
| 5 | 15 | "_next(?:/|$)", |
| @@ -21,6 +31,10 @@ const nextConfig: NextConfig = { · 15 unmodified lines | ||
| 21 | 31 | // Map canonical env vars to NEXT_PUBLIC_ so they're available client-side. |
| 22 | 32 | // This lets us keep a single root .env without NEXT_PUBLIC_ prefixes. |
| 23 | 33 | NEXT_PUBLIC_CONVEX_URL: process.env.EXECUTOR_WEB_CONVEX_URL ?? process.env.CONVEX_URL, |
| 34 | + NEXT_PUBLIC_CONVEX_SITE_URL: | |
| 35 | + process.env.EXECUTOR_WEB_CONVEX_SITE_URL | |
| 36 | + ?? process.env.CONVEX_SITE_URL | |
| 37 | + ?? toSiteUrl(process.env.EXECUTOR_WEB_CONVEX_URL ?? process.env.CONVEX_URL), | |
| 24 | 38 | NEXT_PUBLIC_WORKOS_CLIENT_ID: process.env.WORKOS_CLIENT_ID, |
| 25 | 39 | NEXT_PUBLIC_STRIPE_PRICE_ID: process.env.STRIPE_PRICE_ID, |
| 26 | 40 | }, |
| @@ -41,10 +41,6 @@ function resolveMcpOrigin(windowOrigin: string): string { · 40 unmodified lines | ||
| 41 | 41 | return windowOrigin; |
| 42 | 42 | } |
| 43 | 43 | |
| 44 | −function isLocalHostname(hostname: string): boolean { | |
| 45 | − return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0"; | |
| 46 | −} | |
| 47 | − | |
| 48 | 44 | export function McpSetupCard({ |
| 49 | 45 | workspaceId, |
| 50 | 46 | actorId, |
| @@ -56,26 +52,12 @@ export function McpSetupCard({ · 5 unmodified lines | ||
| 56 | 52 | }) { |
| 57 | 53 | const [selectedProviderId, setSelectedProviderId] = useState(MCP_PROVIDERS[0]?.id ?? "claude-code"); |
| 58 | 54 | const [copiedKey, setCopiedKey] = useState<string | null>(null); |
| 59 | − const [urlMode, setUrlMode] = useState<"default" | "local">(() => { | |
| 60 | − if (typeof window === "undefined") { | |
| 61 | − return "default"; | |
| 62 | − } | |
| 63 | − return isLocalHostname(window.location.hostname) ? "local" : "default"; | |
| 64 | − }); | |
| 65 | 55 | |
| 66 | − const defaultOrigin = useMemo(() => { | |
| 56 | + const origin = useMemo(() => { | |
| 67 | 57 | if (typeof window === "undefined") return ""; |
| 68 | 58 | return resolveMcpOrigin(window.location.origin); |
| 69 | 59 | }, []); |
| 70 | 60 | |
| 71 | − const showLocalGatewayOption = useMemo(() => { | |
| 72 | − if (typeof window === "undefined") return false; | |
| 73 | − return isLocalHostname(window.location.hostname); | |
| 74 | − }, []); | |
| 75 | − | |
| 76 | − const localGatewayOrigin = (process.env.NEXT_PUBLIC_LOCAL_MCP_ORIGIN ?? "http://localhost:4313").trim().replace(/\/$/, ""); | |
| 77 | − const origin = urlMode === "local" ? localGatewayOrigin : defaultOrigin; | |
| 78 | − | |
| 79 | 61 | const mcpUrl = useMemo(() => { |
| 80 | 62 | const base = origin ? new URL("/mcp", origin) : new URL("http://localhost/mcp"); |
| 81 | 63 | if (workspaceId) base.searchParams.set("workspaceId", workspaceId); |
| @@ -112,19 +94,6 @@ export function McpSetupCard({ · 30 unmodified lines | ||
| 112 | 94 | <div className="space-y-5"> |
| 113 | 95 | <div className="space-y-1.5"> |
| 114 | 96 | <Label className="text-xs text-muted-foreground">MCP Server URL</Label> |
| 115 | − {showLocalGatewayOption ? ( | |
| 116 | − <div className="space-y-1.5"> | |
| 117 | − <Select value={urlMode} onValueChange={(value) => setUrlMode(value as "default" | "local")}> | |
| 118 | − <SelectTrigger className="h-8 text-xs bg-background"> | |
| 119 | − <SelectValue /> | |
| 120 | − </SelectTrigger> | |
| 121 | − <SelectContent> | |
| 122 | − <SelectItem value="local" className="text-xs">Local gateway (localhost:4313)</SelectItem> | |
| 123 | − <SelectItem value="default" className="text-xs">Hosted endpoint</SelectItem> | |
| 124 | − </SelectContent> | |
| 125 | − </Select> | |
| 126 | − </div> | |
| 127 | − ) : null} | |
| 128 | 97 | <div className="flex items-center gap-2"> |
| 129 | 98 | <Input value={mcpUrl} readOnly className="h-8 text-xs font-mono bg-background" /> |
| 130 | 99 | <Button |
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | #!/usr/bin/env bun |
| 2 | 2 | |
| 3 | 3 | import fs from "node:fs/promises"; |
| 4 | −import { startMcpGateway } from "./mcp-gateway"; | |
| 5 | 4 | import { managedRuntimeDiagnostics, runManagedBackend, runManagedWeb } from "./lib/managed_runtime"; |
| 6 | 5 | |
| 7 | 6 | function printHelp(): void { |
| @@ -12,14 +11,12 @@ Usage: · 4 unmodified lines | ||
| 12 | 11 | executor up [backend-args] |
| 13 | 12 | executor backend <args> |
| 14 | 13 | executor web [--port <number>] |
| 15 | − executor gateway [--port <number>] | |
| 16 | 14 | |
| 17 | 15 | Commands: |
| 18 | 16 | doctor Bootstrap and verify managed Convex backend runtime |
| 19 | 17 | up Run managed backend and auto-bootstrap Convex functions |
| 20 | 18 | backend Pass through arguments to managed convex-local-backend binary |
| 21 | 19 | web Run packaged web UI (default port: 5312) |
| 22 | − gateway Start Executor MCP gateway (default port: 5313) | |
| 23 | 20 | `); |
| 24 | 21 | } |
| 25 | 22 | |
| @@ -75,12 +72,10 @@ async function run(): Promise<void> { · 49 unmodified lines | ||
| 75 | 72 | if (command === "doctor") { |
| 76 | 73 | const info = await managedRuntimeDiagnostics(); |
| 77 | 74 | const webPort = Number(Bun.env.EXECUTOR_WEB_PORT ?? 5312); |
| 78 | − const gatewayPort = Number(Bun.env.EXECUTOR_MCP_GATEWAY_PORT ?? 5313); | |
| 79 | 75 | const webInstalled = await pathExists(info.webServerEntry); |
| 80 | 76 | const nodeInstalled = await pathExists(info.nodeBin); |
| 81 | 77 | const backendRunning = await checkHttp(`${info.convexUrl}/version`); |
| 82 | 78 | const webRunning = await checkHttp(`http://127.0.0.1:${webPort}/`); |
| 83 | − const gatewayRunning = await checkHttp(`http://127.0.0.1:${gatewayPort}/health`); | |
| 84 | 79 | |
| 85 | 80 | console.log("Managed runtime ready"); |
| 86 | 81 | console.log(` root: ${info.rootDir}`); |
| @@ -90,8 +85,8 @@ async function run(): Promise<void> { · 3 unmodified lines | ||
| 90 | 85 | console.log(` node runtime: ${nodeInstalled ? info.nodeBin : "not installed yet (installed by 'executor web')"}`); |
| 91 | 86 | console.log(` web bundle: ${webInstalled ? info.webServerEntry : "not installed yet (installed by 'executor web')"}`); |
| 92 | 87 | console.log(` web URL: http://127.0.0.1:${webPort}`); |
| 93 | − console.log(` gateway URL: http://127.0.0.1:${gatewayPort}/mcp`); | |
| 94 | − console.log(` running: backend=${backendRunning ? "yes" : "no"} web=${webRunning ? "yes" : "no"} gateway=${gatewayRunning ? "yes" : "no"}`); | |
| 88 | + console.log(` mcp URL: ${info.convexSiteUrl}/mcp`); | |
| 89 | + console.log(` running: backend=${backendRunning ? "yes" : "no"} web=${webRunning ? "yes" : "no"}`); | |
| 95 | 90 | console.log(` config: ${info.configPath}`); |
| 96 | 91 | return; |
| 97 | 92 | } |
| @@ -109,12 +104,6 @@ async function run(): Promise<void> { · 11 unmodified lines | ||
| 109 | 104 | process.exit(exitCode); |
| 110 | 105 | } |
| 111 | 106 | |
| 112 | − if (command === "gateway") { | |
| 113 | − const port = parsePort(rest); | |
| 114 | − startMcpGateway(port); | |
| 115 | − return; | |
| 116 | − } | |
| 117 | − | |
| 118 | 107 | if (command === "web") { |
| 119 | 108 | const port = parsePort(rest); |
| 120 | 109 | const exitCode = await runManagedWeb({ port }); |
| @@ -266,7 +266,6 @@ echo "Run:" · 265 unmodified lines | ||
| 266 | 266 | echo " executor doctor" |
| 267 | 267 | echo " executor up" |
| 268 | 268 | echo " executor web # defaults to http://localhost:5312" |
| 269 | −echo " executor gateway # defaults to http://localhost:5313/mcp" | |
| 270 | 269 | echo "" |
| 271 | 270 | echo "To uninstall later:" |
| 272 | 271 | echo " bash executor/uninstall --yes" |
| @@ -642,7 +642,6 @@ export async function runManagedWeb(options: { port?: number } = {}): Promise<nu · 641 unmodified lines | ||
| 642 | 642 | await ensureNodeRuntime(info); |
| 643 | 643 | await ensureWebBundle(info); |
| 644 | 644 | |
| 645 | − const mcpPort = Number(Bun.env.EXECUTOR_MCP_GATEWAY_PORT ?? 5313); | |
| 646 | 645 | const webPort = options.port ?? Number(Bun.env.EXECUTOR_WEB_PORT ?? 5312); |
| 647 | 646 | const host = Bun.env.EXECUTOR_WEB_INTERFACE ?? "127.0.0.1"; |
| 648 | 647 | |
| @@ -653,7 +652,6 @@ export async function runManagedWeb(options: { port?: number } = {}): Promise<nu · 4 unmodified lines | ||
| 653 | 652 | HOSTNAME: host, |
| 654 | 653 | CONVEX_URL: process.env.CONVEX_URL ?? `http://${info.config.hostInterface}:${info.config.backendPort}`, |
| 655 | 654 | CONVEX_SITE_URL: process.env.CONVEX_SITE_URL ?? `http://${info.config.hostInterface}:${info.config.siteProxyPort}`, |
| 656 | − NEXT_PUBLIC_LOCAL_MCP_ORIGIN: process.env.NEXT_PUBLIC_LOCAL_MCP_ORIGIN ?? `http://localhost:${mcpPort}`, | |
| 657 | 655 | }; |
| 658 | 656 | |
| 659 | 657 | const proc = await runProcess(info.nodeBin, [info.webServerEntry], { |
| @@ -1,532 +0,0 @@ | ||
| 1 | −import { ConvexHttpClient } from "convex/browser"; | |
| 2 | −import { createRemoteJWKSet, jwtVerify } from "jose"; | |
| 3 | −import { api } from "./convex/_generated/api"; | |
| 4 | −import { handleMcpRequest, type McpWorkspaceContext } from "./lib/mcp_server"; | |
| 5 | −import { AnonymousOAuthServer, OAuthBadRequest } from "./lib/anonymous-oauth"; | |
| 6 | −import { ConvexOAuthStorage } from "./lib/convex-oauth-storage"; | |
| 7 | −import type { AnonymousContext, PendingApprovalRecord, TaskRecord, ToolDescriptor } from "./lib/types"; | |
| 8 | −import type { Id } from "./convex/_generated/dataModel"; | |
| 9 | − | |
| 10 | −// --------------------------------------------------------------------------- | |
| 11 | −// Verified token result — unified across WorkOS and self-issued anonymous JWTs | |
| 12 | −// --------------------------------------------------------------------------- | |
| 13 | − | |
| 14 | −interface VerifiedToken { | |
| 15 | − sub: string; | |
| 16 | − provider: "workos" | "anonymous"; | |
| 17 | − /** The raw bearer token string (needed for WorkOS tokens to forward to Convex). */ | |
| 18 | − rawToken: string; | |
| 19 | −} | |
| 20 | − | |
| 21 | −// --------------------------------------------------------------------------- | |
| 22 | −// Configuration | |
| 23 | −// --------------------------------------------------------------------------- | |
| 24 | − | |
| 25 | −let convexUrl: string | undefined; | |
| 26 | −let workosAuthorizationServer: string | undefined; | |
| 27 | −let workosJwks: ReturnType<typeof createRemoteJWKSet> | null = null; | |
| 28 | −let anonOAuth: AnonymousOAuthServer | null = null; | |
| 29 | − | |
| 30 | −function loadConfigFromEnv(): void { | |
| 31 | − const convexUrlFromEnv = Bun.env.CONVEX_URL; | |
| 32 | − if (!convexUrlFromEnv) { | |
| 33 | − throw new Error("CONVEX_URL is required."); | |
| 34 | − } | |
| 35 | − | |
| 36 | − convexUrl = convexUrlFromEnv; | |
| 37 | − workosAuthorizationServer = | |
| 38 | − Bun.env.MCP_AUTHORIZATION_SERVER | |
| 39 | − ?? Bun.env.MCP_AUTHORIZATION_SERVER_URL | |
| 40 | − ?? Bun.env.WORKOS_AUTHKIT_ISSUER | |
| 41 | − ?? Bun.env.WORKOS_AUTHKIT_DOMAIN; | |
| 42 | − workosJwks = workosAuthorizationServer | |
| 43 | − ? createRemoteJWKSet(new URL("/oauth2/jwks", workosAuthorizationServer)) | |
| 44 | − : null; | |
| 45 | −} | |
| 46 | − | |
| 47 | −async function initAnonymousOAuth(issuer: string): Promise<void> { | |
| 48 | − const internalSecret = Bun.env.EXECUTOR_INTERNAL_TOKEN; | |
| 49 | − const storage = convexUrl && internalSecret | |
| 50 | − ? new ConvexOAuthStorage(convexUrl, internalSecret) | |
| 51 | − : undefined; | |
| 52 | − | |
| 53 | − anonOAuth = new AnonymousOAuthServer({ issuer, storage }); | |
| 54 | − await anonOAuth.init(); | |
| 55 | −} | |
| 56 | − | |
| 57 | −function requireConvexUrl(): string { | |
| 58 | − if (!convexUrl) { | |
| 59 | − throw new Error("CONVEX_URL is required."); | |
| 60 | − } | |
| 61 | − return convexUrl; | |
| 62 | −} | |
| 63 | − | |
| 64 | −// --------------------------------------------------------------------------- | |
| 65 | −// Token parsing & verification | |
| 66 | −// --------------------------------------------------------------------------- | |
| 67 | − | |
| 68 | −function parseBearerToken(request: Request): string | null { | |
| 69 | − const header = request.headers.get("authorization"); | |
| 70 | − if (!header || !header.startsWith("Bearer ")) { | |
| 71 | − return null; | |
| 72 | − } | |
| 73 | − const token = header.slice("Bearer ".length).trim(); | |
| 74 | − return token.length > 0 ? token : null; | |
| 75 | −} | |
| 76 | − | |
| 77 | −/** | |
| 78 | − * Unified token verification. Tries the self-issued anonymous OAuth server | |
| 79 | − * first (fast, in-process), then falls back to the external WorkOS JWKS. | |
| 80 | − */ | |
| 81 | −async function verifyToken(request: Request): Promise<VerifiedToken | null> { | |
| 82 | − const rawToken = parseBearerToken(request); | |
| 83 | − if (!rawToken) { | |
| 84 | − return null; | |
| 85 | − } | |
| 86 | − | |
| 87 | − // 1. Try the self-issued anonymous OAuth server | |
| 88 | − if (anonOAuth) { | |
| 89 | − const anon = await anonOAuth.verifyToken(rawToken); | |
| 90 | − if (anon) { | |
| 91 | − return { sub: anon.sub, provider: "anonymous", rawToken }; | |
| 92 | − } | |
| 93 | − } | |
| 94 | − | |
| 95 | − // 2. Try WorkOS | |
| 96 | − if (workosAuthorizationServer && workosJwks) { | |
| 97 | − try { | |
| 98 | − const { payload } = await jwtVerify(rawToken, workosJwks, { | |
| 99 | − issuer: workosAuthorizationServer, | |
| 100 | − }); | |
| 101 | − if (typeof payload.sub === "string" && payload.sub.length > 0) { | |
| 102 | − return { sub: payload.sub, provider: "workos", rawToken }; | |
| 103 | − } | |
| 104 | − } catch { | |
| 105 | − // Not a valid WorkOS token either | |
| 106 | − } | |
| 107 | − } | |
| 108 | − | |
| 109 | − return null; | |
| 110 | −} | |
| 111 | − | |
| 112 | −// --------------------------------------------------------------------------- | |
| 113 | −// OAuth response helpers | |
| 114 | −// --------------------------------------------------------------------------- | |
| 115 | − | |
| 116 | −function resourceMetadataUrl(request: Request): string { | |
| 117 | − const url = new URL(request.url); | |
| 118 | − return `${url.origin}/.well-known/oauth-protected-resource`; | |
| 119 | −} | |
| 120 | − | |
| 121 | −function unauthorizedMcpResponse(request: Request, message: string): Response { | |
| 122 | − const challenge = [ | |
| 123 | − 'Bearer error="unauthorized"', | |
| 124 | − 'error_description="Authorization needed"', | |
| 125 | − `resource_metadata="${resourceMetadataUrl(request)}"`, | |
| 126 | − ].join(", "); | |
| 127 | − | |
| 128 | − return Response.json( | |
| 129 | − { error: message }, | |
| 130 | − { | |
| 131 | − status: 401, | |
| 132 | − headers: { | |
| 133 | − "WWW-Authenticate": challenge, | |
| 134 | − }, | |
| 135 | − }, | |
| 136 | − ); | |
| 137 | −} | |
| 138 | − | |
| 139 | −// --------------------------------------------------------------------------- | |
| 140 | −// Workspace ID parsing | |
| 141 | −// --------------------------------------------------------------------------- | |
| 142 | − | |
| 143 | −/** | |
| 144 | − * Validated boundary for external workspace ID strings. | |
| 145 | − * Convex IDs are opaque strings at runtime; this cast is the single | |
| 146 | − * validated entry point so downstream code never needs ad-hoc casts. | |
| 147 | − */ | |
| 148 | −function parseWorkspaceId(raw: string): Id<"workspaces"> { | |
| 149 | − return raw as Id<"workspaces">; | |
| 150 | −} | |
| 151 | − | |
| 152 | −function parseRequestedContext(url: URL): { | |
| 153 | − workspaceId?: Id<"workspaces">; | |
| 154 | − clientId?: string; | |
| 155 | −} { | |
| 156 | − const rawWorkspaceId = url.searchParams.get("workspaceId"); | |
| 157 | − const workspaceId = rawWorkspaceId ? parseWorkspaceId(rawWorkspaceId) : undefined; | |
| 158 | − const clientId = url.searchParams.get("clientId") ?? undefined; | |
| 159 | − return { workspaceId, clientId }; | |
| 160 | −} | |
| 161 | − | |
| 162 | −// --------------------------------------------------------------------------- | |
| 163 | −// Service factory | |
| 164 | −// --------------------------------------------------------------------------- | |
| 165 | − | |
| 166 | −function createService(context?: McpWorkspaceContext, bearerToken?: string) { | |
| 167 | − const convex = new ConvexHttpClient(requireConvexUrl()); | |
| 168 | − if (bearerToken) { | |
| 169 | − convex.setAuth(bearerToken); | |
| 170 | − } | |
| 171 | − | |
| 172 | − const workspaceId = context?.workspaceId; | |
| 173 | − const sessionId = context?.sessionId; | |
| 174 | − | |
| 175 | − return { | |
| 176 | − createTask: async (input: { | |
| 177 | − code: string; | |
| 178 | − timeoutMs?: number; | |
| 179 | − runtimeId?: string; | |
| 180 | − metadata?: Record<string, unknown>; | |
| 181 | − workspaceId: Id<"workspaces">; | |
| 182 | − actorId: string; | |
| 183 | − clientId?: string; | |
| 184 | − }) => { | |
| 185 | − const created = await convex.mutation(api.executor.createTask, { | |
| 186 | − workspaceId: input.workspaceId, | |
| 187 | − sessionId, | |
| 188 | − code: input.code, | |
| 189 | − timeoutMs: input.timeoutMs, | |
| 190 | − runtimeId: input.runtimeId, | |
| 191 | − metadata: input.metadata, | |
| 192 | − actorId: input.actorId, | |
| 193 | − clientId: input.clientId, | |
| 194 | − }); | |
| 195 | − return created as { task: TaskRecord }; | |
| 196 | − }, | |
| 197 | − | |
| 198 | − getTask: async (taskId: string, workspace?: Id<"workspaces">) => { | |
| 199 | − const effectiveWorkspaceId = workspace ?? workspaceId; | |
| 200 | − if (!effectiveWorkspaceId) { | |
| 201 | − return null; | |
| 202 | − } | |
| 203 | − const task = await convex.query(api.workspace.getTaskInWorkspace, { | |
| 204 | − workspaceId: effectiveWorkspaceId, | |
| 205 | − sessionId, | |
| 206 | − taskId, | |
| 207 | − }); | |
| 208 | − return task as TaskRecord | null; | |
| 209 | − }, | |
| 210 | − | |
| 211 | − subscribe: () => () => {}, | |
| 212 | − | |
| 213 | − bootstrapAnonymousContext: async (requestedSessionId?: string) => { | |
| 214 | − const bootstrap = await convex.mutation(api.workspace.bootstrapAnonymousSession, { | |
| 215 | − sessionId: requestedSessionId, | |
| 216 | − }); | |
| 217 | − return bootstrap as AnonymousContext; | |
| 218 | − }, | |
| 219 | − | |
| 220 | − listTools: async (toolContext?: { workspaceId: Id<"workspaces">; actorId?: string; clientId?: string }) => { | |
| 221 | − if (!toolContext) { | |
| 222 | − return []; | |
| 223 | − } | |
| 224 | − const tools = await convex.action(api.executorNode.listTools, { | |
| 225 | − workspaceId: toolContext.workspaceId, | |
| 226 | − sessionId, | |
| 227 | − actorId: toolContext.actorId, | |
| 228 | − clientId: toolContext.clientId, | |
| 229 | − }); | |
| 230 | − return tools as ToolDescriptor[]; | |
| 231 | − }, | |
| 232 | − | |
| 233 | − listPendingApprovals: async (approvalWorkspaceId: Id<"workspaces">) => { | |
| 234 | − const approvals = await convex.query(api.workspace.listPendingApprovals, { | |
| 235 | − workspaceId: approvalWorkspaceId, | |
| 236 | − sessionId, | |
| 237 | − }); | |
| 238 | − return approvals as PendingApprovalRecord[]; | |
| 239 | − }, | |
| 240 | − | |
| 241 | − resolveApproval: async (input: { | |
| 242 | − workspaceId: Id<"workspaces">; | |
| 243 | − approvalId: string; | |
| 244 | − decision: "approved" | "denied"; | |
| 245 | − reviewerId?: string; | |
| 246 | − reason?: string; | |
| 247 | − }) => { | |
| 248 | − return await convex.mutation(api.executor.resolveApproval, { | |
| 249 | − workspaceId: input.workspaceId, | |
| 250 | − sessionId, | |
| 251 | − approvalId: input.approvalId, | |
| 252 | − decision: input.decision, | |
| 253 | − reviewerId: input.reviewerId, | |
| 254 | − reason: input.reason, | |
| 255 | − }); | |
| 256 | − }, | |
| 257 | − }; | |
| 258 | −} | |
| 259 | − | |
| 260 | −// --------------------------------------------------------------------------- | |
| 261 | −// Context resolution | |
| 262 | −// --------------------------------------------------------------------------- | |
| 263 | − | |
| 264 | −async function resolveContextForWorkos( | |
| 265 | − workspaceId: Id<"workspaces">, | |
| 266 | − bearerToken: string, | |
| 267 | − clientId?: string, | |
| 268 | −): Promise<McpWorkspaceContext> { | |
| 269 | − const convex = new ConvexHttpClient(requireConvexUrl()); | |
| 270 | − convex.setAuth(bearerToken); | |
| 271 | − | |
| 272 | − const requestContext = await convex.query(api.workspace.getRequestContext, { | |
| 273 | − workspaceId, | |
| 274 | − }); | |
| 275 | − | |
| 276 | − return { | |
| 277 | − workspaceId: requestContext.workspaceId, | |
| 278 | − actorId: requestContext.actorId, | |
| 279 | − clientId, | |
| 280 | − }; | |
| 281 | −} | |
| 282 | − | |
| 283 | −// --------------------------------------------------------------------------- | |
| 284 | −// MCP handler | |
| 285 | −// --------------------------------------------------------------------------- | |
| 286 | − | |
| 287 | −async function handleMcp(request: Request): Promise<Response> { | |
| 288 | − const verified = await verifyToken(request); | |
| 289 | − | |
| 290 | − // No token at all — return 401 so MCP clients discover the auth server | |
| 291 | − if (!verified) { | |
| 292 | − return unauthorizedMcpResponse(request, "Authorization required."); | |
| 293 | − } | |
| 294 | − | |
| 295 | − const url = new URL(request.url); | |
| 296 | − const requested = parseRequestedContext(url); | |
| 297 | − | |
| 298 | − let context: McpWorkspaceContext | undefined; | |
| 299 | − | |
| 300 | − if (verified.provider === "workos") { | |
| 301 | − // WorkOS-authenticated user: must provide workspaceId | |
| 302 | − if (!requested.workspaceId) { | |
| 303 | − return Response.json( | |
| 304 | − { error: "workspaceId query parameter is required for authenticated users" }, | |
| 305 | − { status: 400 }, | |
| 306 | − ); | |
| 307 | − } | |
| 308 | − | |
| 309 | − try { | |
| 310 | − context = await resolveContextForWorkos( | |
| 311 | − requested.workspaceId, | |
| 312 | − verified.rawToken, | |
| 313 | − requested.clientId, | |
| 314 | − ); | |
| 315 | − } catch (error) { | |
| 316 | − return Response.json( | |
| 317 | − { error: error instanceof Error ? error.message : "Workspace authorization failed" }, | |
| 318 | − { status: 403 }, | |
| 319 | − ); | |
| 320 | − } | |
| 321 | − } | |
| 322 | − // Anonymous tokens: context is resolved lazily via bootstrapAnonymousContext | |
| 323 | − // in the MCP server's run_code handler (using the token sub as the actor). | |
| 324 | − // If workspaceId is provided, it will be passed through but the anonymous | |
| 325 | − // session bootstrap will create one if needed. | |
| 326 | − | |
| 327 | − const bearerToken = verified.provider === "workos" ? verified.rawToken : undefined; | |
| 328 | − const service = createService(context, bearerToken); | |
| 329 | − return await handleMcpRequest(service, request, context); | |
| 330 | −} | |
| 331 | − | |
| 332 | −// --------------------------------------------------------------------------- | |
| 333 | −// OAuth endpoints — self-issued anonymous auth server | |
| 334 | −// --------------------------------------------------------------------------- | |
| 335 | − | |
| 336 | −function handleProtectedResourceMetadata(request: Request): Response { | |
| 337 | − const url = new URL(request.url); | |
| 338 | − const authorizationServers: string[] = []; | |
| 339 | − | |
| 340 | − // Always advertise the self-issued anonymous auth server | |
| 341 | − if (anonOAuth) { | |
| 342 | − authorizationServers.push(anonOAuth.getIssuer()); | |
| 343 | − } | |
| 344 | − | |
| 345 | − // Also advertise WorkOS if configured | |
| 346 | − if (workosAuthorizationServer) { | |
| 347 | − authorizationServers.push(workosAuthorizationServer); | |
| 348 | − } | |
| 349 | − | |
| 350 | − if (authorizationServers.length === 0) { | |
| 351 | − return Response.json({ error: "No authorization servers configured" }, { status: 404 }); | |
| 352 | − } | |
| 353 | − | |
| 354 | − return Response.json({ | |
| 355 | − resource: `${url.origin}/mcp`, | |
| 356 | − authorization_servers: authorizationServers, | |
| 357 | − bearer_methods_supported: ["header"], | |
| 358 | − }); | |
| 359 | −} | |
| 360 | − | |
| 361 | −function handleAuthServerMetadata(): Response { | |
| 362 | − if (!anonOAuth) { | |
| 363 | − return Response.json({ error: "Anonymous OAuth server not initialized" }, { status: 500 }); | |
| 364 | − } | |
| 365 | − return Response.json(anonOAuth.getMetadata()); | |
| 366 | −} | |
| 367 | − | |
| 368 | −function handleJwks(): Response { | |
| 369 | − if (!anonOAuth) { | |
| 370 | − return Response.json({ error: "Anonymous OAuth server not initialized" }, { status: 500 }); | |
| 371 | − } | |
| 372 | − return Response.json(anonOAuth.getJwks(), { | |
| 373 | − headers: { "cache-control": "public, max-age=3600" }, | |
| 374 | − }); | |
| 375 | −} | |
| 376 | − | |
| 377 | −async function handleRegister(request: Request): Promise<Response> { | |
| 378 | − if (!anonOAuth) { | |
| 379 | − return Response.json({ error: "Anonymous OAuth server not initialized" }, { status: 500 }); | |
| 380 | − } | |
| 381 | − | |
| 382 | − let body: unknown; | |
| 383 | − try { | |
| 384 | − body = await request.json(); | |
| 385 | − } catch { | |
| 386 | − return Response.json({ error: "Invalid JSON body" }, { status: 400 }); | |
| 387 | − } | |
| 388 | − | |
| 389 | − try { | |
| 390 | − const registration = await anonOAuth.registerClient(body as any); | |
| 391 | − return Response.json(registration, { status: 201 }); | |
| 392 | − } catch (error) { | |
| 393 | − if (error instanceof OAuthBadRequest) { | |
| 394 | − return Response.json( | |
| 395 | − { error: "invalid_client_metadata", error_description: error.message }, | |
| 396 | − { status: 400 }, | |
| 397 | − ); | |
| 398 | − } | |
| 399 | − throw error; | |
| 400 | − } | |
| 401 | −} | |
| 402 | − | |
| 403 | −async function handleAuthorize(request: Request): Promise<Response> { | |
| 404 | − if (!anonOAuth) { | |
| 405 | − return Response.json({ error: "Anonymous OAuth server not initialized" }, { status: 500 }); | |
| 406 | − } | |
| 407 | − | |
| 408 | − const url = new URL(request.url); | |
| 409 | − try { | |
| 410 | − const { redirectTo } = await anonOAuth.authorize(url.searchParams); | |
| 411 | − return Response.redirect(redirectTo, 302); | |
| 412 | − } catch (error) { | |
| 413 | − if (error instanceof OAuthBadRequest) { | |
| 414 | − return Response.json( | |
| 415 | − { error: "invalid_request", error_description: error.message }, | |
| 416 | − { status: 400 }, | |
| 417 | − ); | |
| 418 | − } | |
| 419 | − throw error; | |
| 420 | − } | |
| 421 | −} | |
| 422 | − | |
| 423 | −async function handleToken(request: Request): Promise<Response> { | |
| 424 | − if (!anonOAuth) { | |
| 425 | − return Response.json({ error: "Anonymous OAuth server not initialized" }, { status: 500 }); | |
| 426 | − } | |
| 427 | − | |
| 428 | − let body: URLSearchParams; | |
| 429 | − try { | |
| 430 | − const text = await request.text(); | |
| 431 | − body = new URLSearchParams(text); | |
| 432 | − } catch { | |
| 433 | − return Response.json({ error: "Invalid request body" }, { status: 400 }); | |
| 434 | − } | |
| 435 | − | |
| 436 | − try { | |
| 437 | − const tokens = await anonOAuth.exchangeToken(body); | |
| 438 | − return Response.json(tokens, { | |
| 439 | − headers: { | |
| 440 | − "cache-control": "no-store", | |
| 441 | − "pragma": "no-cache", | |
| 442 | − }, | |
| 443 | − }); | |
| 444 | − } catch (error) { | |
| 445 | − if (error instanceof OAuthBadRequest) { | |
| 446 | − return Response.json( | |
| 447 | − { error: "invalid_grant", error_description: error.message }, | |
| 448 | − { status: 400 }, | |
| 449 | − ); | |
| 450 | − } | |
| 451 | − throw error; | |
| 452 | − } | |
| 453 | −} | |
| 454 | − | |
| 455 | −// --------------------------------------------------------------------------- | |
| 456 | −// Server | |
| 457 | −// --------------------------------------------------------------------------- | |
| 458 | − | |
| 459 | −export async function startMcpGateway( | |
| 460 | − port = Number(Bun.env.EXECUTOR_MCP_GATEWAY_PORT ?? Bun.env.PORT ?? 5313), | |
| 461 | −): Promise<ReturnType<typeof Bun.serve>> { | |
| 462 | − loadConfigFromEnv(); | |
| 463 | − | |
| 464 | − // Initialize the self-issued anonymous OAuth server. | |
| 465 | − // The issuer must match the gateway's public origin. | |
| 466 | − const gatewayOrigin = Bun.env.MCP_GATEWAY_ORIGIN ?? `http://localhost:${port}`; | |
| 467 | − await initAnonymousOAuth(gatewayOrigin); | |
| 468 | − | |
| 469 | − // Periodically purge expired authorization codes | |
| 470 | − setInterval(() => { | |
| 471 | − anonOAuth?.purgeExpiredCodes(); | |
| 472 | − }, 60_000); | |
| 473 | − | |
| 474 | − const server = Bun.serve({ | |
| 475 | − port, | |
| 476 | − async fetch(request) { | |
| 477 | − const url = new URL(request.url); | |
| 478 | − | |
| 479 | − if (url.pathname === "/health") { | |
| 480 | − return new Response("ok"); | |
| 481 | − } | |
| 482 | − | |
| 483 | − // ── MCP endpoint ────────────────────────────────────────────────── | |
| 484 | − if (url.pathname === "/" || url.pathname === "/mcp") { | |
| 485 | − if (request.method === "POST" || request.method === "GET" || request.method === "DELETE") { | |
| 486 | − return await handleMcp(request); | |
| 487 | − } | |
| 488 | − return new Response("Method Not Allowed", { status: 405 }); | |
| 489 | − } | |
| 490 | − | |
| 491 | − // ── OAuth discovery (RFC 9728 + RFC 8414) ───────────────────────── | |
| 492 | − if (url.pathname === "/.well-known/oauth-protected-resource") { | |
| 493 | − return handleProtectedResourceMetadata(request); | |
| 494 | − } | |
| 495 | − | |
| 496 | − if (url.pathname === "/.well-known/oauth-authorization-server") { | |
| 497 | − return handleAuthServerMetadata(); | |
| 498 | − } | |
| 499 | − | |
| 500 | − // ── Self-issued OAuth endpoints ─────────────────────────────────── | |
| 501 | − if (url.pathname === "/oauth2/jwks") { | |
| 502 | − return handleJwks(); | |
| 503 | − } | |
| 504 | − | |
| 505 | − if (url.pathname === "/register" && request.method === "POST") { | |
| 506 | − return await handleRegister(request); | |
| 507 | − } | |
| 508 | − | |
| 509 | − if (url.pathname === "/authorize" && request.method === "GET") { | |
| 510 | − return await handleAuthorize(request); | |
| 511 | − } | |
| 512 | − | |
| 513 | − if (url.pathname === "/token" && request.method === "POST") { | |
| 514 | − return await handleToken(request); | |
| 515 | − } | |
| 516 | − | |
| 517 | − return new Response("Not found", { status: 404 }); | |
| 518 | − }, | |
| 519 | − }); | |
| 520 | − | |
| 521 | − const hasWorkos = Boolean(workosAuthorizationServer); | |
| 522 | − console.log( | |
| 523 | − `[executor-mcp-gateway] listening on http://localhost:${port}/mcp` | |
| 524 | − + ` (anonymous-oauth: enabled, workos: ${hasWorkos ? "enabled" : "disabled"})`, | |
| 525 | − ); | |
| 526 | − | |
| 527 | − return server; | |
| 528 | −} | |
| 529 | − | |
| 530 | −if (import.meta.main) { | |
| 531 | − await startMcpGateway(); | |
| 532 | −} | |
| @@ -8,13 +8,10 @@ · 7 unmodified lines | ||
| 8 | 8 | "backend": "bun run executor.ts backend", |
| 9 | 9 | "convex": "bun run executor.ts backend", |
| 10 | 10 | "web": "bun run executor.ts web", |
| 11 | − "gateway": "bun run executor.ts gateway", | |
| 12 | 11 | "deploy": "bunx convex deploy", |
| 13 | 12 | "codegen": "bunx convex codegen", |
| 14 | 13 | "build:binary": "bun build executor.ts --compile --outfile dist/executor", |
| 15 | − "build:release": "bun run scripts/build-release.ts", | |
| 16 | − "dev:mcp-gateway": "EXECUTOR_MCP_GATEWAY_PORT=4313 bun --hot mcp-gateway.ts", | |
| 17 | − "start:mcp-gateway": "bun mcp-gateway.ts" | |
| 14 | + "build:release": "bun run scripts/build-release.ts" | |
| 18 | 15 | }, |
| 19 | 16 | "dependencies": { |
| 20 | 17 | "convex": "^1.31.7" |
| @@ -81,7 +81,7 @@ async function buildWebArtifact(rootDir: string, releaseDir: string, checksums: · 80 unmodified lines | ||
| 81 | 81 | const webBuildEnv = { |
| 82 | 82 | ...process.env, |
| 83 | 83 | EXECUTOR_WEB_CONVEX_URL: "http://127.0.0.1:5410", |
| 84 | − NEXT_PUBLIC_LOCAL_MCP_ORIGIN: "http://localhost:5313", | |
| 84 | + EXECUTOR_WEB_CONVEX_SITE_URL: "http://127.0.0.1:5411", | |
| 85 | 85 | }; |
| 86 | 86 | |
| 87 | 87 | await runCommand(["bunx", "next", "build"], { |
| @@ -21,7 +21,6 @@ · 20 unmodified lines | ||
| 21 | 21 | "lib/**/*.ts", |
| 22 | 22 | "scripts/**/*.ts", |
| 23 | 23 | "executor.ts", |
| 24 | − "mcp-gateway.ts", | |
| 25 | 24 | "vscode-elicitation-server.ts" |
| 26 | 25 | ] |
| 27 | 26 | } |