fix(mcp): RFC 9728 protected-resource metadata path + audience binding
All checks were successful
Deploy to Production / deploy (push) Successful in 1m31s
All checks were successful
Deploy to Production / deploy (push) Successful in 1m31s
Codex/RFC review showed that Claude Desktop addresses the MCP resource as <PUBLIC_URL>/mcp (the streamable-HTTP endpoint) rather than the base URL. Per RFC 9728 the protected-resource metadata then lives at .well-known/oauth-protected-resource inserted between host and path: https://mcp.buildmymcpserver.com/.well-known/oauth-protected-resource/<slug>/mcp Runner template now: - publishes `resource: <PUBLIC_URL>/mcp` - sets WWW-Authenticate to the RFC 9728 well-known URL - serves /.well-known/oauth-protected-resource[/*] so the metadata answers at both the legacy and RFC paths during transition - accepts both audiences (<PUBLIC_URL>/mcp + <PUBLIC_URL>) during rollout so already-issued tokens keep working API: - resolveServerByResource() tries port first, then path segment (production path-routing), with a guard against treating "mcp" as a tenant slug - AS metadata advertises resource_parameter_supported: true nginx (scripts/setup-runner-tls.sh + scripts/bmm-mcp-runners.nginx): - new location matches /.well-known/oauth-protected-resource/<slug>/... and proxies to the slug's runner with the slug stripped, so the runner sees the local well-known path Docs (oauth + api-reference) updated to the RFC paths.
This commit is contained in:
@@ -41,24 +41,9 @@ function pkceVerify(verifier: string, challenge: string, method: string): boolea
|
||||
async function resolveServerByResource(resource: string) {
|
||||
const url = new URL(resource);
|
||||
|
||||
// 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/<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;
|
||||
}
|
||||
|
||||
// Port-based lookup — used in local dev where the runner is reached
|
||||
// directly at http://<RUNNER_HOST>:<port>.
|
||||
// Local direct runner URLs are addressed by host port, e.g.
|
||||
// http://localhost:4103/mcp. Resolve those before path routing so the
|
||||
// transport endpoint segment is not treated as a tenant slug.
|
||||
const port = url.port ? Number(url.port) : null;
|
||||
if (port !== null) {
|
||||
const [s] = await db
|
||||
@@ -69,6 +54,22 @@ async function resolveServerByResource(resource: string) {
|
||||
if (s) return s;
|
||||
}
|
||||
|
||||
// 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/<slug>/mcp"
|
||||
// and look up the wrong server. Claude Desktop's RFC 8707 resource parameter
|
||||
// matches the `resource` field we publish in RFC 9728 protected resource
|
||||
// metadata, which is the path-routed MCP endpoint 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;
|
||||
}
|
||||
|
||||
// Subdomain routing — legacy / future <slug>.mcp.example.com setup.
|
||||
const slug = url.hostname.split('.')[0];
|
||||
if (slug && slug !== 'mcp') {
|
||||
@@ -119,6 +120,7 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
||||
'none',
|
||||
],
|
||||
scopes_supported: ['mcp:read', 'mcp:write'],
|
||||
resource_parameter_supported: true,
|
||||
};
|
||||
};
|
||||
const asMetadataHandler = async (_req: unknown, reply: { send: (body: unknown) => unknown }) =>
|
||||
@@ -439,4 +441,3 @@ export async function oauthRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user