(server)
| 63 | name: "executor-selfhost-api", |
| 64 | apply: "serve", |
| 65 | configureServer(server) { |
| 66 | server.watcher.on("change", (path) => { |
| 67 | if (path.includes("/src/") || path.endsWith("/executor.config.ts")) handlerPromise = null; |
| 68 | }); |
| 69 | server.middlewares.use(async (req, res, next) => { |
| 70 | let rawUrl = req.url ?? "/"; |
| 71 | // The "Connect an agent" card prints `/<organizationId>/mcp`; self-host |
| 72 | // serves the bare `/mcp`, so rewrite it here (prod does the same in |
| 73 | // serve.ts) — otherwise this org-pinned path isn't recognized as an MCP |
| 74 | // path and falls through to the SPA as a 404. Mirrors ./src/mcp/org-path. |
| 75 | const devOrigin = `http://${req.headers.host ?? `localhost:${DEV_PORT}`}`; |
| 76 | const originalPathname = new URL(rawUrl, devOrigin).pathname; |
| 77 | const pathname = stripMcpOrgSegment(originalPathname) ?? ""; |
| 78 | // Carries the ORIGINAL org-scoped pathname through to the handler (see |
| 79 | // ./src/mcp/auth.ts) so the protected-resource metadata can echo it |
| 80 | // back to a client that dialed org-scoped — mirrors serve.ts's prod |
| 81 | // middleware. Set only when we ourselves rewrote this request; any |
| 82 | // client-supplied value is dropped below so it can't be spoofed. |
| 83 | let originalPathHeader: string | null = null; |
| 84 | if (pathname !== "") { |
| 85 | const original = new URL(rawUrl, devOrigin); |
| 86 | rawUrl = `${pathname}${original.search}`; |
| 87 | originalPathHeader = originalPathname; |
| 88 | } |
| 89 | // Match on PATHNAME, not a raw-URL prefix: `/mcp` must NOT swallow the |
| 90 | // SPA route `/mcp-consent`, or the dev server misroutes it to the API |
| 91 | // handler and returns a 404. |
| 92 | const path = new URL(rawUrl, devOrigin).pathname; |
| 93 | const handled = |
| 94 | path === "/api" || |
| 95 | path.startsWith("/api/") || |
| 96 | path === "/mcp" || |
| 97 | path.startsWith("/mcp/") || |
| 98 | path === "/docs" || |
| 99 | path.startsWith("/docs/") || |
| 100 | // Un-prefixed app-level routes (e.g. `/v1/app/npm/dist-tags`, which the |
| 101 | // shell's update check fetches). Served by the Effect router in prod; |
| 102 | // without this the SPA index.html fallback answers 200-with-HTML and |
| 103 | // the JSON parse fails, so the UpdateCard never appears. |
| 104 | path === "/v1" || |
| 105 | path.startsWith("/v1/") || |
| 106 | // RFC 9728 / RFC 8414 OAuth discovery the MCP client fetches before |
| 107 | // auth. Served by the Effect router in prod; without this the SPA |
| 108 | // index.html fallback answers 200-with-HTML and breaks discovery. |
| 109 | path.startsWith("/.well-known/"); |
| 110 | if (!handled) { |
| 111 | // SPA document navigations must receive the app shell, not a module. |
| 112 | // A browser navigating to a route like `/login` (Better Auth's |
| 113 | // MCP-OAuth `loginPage` 302s here as a real document GET) sends an |
| 114 | // extensionless request that Vite's transform middleware would resolve |
| 115 | // to a colliding web-root module (`web/login.tsx` shadows `/login`, |
| 116 | // `web/setup.tsx` shadows `/setup`) and serve as text/javascript, so |
| 117 | // the page renders as raw JS and the OAuth login never appears. Rewrite |
| 118 | // genuine page navigations to the index so Vite's html fallback serves |
| 119 | // index.html; the SPA reads window.location and renders the right |
| 120 | // route. Module/asset/HMR requests carry a file extension or a |
| 121 | // non-document fetch destination, so they fall through untouched. This |
| 122 | // is dev-only: the production static server already serves index.html |
nothing calls this directly
no test coverage detected