Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/opencode/src/mcp/oauth-callback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Log from "@opencode-ai/core/util/log"
import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH, parseRedirectUri } from "./oauth-provider"

const log = Log.create({ service: "mcp.oauth-callback" })
const OAUTH_CALLBACK_HOST = "127.0.0.1"

// Current callback server configuration (may differ from defaults if custom redirectUri is used)
let currentPort = OAUTH_CALLBACK_PORT
Expand Down Expand Up @@ -161,8 +162,8 @@ export async function ensureRunning(redirectUri?: string): Promise<void> {

server = createServer(handleRequest)
await new Promise<void>((resolve, reject) => {
server!.listen(currentPort, () => {
log.info("oauth callback server started", { port: currentPort, path: currentPath })
server!.listen(currentPort, OAUTH_CALLBACK_HOST, () => {
log.info("oauth callback server started", { host: OAUTH_CALLBACK_HOST, port: currentPort, path: currentPath })
resolve()
})
server!.on("error", reject)
Expand Down
42 changes: 42 additions & 0 deletions packages/opencode/test/mcp/oauth-callback.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import { test, expect, describe, afterEach } from "bun:test"
import { createConnection, createServer as createNetServer } from "net"
import { McpOAuthCallback } from "../../src/mcp/oauth-callback"
import { parseRedirectUri } from "../../src/mcp/oauth-provider"

async function getFreeLoopbackPort(): Promise<number> {
return new Promise((resolve, reject) => {
const probe = createNetServer()
probe.once("error", reject)
probe.listen(0, "127.0.0.1", () => {
const address = probe.address()
probe.close(() => {
if (typeof address === "object" && address) {
resolve(address.port)
return
}
reject(new Error("Could not allocate a loopback port"))
})
})
})
}

async function canConnect(host: string, port: number): Promise<boolean> {
return new Promise((resolve) => {
const socket = createConnection({ host, port })
const done = (ok: boolean) => {
socket.removeAllListeners()
socket.destroy()
resolve(ok)
}

socket.setTimeout(500)
socket.once("connect", () => done(true))
socket.once("error", () => done(false))
socket.once("timeout", () => done(false))
})
}

describe("parseRedirectUri", () => {
test("returns defaults when no URI provided", () => {
const result = parseRedirectUri()
Expand Down Expand Up @@ -31,4 +65,12 @@ describe("McpOAuthCallback.ensureRunning", () => {
await McpOAuthCallback.ensureRunning("http://127.0.0.1:18000/custom/callback")
expect(McpOAuthCallback.isRunning()).toBe(true)
})

test("binds the callback server to IPv4 loopback", async () => {
const port = await getFreeLoopbackPort()
await McpOAuthCallback.ensureRunning(`http://127.0.0.1:${port}/custom/callback`)

expect(await canConnect("127.0.0.1", port)).toBe(true)
expect(await canConnect("::1", port)).toBe(false)
})
})
Loading