Compare commits

...

1 Commits

Author SHA1 Message Date
rekram1-node 7b04192d29 fix(core): honor MCP OAuth metadata challenge 2026-08-24 23:07:45 +00:00
2 changed files with 97 additions and 3 deletions
+41 -3
View File
@@ -1,7 +1,8 @@
export * as MCPOAuth from "./oauth.js"
import { auth, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import { auth, extractWWWAuthenticateParams, type OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js"
import type { OAuthClientInformationMixed, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js"
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js"
import { Deferred, Effect } from "effect"
import { Credential } from "@opencode-ai/schema/credential"
import { ConfigMCP } from "@opencode-ai/schema/config/mcp"
@@ -213,8 +214,40 @@ export const authorize = (input: {
return toCredential({ methodID: input.methodID, serverUrl: input.config.url, tokens, client })
})
const challenge = yield* Effect.tryPromise({
try: async () => {
const response = await fetch(input.config.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json, text/event-stream",
...input.config.headers,
},
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: {
protocolVersion: LATEST_PROTOCOL_VERSION,
capabilities: {},
clientInfo: { name: "opencode", version: "unknown" },
},
}),
})
const result = extractWWWAuthenticateParams(response)
await response.body?.cancel()
return result
},
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
const result = yield* Effect.tryPromise({
try: () => auth(oauthProvider, { serverUrl: input.config.url, scope: oauth?.scope }),
try: () =>
auth(oauthProvider, {
serverUrl: input.config.url,
resourceMetadataUrl: challenge.resourceMetadataUrl,
scope: oauth?.scope ?? challenge.scope,
}),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
})
@@ -238,7 +271,12 @@ export const authorize = (input: {
Effect.flatMap((value) =>
Effect.tryPromise({
try: () =>
auth(oauthProvider, { serverUrl: input.config.url, authorizationCode: value, scope: oauth?.scope }),
auth(oauthProvider, {
serverUrl: input.config.url,
authorizationCode: value,
resourceMetadataUrl: challenge.resourceMetadataUrl,
scope: oauth?.scope ?? challenge.scope,
}),
catch: (error) => (error instanceof Error ? error : new Error(String(error))),
}),
),
+56
View File
@@ -73,4 +73,60 @@ describe("MCP OAuth", () => {
test("rejects an invalid redirect URL", async () => {
await expect(authorize("not a URL")).rejects.toThrow("cannot be parsed as a URL")
})
test("uses protected resource metadata announced by the MCP challenge", async () => {
let metadataRequest = ""
const server = Bun.serve({
port: 0,
fetch(request) {
const url = new URL(request.url)
if (url.pathname === "/runtimes/example/invocations")
return new Response(null, {
status: 401,
headers: {
"WWW-Authenticate": `Bearer resource_metadata="${url.origin}/runtimes/example/invocations/.well-known/oauth-protected-resource?qualifier=dev"`,
},
})
if (url.pathname === "/runtimes/example/invocations/.well-known/oauth-protected-resource") {
metadataRequest = url.href
return Response.json({
resource: `${url.origin}/runtimes/example/invocations`,
authorization_servers: [url.origin],
})
}
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"],
code_challenge_methods_supported: ["S256"],
})
return new Response(null, { status: 404 })
},
})
try {
const authorization = await Effect.runPromise(
Effect.scoped(
MCPOAuth.authorize({
name: "aws",
config: new ConfigMCP.Remote({
type: "remote",
url: `${server.url}runtimes/example/invocations?qualifier=dev`,
oauth: { client_id: "client" },
}),
methodID: Integration.MethodID.make("oauth"),
}),
),
)
expect(authorization.url).toStartWith(`${server.url}authorize?`)
expect(metadataRequest).toBe(
`${server.url}runtimes/example/invocations/.well-known/oauth-protected-resource?qualifier=dev`,
)
} finally {
server.stop(true)
}
})
})