fix(oauth): resolve server by path segment, not subdomain
All checks were successful
Deploy to Production / deploy (push) Successful in 1m24s

Claude Desktop got past discovery + DCR but /oauth/authorize rejected
the resource parameter with invalid_resource. Root cause:
resolveServerByResource() extracted the slug from the URL's first
hostname label (subdomain routing), but production runs path routing —
mcp.buildmymcpserver.com/<slug>. The function saw resource
"https://mcp.buildmymcpserver.com/text-generation", tried to look up
slug="mcp", missed, returned null → 400.

Path lookup is now tried first (matches the production topology and
the resource URL we publish via /.well-known/oauth-protected-resource),
port lookup second (local dev), subdomain lookup last with an explicit
"mcp" guard so the legacy path doesn't shadow the new one.
This commit is contained in:
Marco Sadjadi 2026-05-28 19:58:31 +02:00
parent 86cf89ef42
commit 1d845abf92

View File

@ -40,16 +40,42 @@ function pkceVerify(verifier: string, challenge: string, method: string): boolea
async function resolveServerByResource(resource: string) { async function resolveServerByResource(resource: string) {
const url = new URL(resource); const url = new URL(resource);
const port = url.port ? Number(url.port) : null;
if (port !== null) { // Path routing (the prod topology on mcp.buildmymcpserver.com): the slug
const [s] = await db.select().from(mcpServers).where(eq(mcpServers.hostPort, port)).limit(1); // is the first path segment. Has to be checked BEFORE the subdomain
// heuristic below, otherwise we extract "mcp" from "mcp.example.com/<slug>"
// 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; if (s) return s;
} }
// Port-based lookup — used in local dev where the runner is reached
// directly at http://<RUNNER_HOST>:<port>.
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 <slug>.mcp.example.com setup.
const slug = url.hostname.split('.')[0]; 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); const [s] = await db.select().from(mcpServers).where(eq(mcpServers.slug, slug)).limit(1);
if (s) return s; if (s) return s;
} }
return null; return null;
} }