Просмотр исходного кода

refactor(mcp): remove mcp auth async facade exports (#22338)

Kit Langton 5 дней назад
Родитель
Сommit
29c202e6ab

+ 6 - 1
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)...")

+ 0 - 29
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))
 }

+ 2 - 0
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 })

+ 35 - 29
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
     }
   }

+ 23 - 4
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()