refactor(mcp-oauth): replace Bun.serve with node:http in callback-server

The OAuth callback server was using Bun.serve, which would crash if
mcp-oauth code paths ever entered the plugin bundle. Switch to
node:http.createServer with the same WHATWG behavior:

- Binds to 127.0.0.1, preserves 200/400/404 status codes
- Translates fetch(Request)→Response to (req, res) callback style
- Clears OAuth timeout on success/error/missing-param paths
- Replaces server.stop(true) with server.close() for shutdown

Functional behavior and response bodies unchanged.
This commit is contained in:
YeonGyu-Kim
2026-05-12 12:46:31 +09:00
parent 2386cbd9b9
commit 22c7e4eb8e
+56 -36
View File
@@ -1,3 +1,5 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
import { findAvailablePort as findAvailablePortShared } from "../../shared/port-utils"
const DEFAULT_PORT = 19877
@@ -51,56 +53,74 @@ export async function startCallbackServer(startPort: number = DEFAULT_PORT): Pro
const timeoutId = setTimeout(() => {
rejectCallback?.(new Error("OAuth callback timed out after 5 minutes"))
server.stop(true)
server.close()
}, TIMEOUT_MS)
const server = Bun.serve({
port: requestedPort,
hostname: "127.0.0.1",
fetch(request: Request): Response {
const url = new URL(request.url)
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
const host = request.headers.host ?? "127.0.0.1"
const url = new URL(request.url ?? "/", `http://${host}`)
if (url.pathname !== "/oauth/callback") {
return new Response("Not Found", { status: 404 })
}
if (url.pathname !== "/oauth/callback") {
response.statusCode = 404
response.end("Not Found")
return
}
const oauthError = url.searchParams.get("error")
if (oauthError) {
const description = url.searchParams.get("error_description") ?? oauthError
clearTimeout(timeoutId)
rejectCallback?.(new Error(`OAuth authorization failed: ${description}`))
setTimeout(() => server.stop(true), 100)
return new Response(`Authorization failed: ${description}`, { status: 400 })
}
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
if (!code || !state) {
clearTimeout(timeoutId)
rejectCallback?.(new Error("OAuth callback missing code or state parameter"))
setTimeout(() => server.stop(true), 100)
return new Response("Missing code or state parameter", { status: 400 })
}
resolveCallback?.({ code, state })
const oauthError = url.searchParams.get("error")
if (oauthError) {
const description = url.searchParams.get("error_description") ?? oauthError
clearTimeout(timeoutId)
rejectCallback?.(new Error(`OAuth authorization failed: ${description}`))
response.statusCode = 400
response.end(`Authorization failed: ${description}`)
setTimeout(() => server.close(), 100)
return
}
setTimeout(() => server.stop(true), 100)
const code = url.searchParams.get("code")
const state = url.searchParams.get("state")
return new Response(SUCCESS_HTML, {
headers: { "content-type": "text/html; charset=utf-8" },
})
},
if (!code || !state) {
clearTimeout(timeoutId)
rejectCallback?.(new Error("OAuth callback missing code or state parameter"))
response.statusCode = 400
response.end("Missing code or state parameter")
setTimeout(() => server.close(), 100)
return
}
resolveCallback?.({ code, state })
clearTimeout(timeoutId)
response.statusCode = 200
response.setHeader("content-type", "text/html; charset=utf-8")
response.end(SUCCESS_HTML)
setTimeout(() => server.close(), 100)
})
const activePort = server.port ?? requestedPort
await new Promise<void>((resolve, reject) => {
const handleError = (error: Error): void => {
clearTimeout(timeoutId)
reject(error)
}
server.once("error", handleError)
server.once("listening", () => {
server.off("error", handleError)
resolve()
})
server.listen(requestedPort, "127.0.0.1")
})
const address = server.address()
const activePort = typeof address === "object" && address !== null ? address.port : requestedPort
return {
port: activePort,
waitForCallback: () => callbackPromise,
close: () => {
clearTimeout(timeoutId)
server.stop(true)
server.close()
},
}
}