mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-21 02:06:00 +00:00
fix(opencode): always print MCP OAuth URL (#33716)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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" })
|
||||
|
||||
Reference in New Issue
Block a user