summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-13 15:36:12 -0400
committerGitHub <[email protected]>2026-04-13 15:36:12 -0400
commit29c202e6ab0b84d3884da5f8ab94f4873597084c (patch)
treed71d7c66301d3ce52a7c02d681f076b6514de919
parentdcbf11f41a299cf18879a1096584dcaf99fa6b76 (diff)
downloadopencode-29c202e6ab0b84d3884da5f8ab94f4873597084c.tar.gz
opencode-29c202e6ab0b84d3884da5f8ab94f4873597084c.zip
refactor(mcp): remove mcp auth async facade exports (#22338)
-rw-r--r--packages/opencode/src/cli/cmd/mcp.ts7
-rw-r--r--packages/opencode/src/mcp/auth.ts29
-rw-r--r--packages/opencode/src/mcp/index.ts2
-rw-r--r--packages/opencode/src/mcp/oauth-provider.ts64
-rw-r--r--packages/opencode/test/mcp/oauth-auto-connect.test.ts27
5 files changed, 66 insertions, 63 deletions
diff --git a/packages/opencode/src/cli/cmd/mcp.ts b/packages/opencode/src/cli/cmd/mcp.ts
index 4904db1c3..3afedb356 100644
--- a/packages/opencode/src/cli/cmd/mcp.ts
+++ b/packages/opencode/src/cli/cmd/mcp.ts
@@ -361,7 +361,6 @@ export const McpLogoutCommand = cmd({
UI.empty()
prompts.intro("MCP OAuth Logout")
- const authPath = path.join(Global.Path.data, "mcp-auth.json")
const credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
const serverNames = Object.keys(credentials)
@@ -717,6 +716,11 @@ export const McpDebugCommand = cmd({
// Try to discover OAuth metadata
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined
+ const auth = await AppRuntime.runPromise(
+ Effect.gen(function* () {
+ return yield* McpAuth.Service
+ }),
+ )
const authProvider = new McpOAuthProvider(
serverName,
serverConfig.url,
@@ -729,6 +733,7 @@ export const McpDebugCommand = cmd({
{
onRedirect: async () => {},
},
+ auth,
)
prompts.log.info("Testing OAuth flow (without completing authorization)...")
diff --git a/packages/opencode/src/mcp/auth.ts b/packages/opencode/src/mcp/auth.ts
index 7f33f32b8..6eefc107d 100644
--- a/packages/opencode/src/mcp/auth.ts
+++ b/packages/opencode/src/mcp/auth.ts
@@ -3,7 +3,6 @@ import z from "zod"
import { Global } from "../global"
import { Effect, Layer, Context } from "effect"
import { AppFileSystem } from "@/filesystem"
-import { makeRuntime } from "@/effect/run-service"
export namespace McpAuth {
export const Tokens = z.object({
@@ -142,32 +141,4 @@ export namespace McpAuth {
)
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer))
-
- const { runPromise } = makeRuntime(Service, defaultLayer)
-
- // Async facades for backward compat (used by McpOAuthProvider, CLI)
-
- export const get = async (mcpName: string) => runPromise((svc) => svc.get(mcpName))
-
- export const getForUrl = async (mcpName: string, serverUrl: string) =>
- runPromise((svc) => svc.getForUrl(mcpName, serverUrl))
-
- export const all = async () => runPromise((svc) => svc.all())
-
- export const set = async (mcpName: string, entry: Entry, serverUrl?: string) =>
- runPromise((svc) => svc.set(mcpName, entry, serverUrl))
-
- export const remove = async (mcpName: string) => runPromise((svc) => svc.remove(mcpName))
-
- export const updateTokens = async (mcpName: string, tokens: Tokens, serverUrl?: string) =>
- runPromise((svc) => svc.updateTokens(mcpName, tokens, serverUrl))
-
- export const updateClientInfo = async (mcpName: string, clientInfo: ClientInfo, serverUrl?: string) =>
- runPromise((svc) => svc.updateClientInfo(mcpName, clientInfo, serverUrl))
-
- export const updateCodeVerifier = async (mcpName: string, codeVerifier: string) =>
- runPromise((svc) => svc.updateCodeVerifier(mcpName, codeVerifier))
-
- export const updateOAuthState = async (mcpName: string, oauthState: string) =>
- runPromise((svc) => svc.updateOAuthState(mcpName, oauthState))
}
diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts
index fc34143a2..39c7ed98e 100644
--- a/packages/opencode/src/mcp/index.ts
+++ b/packages/opencode/src/mcp/index.ts
@@ -293,6 +293,7 @@ export namespace MCP {
log.info("oauth redirect requested", { key, url: url.toString() })
},
},
+ auth,
)
}
@@ -744,6 +745,7 @@ export namespace MCP {
capturedUrl = url
},
},
+ auth,
)
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
diff --git a/packages/opencode/src/mcp/oauth-provider.ts b/packages/opencode/src/mcp/oauth-provider.ts
index d675fc71e..4fdc192df 100644
--- a/packages/opencode/src/mcp/oauth-provider.ts
+++ b/packages/opencode/src/mcp/oauth-provider.ts
@@ -5,6 +5,7 @@ import type {
OAuthClientInformation,
OAuthClientInformationFull,
} from "@modelcontextprotocol/sdk/shared/auth.js"
+import { Effect } from "effect"
import { McpAuth } from "./auth"
import { Log } from "../util/log"
@@ -30,6 +31,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
private serverUrl: string,
private config: McpOAuthConfig,
private callbacks: McpOAuthCallbacks,
+ private auth: McpAuth.Interface,
) {}
get redirectUrl(): string {
@@ -61,7 +63,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
// Check stored client info (from dynamic registration)
// Use getForUrl to validate credentials are for the current server URL
- const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
+ const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (entry?.clientInfo) {
// Check if client secret has expired
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) {
@@ -79,15 +81,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
- await McpAuth.updateClientInfo(
- this.mcpName,
- {
- clientId: info.client_id,
- clientSecret: info.client_secret,
- clientIdIssuedAt: info.client_id_issued_at,
- clientSecretExpiresAt: info.client_secret_expires_at,
- },
- this.serverUrl,
+ await Effect.runPromise(
+ this.auth.updateClientInfo(
+ this.mcpName,
+ {
+ clientId: info.client_id,
+ clientSecret: info.client_secret,
+ clientIdIssuedAt: info.client_id_issued_at,
+ clientSecretExpiresAt: info.client_secret_expires_at,
+ },
+ this.serverUrl,
+ ),
)
log.info("saved dynamically registered client", {
mcpName: this.mcpName,
@@ -97,7 +101,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
async tokens(): Promise<OAuthTokens | undefined> {
// Use getForUrl to validate tokens are for the current server URL
- const entry = await McpAuth.getForUrl(this.mcpName, this.serverUrl)
+ const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (!entry?.tokens) return undefined
return {
@@ -112,15 +116,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
async saveTokens(tokens: OAuthTokens): Promise<void> {
- await McpAuth.updateTokens(
- this.mcpName,
- {
- accessToken: tokens.access_token,
- refreshToken: tokens.refresh_token,
- expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
- scope: tokens.scope,
- },
- this.serverUrl,
+ await Effect.runPromise(
+ this.auth.updateTokens(
+ this.mcpName,
+ {
+ accessToken: tokens.access_token,
+ refreshToken: tokens.refresh_token,
+ expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
+ scope: tokens.scope,
+ },
+ this.serverUrl,
+ ),
)
log.info("saved oauth tokens", { mcpName: this.mcpName })
}
@@ -131,11 +137,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
async saveCodeVerifier(codeVerifier: string): Promise<void> {
- await McpAuth.updateCodeVerifier(this.mcpName, codeVerifier)
+ await Effect.runPromise(this.auth.updateCodeVerifier(this.mcpName, codeVerifier))
}
async codeVerifier(): Promise<string> {
- const entry = await McpAuth.get(this.mcpName)
+ const entry = await Effect.runPromise(this.auth.get(this.mcpName))
if (!entry?.codeVerifier) {
throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`)
}
@@ -143,11 +149,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
}
async saveState(state: string): Promise<void> {
- await McpAuth.updateOAuthState(this.mcpName, state)
+ await Effect.runPromise(this.auth.updateOAuthState(this.mcpName, state))
}
async state(): Promise<string> {
- const entry = await McpAuth.get(this.mcpName)
+ const entry = await Effect.runPromise(this.auth.get(this.mcpName))
if (entry?.oauthState) {
return entry.oauthState
}
@@ -159,28 +165,28 @@ export class McpOAuthProvider implements OAuthClientProvider {
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
- await McpAuth.updateOAuthState(this.mcpName, newState)
+ await Effect.runPromise(this.auth.updateOAuthState(this.mcpName, newState))
return newState
}
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
log.info("invalidating credentials", { mcpName: this.mcpName, type })
- const entry = await McpAuth.get(this.mcpName)
+ const entry = await Effect.runPromise(this.auth.get(this.mcpName))
if (!entry) {
return
}
switch (type) {
case "all":
- await McpAuth.remove(this.mcpName)
+ await Effect.runPromise(this.auth.remove(this.mcpName))
break
case "client":
delete entry.clientInfo
- await McpAuth.set(this.mcpName, entry)
+ await Effect.runPromise(this.auth.set(this.mcpName, entry))
break
case "tokens":
delete entry.tokens
- await McpAuth.set(this.mcpName, entry)
+ await Effect.runPromise(this.auth.set(this.mcpName, entry))
break
}
}
diff --git a/packages/opencode/test/mcp/oauth-auto-connect.test.ts b/packages/opencode/test/mcp/oauth-auto-connect.test.ts
index 786f1fb46..7f267e9c3 100644
--- a/packages/opencode/test/mcp/oauth-auto-connect.test.ts
+++ b/packages/opencode/test/mcp/oauth-auto-connect.test.ts
@@ -154,15 +154,22 @@ test("state() generates a new state when none is saved", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
+ const auth = await Effect.runPromise(
+ Effect.gen(function* () {
+ return yield* McpAuth.Service
+ }).pipe(Effect.provide(McpAuth.defaultLayer)),
+ )
const provider = new McpOAuthProvider(
"test-state-gen",
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
+ auth,
)
- // Ensure no state exists
- const entryBefore = await McpAuth.get("test-state-gen")
+ const entryBefore = await Effect.runPromise(
+ McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)),
+ )
expect(entryBefore?.oauthState).toBeUndefined()
// state() should generate and return a new state, not throw
@@ -171,7 +178,9 @@ test("state() generates a new state when none is saved", async () => {
expect(state.length).toBe(64) // 32 bytes as hex
// The generated state should be persisted
- const entryAfter = await McpAuth.get("test-state-gen")
+ const entryAfter = await Effect.runPromise(
+ McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)),
+ )
expect(entryAfter?.oauthState).toBe(state)
},
})
@@ -186,16 +195,26 @@ test("state() returns existing state when one is saved", async () => {
await Instance.provide({
directory: tmp.path,
fn: async () => {
+ const auth = await Effect.runPromise(
+ Effect.gen(function* () {
+ return yield* McpAuth.Service
+ }).pipe(Effect.provide(McpAuth.defaultLayer)),
+ )
const provider = new McpOAuthProvider(
"test-state-existing",
"https://example.com/mcp",
{},
{ onRedirect: async () => {} },
+ auth,
)
// Pre-save a state
const existingState = "pre-saved-state-value"
- await McpAuth.updateOAuthState("test-state-existing", existingState)
+ await Effect.runPromise(
+ McpAuth.Service.use((auth) => auth.updateOAuthState("test-state-existing", existingState)).pipe(
+ Effect.provide(McpAuth.defaultLayer),
+ ),
+ )
// state() should return the existing state
const state = await provider.state()