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
+37 -17
View File
@@ -1,3 +1,5 @@
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
import { findAvailablePort as findAvailablePortShared } from "../../shared/port-utils" import { findAvailablePort as findAvailablePortShared } from "../../shared/port-utils"
const DEFAULT_PORT = 19877 const DEFAULT_PORT = 19877
@@ -51,17 +53,17 @@ export async function startCallbackServer(startPort: number = DEFAULT_PORT): Pro
const timeoutId = setTimeout(() => { const timeoutId = setTimeout(() => {
rejectCallback?.(new Error("OAuth callback timed out after 5 minutes")) rejectCallback?.(new Error("OAuth callback timed out after 5 minutes"))
server.stop(true) server.close()
}, TIMEOUT_MS) }, TIMEOUT_MS)
const server = Bun.serve({ const server = createServer((request: IncomingMessage, response: ServerResponse) => {
port: requestedPort, const host = request.headers.host ?? "127.0.0.1"
hostname: "127.0.0.1", const url = new URL(request.url ?? "/", `http://${host}`)
fetch(request: Request): Response {
const url = new URL(request.url)
if (url.pathname !== "/oauth/callback") { if (url.pathname !== "/oauth/callback") {
return new Response("Not Found", { status: 404 }) response.statusCode = 404
response.end("Not Found")
return
} }
const oauthError = url.searchParams.get("error") const oauthError = url.searchParams.get("error")
@@ -69,8 +71,10 @@ export async function startCallbackServer(startPort: number = DEFAULT_PORT): Pro
const description = url.searchParams.get("error_description") ?? oauthError const description = url.searchParams.get("error_description") ?? oauthError
clearTimeout(timeoutId) clearTimeout(timeoutId)
rejectCallback?.(new Error(`OAuth authorization failed: ${description}`)) rejectCallback?.(new Error(`OAuth authorization failed: ${description}`))
setTimeout(() => server.stop(true), 100) response.statusCode = 400
return new Response(`Authorization failed: ${description}`, { status: 400 }) response.end(`Authorization failed: ${description}`)
setTimeout(() => server.close(), 100)
return
} }
const code = url.searchParams.get("code") const code = url.searchParams.get("code")
@@ -79,28 +83,44 @@ export async function startCallbackServer(startPort: number = DEFAULT_PORT): Pro
if (!code || !state) { if (!code || !state) {
clearTimeout(timeoutId) clearTimeout(timeoutId)
rejectCallback?.(new Error("OAuth callback missing code or state parameter")) rejectCallback?.(new Error("OAuth callback missing code or state parameter"))
setTimeout(() => server.stop(true), 100) response.statusCode = 400
return new Response("Missing code or state parameter", { status: 400 }) response.end("Missing code or state parameter")
setTimeout(() => server.close(), 100)
return
} }
resolveCallback?.({ code, state }) resolveCallback?.({ code, state })
clearTimeout(timeoutId) clearTimeout(timeoutId)
setTimeout(() => server.stop(true), 100) response.statusCode = 200
response.setHeader("content-type", "text/html; charset=utf-8")
response.end(SUCCESS_HTML)
setTimeout(() => server.close(), 100)
})
return new Response(SUCCESS_HTML, { await new Promise<void>((resolve, reject) => {
headers: { "content-type": "text/html; charset=utf-8" }, 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 activePort = server.port ?? requestedPort
const address = server.address()
const activePort = typeof address === "object" && address !== null ? address.port : requestedPort
return { return {
port: activePort, port: activePort,
waitForCallback: () => callbackPromise, waitForCallback: () => callbackPromise,
close: () => { close: () => {
clearTimeout(timeoutId) clearTimeout(timeoutId)
server.stop(true) server.close()
}, },
} }
} }