fix(core): keep the configured MCP URL as the OAuth resource (#49241)

This commit is contained in:
Aiden Cline
2026-09-15 23:50:36 -05:00
committed by GitHub
Unverified
parent 7c3c50bac2
commit 6ee156e0b9
2 changed files with 44 additions and 1 deletions
+6 -1
View File
@@ -168,7 +168,12 @@ export const provider = (options: Options): OAuthClientProvider => {
if (!resource) return identity
if (!checkResourceAllowed({ requestedResource: identity, configuredResource: resource }))
throw new Error(`Protected resource ${resource} does not cover ${identity}`)
return new URL(resource)
const canonical = new URL(resource)
// The transport dials the configured URL with extra query parameters and some servers echo
// that back as the resource. Query is transport detail, not identity: the token stays bound
// to the configured URL so a later refresh names the same resource the login did.
if (canonical.origin === identity.origin && canonical.pathname === identity.pathname) return identity
return canonical
},
...(options.clientMetadataUrl ? { clientMetadataUrl: options.clientMetadataUrl } : {}),
...(redirect ? { state: () => redirect.state } : {}),
+38
View File
@@ -324,6 +324,44 @@ describe("MCP OAuth", () => {
expect(probes.some((probe) => probe.includes("codemode"))).toBe(false)
})
test("keeps the configured URL as the resource when metadata echoes the dialed query", async () => {
const tokenRequests: URLSearchParams[] = []
const server = Bun.serve({
port: 0,
async fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/.well-known/oauth-authorization-server")
return Response.json({
issuer: url.origin,
authorization_endpoint: `${url.origin}/authorize`,
token_endpoint: `${url.origin}/token`,
response_types_supported: ["code"],
})
if (url.pathname === "/.well-known/oauth-protected-resource/mcp")
return Response.json({ resource: `${url.origin}/mcp${url.search}`, authorization_servers: [url.origin] })
if (request.method === "POST" && url.pathname === "/token") {
tokenRequests.push(new URLSearchParams(await request.text()))
return Response.json({ access_token: "next", token_type: "Bearer" })
}
return new Response(null, { status: 404 })
},
})
const url = `${server.url.origin}/mcp`
const oauthProvider = await connectProvider(
remote(url),
memoryCredentials([credential({ access: "expired", refresh: "refresh", url })]),
)
// The transport dials with ?codemode=false and follows the 401 challenge to metadata that echoes it.
await auth(oauthProvider, {
serverUrl: `${url}?codemode=false`,
resourceMetadataUrl: new URL(`${server.url.origin}/.well-known/oauth-protected-resource/mcp?codemode=false`),
}).finally(() => server.stop(true))
expect(tokenRequests[0]?.get("grant_type")).toBe("refresh_token")
expect(tokenRequests[0]?.get("resource")).toBe(url)
})
test("finds resource metadata through the 401 header when the well-known path is not served", async () => {
const server = Bun.serve({
port: 0,