import { Log } from "../util/log"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
const log = Log.create({ service: "mcp.oauth-callback" })
const HTML_SUCCESS = `
OpenCode - Authorization Successful
Authorization Successful
You can close this window and return to OpenCode.
`
const HTML_ERROR = (error: string) => `
OpenCode - Authorization Failed
Authorization Failed
An error occurred during authorization.
${error}
`
interface PendingAuth {
resolve: (code: string) => void
reject: (error: Error) => void
timeout: ReturnType
}
export namespace McpOAuthCallback {
let server: ReturnType | undefined
const pendingAuths = new Map()
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
export async function ensureRunning(): Promise {
if (server) return
const running = await isPortInUse()
if (running) {
log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
return
}
server = Bun.serve({
port: OAUTH_CALLBACK_PORT,
fetch(req) {
const url = new URL(req.url)
if (url.pathname !== OAUTH_CALLBACK_PATH) {
return new Response("Not found", { status: 404 })
}
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
const error = url.searchParams.get("error")
const errorDescription = url.searchParams.get("error_description")
log.info("received oauth callback", { hasCode: !!code, state, error })
if (error) {
const errorMsg = errorDescription || error
if (state && pendingAuths.has(state)) {
const pending = pendingAuths.get(state)!
clearTimeout(pending.timeout)
pendingAuths.delete(state)
pending.reject(new Error(errorMsg))
}
return new Response(HTML_ERROR(errorMsg), {
headers: { "Content-Type": "text/html" },
})
}
if (!code) {
return new Response(HTML_ERROR("No authorization code provided"), {
status: 400,
headers: { "Content-Type": "text/html" },
})
}
// Try to find the pending auth by state parameter, or if no state, use the single pending auth
let pending: PendingAuth | undefined
let pendingKey: string | undefined
if (state && pendingAuths.has(state)) {
pending = pendingAuths.get(state)!
pendingKey = state
} else if (!state && pendingAuths.size === 1) {
// No state parameter but only one pending auth - use it
const [key, value] = pendingAuths.entries().next().value as [string, PendingAuth]
pending = value
pendingKey = key
log.info("no state parameter, using single pending auth", { key })
}
if (!pending || !pendingKey) {
const errorMsg = !state
? "No state parameter provided and multiple pending authorizations"
: "Unknown or expired authorization request"
return new Response(HTML_ERROR(errorMsg), {
status: 400,
headers: { "Content-Type": "text/html" },
})
}
clearTimeout(pending.timeout)
pendingAuths.delete(pendingKey)
pending.resolve(code)
return new Response(HTML_SUCCESS, {
headers: { "Content-Type": "text/html" },
})
},
})
log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
}
export function waitForCallback(mcpName: string): Promise {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
if (pendingAuths.has(mcpName)) {
pendingAuths.delete(mcpName)
reject(new Error("OAuth callback timeout - authorization took too long"))
}
}, CALLBACK_TIMEOUT_MS)
pendingAuths.set(mcpName, { resolve, reject, timeout })
})
}
export function cancelPending(mcpName: string): void {
const pending = pendingAuths.get(mcpName)
if (pending) {
clearTimeout(pending.timeout)
pendingAuths.delete(mcpName)
pending.reject(new Error("Authorization cancelled"))
}
}
export async function isPortInUse(): Promise {
return new Promise((resolve) => {
Bun.connect({
hostname: "127.0.0.1",
port: OAUTH_CALLBACK_PORT,
socket: {
open(socket) {
socket.end()
resolve(true)
},
error() {
resolve(false)
},
data() {},
close() {},
},
}).catch(() => {
resolve(false)
})
})
}
export async function stop(): Promise {
if (server) {
server.stop()
server = undefined
log.info("oauth callback server stopped")
}
for (const [name, pending] of pendingAuths) {
clearTimeout(pending.timeout)
pending.reject(new Error("OAuth callback server stopped"))
}
pendingAuths.clear()
}
export function isRunning(): boolean {
return server !== undefined
}
}