67 lines
2.7 KiB
TypeScript
67 lines
2.7 KiB
TypeScript
import { EOL } from "node:os"
|
|
import { Effect } from "effect"
|
|
import {
|
|
createOpencodeClient,
|
|
type IntegrationAttemptStatus,
|
|
type IntegrationOAuthMethod,
|
|
type OpencodeClient,
|
|
} from "@opencode-ai/sdk/v2/client"
|
|
import { Commands } from "../../commands"
|
|
import { Runtime } from "../../../framework/runtime"
|
|
import { Service } from "@opencode-ai/client/effect"
|
|
import { ServiceConfig } from "../../../services/service-config"
|
|
import { resolveIntegration } from "./resolve"
|
|
|
|
const location = { directory: process.cwd() }
|
|
|
|
export default Runtime.handler(
|
|
Commands.commands.mcp.commands.auth,
|
|
Effect.fn("cli.mcp.auth")(function* (input) {
|
|
const options = yield* ServiceConfig.options()
|
|
const found = yield* Service.discover(options)
|
|
const transport = found ?? (yield* Service.start(options))
|
|
const client = createOpencodeClient({ baseUrl: transport.url, headers: transport.headers })
|
|
|
|
const integration = yield* resolveIntegration(client, input.name, location)
|
|
if (!integration)
|
|
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
|
const method = integration.methods.find(
|
|
(candidate): candidate is IntegrationOAuthMethod => candidate.type === "oauth",
|
|
)
|
|
if (!method)
|
|
return yield* Effect.fail(new Error(`MCP server "${input.name}" is not an OAuth-capable remote server`))
|
|
|
|
const started = yield* Effect.promise(() =>
|
|
client.v2.integration.connect.oauth({ integrationID: integration.id, methodID: method.id, inputs: {}, location }),
|
|
)
|
|
const attempt = started.data?.data
|
|
if (!attempt) return yield* Effect.fail(new Error(started.error?.message ?? "Failed to start OAuth attempt"))
|
|
if (attempt.mode === "code")
|
|
return yield* Effect.fail(new Error("This server requires manual code entry, which the CLI does not support"))
|
|
|
|
process.stdout.write(attempt.instructions + EOL + attempt.url + EOL)
|
|
|
|
const result = yield* poll(client, attempt.attemptID)
|
|
if (result.status === "complete") {
|
|
process.stdout.write(`Authenticated with ${input.name}` + EOL)
|
|
return
|
|
}
|
|
const reason = result.status === "failed" ? `: ${result.message}` : ""
|
|
return yield* Effect.fail(new Error(`Authentication ${result.status}${reason}`))
|
|
}),
|
|
)
|
|
|
|
const poll = (
|
|
client: OpencodeClient,
|
|
attemptID: string,
|
|
): Effect.Effect<Exclude<IntegrationAttemptStatus, { status: "pending" }>> =>
|
|
Effect.gen(function* () {
|
|
const response = yield* Effect.promise(() => client.v2.integration.attempt.status({ attemptID, location }))
|
|
const status = response.data?.data
|
|
if (!status || status.status === "pending") {
|
|
yield* Effect.sleep("1 second")
|
|
return yield* poll(client, attemptID)
|
|
}
|
|
return status
|
|
})
|