Quellcode durchsuchen

fix(opencode): always print MCP OAuth URL (#33716)

Aiden Cline vor 1 Monat
Ursprung
Commit
49ea8a9455

+ 6 - 17
packages/opencode/src/cli/cmd/mcp.ts

@@ -19,8 +19,6 @@ import path from "path"
 import { Global } from "@opencode-ai/core/global"
 import { modify, applyEdits } from "jsonc-parser"
 import { Filesystem } from "@/util/filesystem"
-import { EventV2Bridge } from "@/event-v2-bridge"
-import { EventV2 } from "@opencode-ai/core/event"
 import { Effect } from "effect"
 
 function getAuthStatusIcon(status: MCP.AuthStatus): string {
@@ -258,21 +256,13 @@ export const McpAuthCommand = effectCmd({
     const spinner = prompts.spinner()
     spinner.start("Starting OAuth flow...")
 
-    // Subscribe to browser open failure events to show URL for manual opening
-    const events = yield* EventV2Bridge.Service
-    const unsubscribe = yield* events.listen((event) => {
-      if (event.type !== MCP.BrowserOpenFailed.type) return Effect.void
-      const data = event.data as EventV2.Data<typeof MCP.BrowserOpenFailed>
-      if (data.mcpName === serverName) {
-        spinner.stop("Could not open browser automatically")
-        prompts.log.warn("Please open this URL in your browser to authenticate:")
-        prompts.log.info(data.url)
+    yield* MCP.Service.use((mcp) =>
+      mcp.authenticate(serverName, (url) => {
+        spinner.stop("Authorize in your browser:")
+        prompts.log.info(url)
         spinner.start("Waiting for authorization...")
-      }
-      return Effect.void
-    })
-
-    yield* MCP.Service.use((mcp) => mcp.authenticate(serverName)).pipe(
+      }),
+    ).pipe(
       Effect.tap((status) =>
         Effect.sync(() => {
           if (status.status === "connected") {
@@ -307,7 +297,6 @@ export const McpAuthCommand = effectCmd({
           prompts.log.error(error instanceof Error ? error.message : String(error))
         }),
       ),
-      Effect.ensuring(unsubscribe),
     )
 
     prompts.outro("Done")

+ 9 - 2
packages/opencode/src/mcp/index.ts

@@ -179,7 +179,10 @@ export interface Interface {
   readonly startAuth: (
     mcpName: string,
   ) => Effect.Effect<{ authorizationUrl: string; oauthState: string }, NotFoundError>
-  readonly authenticate: (mcpName: string) => Effect.Effect<Status, NotFoundError>
+  readonly authenticate: (
+    mcpName: string,
+    onAuthorization?: (authorizationUrl: string) => void,
+  ) => Effect.Effect<Status, NotFoundError>
   readonly finishAuth: (mcpName: string, authorizationCode: string) => Effect.Effect<Status, NotFoundError>
   readonly removeAuth: (mcpName: string) => Effect.Effect<void>
   readonly supportsOAuth: (mcpName: string) => Effect.Effect<boolean, NotFoundError>
@@ -858,7 +861,10 @@ export const layer = Layer.effect(
       )
     })
 
-    const authenticate = Effect.fn("MCP.authenticate")(function* (mcpName: string) {
+    const authenticate = Effect.fn("MCP.authenticate")(function* (
+      mcpName: string,
+      onAuthorization?: (authorizationUrl: string) => void,
+    ) {
       const result = yield* startAuth(mcpName)
       if (!result.authorizationUrl) {
         const client = "client" in result ? result.client : undefined
@@ -882,6 +888,7 @@ export const layer = Layer.effect(
       }
 
       const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName)
+      onAuthorization?.(result.authorizationUrl)
 
       yield* Effect.tryPromise(() => open(result.authorizationUrl)).pipe(
         Effect.flatMap((subprocess) =>

+ 10 - 3
packages/opencode/test/mcp/oauth-browser.test.ts

@@ -163,10 +163,10 @@ const trackBrowserOpenFailed = Effect.gen(function* () {
   return event
 })
 
-const authenticateScoped = (name: string) =>
+const authenticateScoped = (name: string, onAuthorization?: (authorizationUrl: string) => void) =>
   Effect.gen(function* () {
     const mcp = yield* service
-    yield* mcp.authenticate(name).pipe(
+    yield* mcp.authenticate(name, onAuthorization).pipe(
       Effect.ignore,
       Effect.catchCause(() => Effect.void),
       Effect.forkScoped,
@@ -225,12 +225,19 @@ mcpTest.instance(
 
       const opened = yield* trackBrowserOpen
       const event = yield* trackBrowserOpenFailed
-      yield* authenticateScoped("test-oauth-server-3")
+      const authorization = yield* Deferred.make<string>()
+      yield* authenticateScoped("test-oauth-server-3", (url) => Deferred.doneUnsafe(authorization, Effect.succeed(url)))
 
       const url = yield* awaitWithTimeout(Deferred.await(opened), "Timed out waiting for open()", "5 seconds")
+      const authorizationUrl = yield* awaitWithTimeout(
+        Deferred.await(authorization),
+        "Timed out waiting for authorization URL",
+        "5 seconds",
+      )
       const failure = yield* Deferred.await(event).pipe(Effect.timeoutOption("700 millis"))
 
       expect(failure).toEqual(Option.none())
+      expect(authorizationUrl).toBe(url)
       expect(typeof url).toBe("string")
       expect(url).toContain("https://")
       expect(transportCalls.at(-1)?.options.requestInit?.headers).toEqual({ "X-Custom-Header": "custom-value" })