diff --git a/apps/api/src/routes/oauth.ts b/apps/api/src/routes/oauth.ts index 79a0cea..00add85 100644 --- a/apps/api/src/routes/oauth.ts +++ b/apps/api/src/routes/oauth.ts @@ -40,16 +40,42 @@ function pkceVerify(verifier: string, challenge: string, method: string): boolea async function resolveServerByResource(resource: string) { const url = new URL(resource); - const port = url.port ? Number(url.port) : null; - if (port !== null) { - const [s] = await db.select().from(mcpServers).where(eq(mcpServers.hostPort, port)).limit(1); + + // Path routing (the prod topology on mcp.buildmymcpserver.com): the slug + // is the first path segment. Has to be checked BEFORE the subdomain + // heuristic below, otherwise we extract "mcp" from "mcp.example.com/" + // and look up the wrong (or no) server. Claude Desktop's RFC 8707 resource + // parameter matches the `resource` field we publish in /.well-known/ + // oauth-protected-resource, which is exactly the path-routed public URL. + const firstSegment = url.pathname.split('/').filter(Boolean)[0]; + if (firstSegment) { + const [s] = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.slug, firstSegment)) + .limit(1); if (s) return s; } + + // Port-based lookup — used in local dev where the runner is reached + // directly at http://:. + const port = url.port ? Number(url.port) : null; + if (port !== null) { + const [s] = await db + .select() + .from(mcpServers) + .where(eq(mcpServers.hostPort, port)) + .limit(1); + if (s) return s; + } + + // Subdomain routing — legacy / future .mcp.example.com setup. const slug = url.hostname.split('.')[0]; - if (slug) { + if (slug && slug !== 'mcp') { const [s] = await db.select().from(mcpServers).where(eq(mcpServers.slug, slug)).limit(1); if (s) return s; } + return null; }