fix(oauth): resolve server by path segment, not subdomain
All checks were successful
Deploy to Production / deploy (push) Successful in 1m24s
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:
parent
86cf89ef42
commit
1d845abf92
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user