Skip to content

Commit 49ea8a9

Browse files
authored
fix(opencode): always print MCP OAuth URL (#33716)
1 parent 142c5c1 commit 49ea8a9

3 files changed

Lines changed: 25 additions & 22 deletions

File tree

packages/opencode/src/cli/cmd/mcp.ts

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import path from "path"
1919
import { Global } from "@opencode-ai/core/global"
2020
import { modify, applyEdits } from "jsonc-parser"
2121
import { Filesystem } from "@/util/filesystem"
22-
import { EventV2Bridge } from "@/event-v2-bridge"
23-
import { EventV2 } from "@opencode-ai/core/event"
2422
import { Effect } from "effect"
2523

2624
function getAuthStatusIcon(status: MCP.AuthStatus): string {
@@ -258,21 +256,13 @@ export const McpAuthCommand = effectCmd({
258256
const spinner = prompts.spinner()
259257
spinner.start("Starting OAuth flow...")
260258

261-
// Subscribe to browser open failure events to show URL for manual opening
262-
const events = yield* EventV2Bridge.Service
263-
const unsubscribe = yield* events.listen((event) => {
264-
if (event.type !== MCP.BrowserOpenFailed.type) return Effect.void
265-
const data = event.data as EventV2.Data<typeof MCP.BrowserOpenFailed>
266-
if (data.mcpName === serverName) {
267-
spinner.stop("Could not open browser automatically")
268-
prompts.log.warn("Please open this URL in your browser to authenticate:")
269-
prompts.log.info(data.url)
259+
yield* MCP.Service.use((mcp) =>
260+
mcp.authenticate(serverName, (url) => {
261+
spinner.stop("Authorize in your browser:")
262+
prompts.log.info(url)
270263
spinner.start("Waiting for authorization...")
271-
}
272-
return Effect.void
273-
})
274-
275-
yield* MCP.Service.use((mcp) => mcp.authenticate(serverName)).pipe(
264+
}),
265+
).pipe(
276266
Effect.tap((status) =>
277267
Effect.sync(() => {
278268
if (status.status === "connected") {
@@ -307,7 +297,6 @@ export const McpAuthCommand = effectCmd({
307297
prompts.log.error(error instanceof Error ? error.message : String(error))
308298
}),
309299
),
310-
Effect.ensuring(unsubscribe),
311300
)
312301

313302
prompts.outro("Done")

packages/opencode/src/mcp/index.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ export interface Interface {
179179
readonly startAuth: (
180180
mcpName: string,
181181
) => Effect.Effect<{ authorizationUrl: string; oauthState: string }, NotFoundError>
182-
readonly authenticate: (mcpName: string) => Effect.Effect<Status, NotFoundError>
182+
readonly authenticate: (
183+
mcpName: string,
184+
onAuthorization?: (authorizationUrl: string) => void,
185+
) => Effect.Effect<Status, NotFoundError>
183186
readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
184187
readonly removeAuth: (mcpName: string) => Effect.Effect<void>
185188
readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
@@ -858,7 +861,10 @@ export const layer = Layer.effect(
858861
)
859862
})
860863

861-
const authenticate = Effect.fn("MCP.authenticate")(function* (mcpName: string) {
864+
const authenticate = Effect.fn("MCP.authenticate")(function* (
865+
mcpName: string,
866+
onAuthorization?: (authorizationUrl: string) => void,
867+
) {
862868
const result = yield* startAuth(mcpName)
863869
if (!result.authorizationUrl) {
864870
const client = "client" in result ? result.client : undefined
@@ -882,6 +888,7 @@ export const layer = Layer.effect(
882888
}
883889

884890
const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
891+
onAuthorization?.(result.authorizationUrl)
885892

886893
yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe(
887894
Effect.flatMap((subprocess) =>

packages/opencode/test/mcp/oauth-browser.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,10 @@ const trackBrowserOpenFailed = Effect.gen(function* () {
163163
return event
164164
})
165165

166-
const authenticateScoped = (name: string) =>
166+
const authenticateScoped = (name: string, onAuthorization?: (authorizationUrl: string) => void) =>
167167
Effect.gen(function* () {
168168
const mcp = yield* service
169-
yield* mcp.authenticate(name).pipe(
169+
yield* mcp.authenticate(name, onAuthorization).pipe(
170170
Effect.ignore,
171171
Effect.catchCause(() => Effect.void),
172172
Effect.forkScoped,
@@ -225,12 +225,19 @@ mcpTest.instance(
225225

226226
const opened = yield* trackBrowserOpen
227227
const event = yield* trackBrowserOpenFailed
228-
yield* authenticateScoped("test-oauth-server-3")
228+
const authorization = yield* Deferred.make<string>()
229+
yield* authenticateScoped("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url)))
229230

230231
const url = yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds")
232+
const authorizationUrl = yield* awaitWithTimeout(
233+
Deferred.await(authorization),
234+
"Timed out waiting for authorization URL",
235+
"5 seconds",
236+
)
231237
const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
232238

233239
expect(failure).toEqual(Option.none())
240+
expect(authorizationUrl).toBe(url)
234241
expect(typeof url).toBe("string")
235242
expect(url).toContain("https://")
236243
expect(transportCalls.at(-1)?.options.requestInit?.headers).toEqual({ "X-Custom-Header": "custom-value" })

0 commit comments

Comments
 (0)