Skip to content

Commit 552c126

Browse files
committed
fix(mcp): stop OAuth callback server after authentication completes
1 parent ae7a351 commit 552c126

2 files changed

Lines changed: 38 additions & 28 deletions

File tree

packages/opencode/src/mcp/index.ts

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -808,41 +808,45 @@ export const layer = Layer.effect(
808808
return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout)
809809
}
810810

811-
log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState })
811+
return yield* Effect.gen(function* () {
812+
log.info("opening browser for oauth", { mcpName, url: result.authorizationUrl, state: result.oauthState })
812813

813-
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
814+
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
814815

815-
yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe(
816-
Effect.flatMap((subprocess) =>
817-
Effect.callback<void, Error>((resume) => {
818-
const timer = setTimeout(() => resume(Effect.void), 500)
819-
subprocess.on("error", (err) => {
820-
clearTimeout(timer)
821-
resume(Effect.fail(err))
822-
})
823-
subprocess.on("exit", (code) => {
824-
if (code !== null && code !== 0) {
816+
yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe(
817+
Effect.flatMap((subprocess) =>
818+
Effect.callback<void, Error>((resume) => {
819+
const timer = setTimeout(() => resume(Effect.void), 500)
820+
subprocess.on("error", (err) => {
825821
clearTimeout(timer)
826-
resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`)))
827-
}
828-
})
822+
resume(Effect.fail(err))
823+
})
824+
subprocess.on("exit", (code) => {
825+
if (code !== null && code !== 0) {
826+
clearTimeout(timer)
827+
resume(Effect.fail(new Error(`Browser open failed with exit code ${code}`)))
828+
}
829+
})
830+
}),
831+
),
832+
Effect.catch(() => {
833+
log.warn("failed to open browser, user must open URL manually", { mcpName })
834+
return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore)
829835
}),
830-
),
831-
Effect.catch(() => {
832-
log.warn("failed to open browser, user must open URL manually", { mcpName })
833-
return bus.publish(BrowserOpenFailed, { mcpName, url: result.authorizationUrl }).pipe(Effect.ignore)
834-
}),
835-
)
836+
)
836837

837-
const code = yield* Effect.promise(() => callbackPromise)
838+
const code = yield* Effect.promise(() => callbackPromise)
838839

839-
const storedState = yield* auth.getOAuthState(mcpName)
840-
if (storedState !== result.oauthState) {
840+
const storedState = yield* auth.getOAuthState(mcpName)
841+
if (storedState !== result.oauthState) {
842+
yield* auth.clearOAuthState(mcpName)
843+
throw new Error("OAuth state mismatch - potential CSRF attack")
844+
}
841845
yield* auth.clearOAuthState(mcpName)
842-
throw new Error("OAuth state mismatch - potential CSRF attack")
843-
}
844-
yield* auth.clearOAuthState(mcpName)
845-
return yield* finishAuth(mcpName, code)
846+
return yield* finishAuth(mcpName, code)
847+
}).pipe(
848+
Effect.ensuring(Effect.tryPromise(() => McpOAuthCallback.stopIfIdle()).pipe(Effect.ignore)),
849+
)
846850
})
847851

848852
const finishAuth = Effect.fn("MCP.finishAuth")(function* (mcpName: string, authorizationCode: string) {

packages/opencode/src/mcp/oauth-callback.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,12 @@ export async function stop(): Promise<void> {
225225
mcpNameToState.clear()
226226
}
227227

228+
export async function stopIfIdle(): Promise<void> {
229+
if (pendingAuths.size === 0) {
230+
await stop()
231+
}
232+
}
233+
228234
export function isRunning(): boolean {
229235
return server !== undefined
230236
}

0 commit comments

Comments
 (0)