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

This commit is contained in:
Kit Langton
2026-04-13 15:36:12 -04:00
committed by GitHub
parent dcbf11f41a
commit 29c202e6ab
5 changed files with 66 additions and 63 deletions
+6 -1
View File
@@ -361,7 +361,6 @@ export const McpLogoutCommand = cmd({
UI.empty() UI.empty()
prompts.intro("MCP OAuth Logout") 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 credentials = await AppRuntime.runPromise(McpAuth.Service.use((auth) => auth.all()))
const serverNames = Object.keys(credentials) const serverNames = Object.keys(credentials)
@@ -717,6 +716,11 @@ export const McpDebugCommand = cmd({
// Try to discover OAuth metadata // Try to discover OAuth metadata
const oauthConfig = typeof serverConfig.oauth === "object" ? serverConfig.oauth : undefined 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( const authProvider = new McpOAuthProvider(
serverName, serverName,
serverConfig.url, serverConfig.url,
@@ -729,6 +733,7 @@ export const McpDebugCommand = cmd({
{ {
onRedirect: async () => {}, onRedirect: async () => {},
}, },
auth,
) )
prompts.log.info("Testing OAuth flow (without completing authorization)...") prompts.log.info("Testing OAuth flow (without completing authorization)...")
-29
View File
@@ -3,7 +3,6 @@ import z from "zod"
import { Global } from "../global" import { Global } from "../global"
import { Effect, Layer, Context } from "effect" import { Effect, Layer, Context } from "effect"
import { AppFileSystem } from "@/filesystem" import { AppFileSystem } from "@/filesystem"
import { makeRuntime } from "@/effect/run-service"
export namespace McpAuth { export namespace McpAuth {
export const Tokens = z.object({ export const Tokens = z.object({
@@ -142,32 +141,4 @@ export namespace McpAuth {
) )
export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer)) 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
View File
@@ -293,6 +293,7 @@ export namespace MCP {
log.info("oauth redirect requested", { key, url: url.toString() }) log.info("oauth redirect requested", { key, url: url.toString() })
}, },
}, },
auth,
) )
} }
@@ -744,6 +745,7 @@ export namespace MCP {
capturedUrl = url capturedUrl = url
}, },
}, },
auth,
) )
const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider }) const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
+35 -29
View File
@@ -5,6 +5,7 @@ import type {
OAuthClientInformation, OAuthClientInformation,
OAuthClientInformationFull, OAuthClientInformationFull,
} from "@modelcontextprotocol/sdk/shared/auth.js" } from "@modelcontextprotocol/sdk/shared/auth.js"
import { Effect } from "effect"
import { McpAuth } from "./auth" import { McpAuth } from "./auth"
import { Log } from "../util/log" import { Log } from "../util/log"
@@ -30,6 +31,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
private serverUrl: string, private serverUrl: string,
private config: McpOAuthConfig, private config: McpOAuthConfig,
private callbacks: McpOAuthCallbacks, private callbacks: McpOAuthCallbacks,
private auth: McpAuth.Interface,
) {} ) {}
get redirectUrl(): string { get redirectUrl(): string {
@@ -61,7 +63,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
// Check stored client info (from dynamic registration) // Check stored client info (from dynamic registration)
// Use getForUrl to validate credentials are for the current server URL // 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) { if (entry?.clientInfo) {
// Check if client secret has expired // Check if client secret has expired
if (entry.clientInfo.clientSecretExpiresAt && entry.clientInfo.clientSecretExpiresAt < Date.now() / 1000) { 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> { async saveClientInformation(info: OAuthClientInformationFull): Promise<void> {
await McpAuth.updateClientInfo( await Effect.runPromise(
this.mcpName, this.auth.updateClientInfo(
{ this.mcpName,
clientId: info.client_id, {
clientSecret: info.client_secret, clientId: info.client_id,
clientIdIssuedAt: info.client_id_issued_at, clientSecret: info.client_secret,
clientSecretExpiresAt: info.client_secret_expires_at, clientIdIssuedAt: info.client_id_issued_at,
}, clientSecretExpiresAt: info.client_secret_expires_at,
this.serverUrl, },
this.serverUrl,
),
) )
log.info("saved dynamically registered client", { log.info("saved dynamically registered client", {
mcpName: this.mcpName, mcpName: this.mcpName,
@@ -97,7 +101,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
async tokens(): Promise<OAuthTokens | undefined> { async tokens(): Promise<OAuthTokens | undefined> {
// Use getForUrl to validate tokens are for the current server URL // 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 if (!entry?.tokens) return undefined
return { return {
@@ -112,15 +116,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
} }
async saveTokens(tokens: OAuthTokens): Promise<void> { async saveTokens(tokens: OAuthTokens): Promise<void> {
await McpAuth.updateTokens( await Effect.runPromise(
this.mcpName, this.auth.updateTokens(
{ this.mcpName,
accessToken: tokens.access_token, {
refreshToken: tokens.refresh_token, accessToken: tokens.access_token,
expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined, refreshToken: tokens.refresh_token,
scope: tokens.scope, expiresAt: tokens.expires_in ? Date.now() / 1000 + tokens.expires_in : undefined,
}, scope: tokens.scope,
this.serverUrl, },
this.serverUrl,
),
) )
log.info("saved oauth tokens", { mcpName: this.mcpName }) log.info("saved oauth tokens", { mcpName: this.mcpName })
} }
@@ -131,11 +137,11 @@ export class McpOAuthProvider implements OAuthClientProvider {
} }
async saveCodeVerifier(codeVerifier: string): Promise<void> { 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> { async codeVerifier(): Promise<string> {
const entry = await McpAuth.get(this.mcpName) const entry = await Effect.runPromise(this.auth.get(this.mcpName))
if (!entry?.codeVerifier) { if (!entry?.codeVerifier) {
throw new Error(`No code verifier saved for MCP server: ${this.mcpName}`) 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> { 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> { async state(): Promise<string> {
const entry = await McpAuth.get(this.mcpName) const entry = await Effect.runPromise(this.auth.get(this.mcpName))
if (entry?.oauthState) { if (entry?.oauthState) {
return entry.oauthState return entry.oauthState
} }
@@ -159,28 +165,28 @@ export class McpOAuthProvider implements OAuthClientProvider {
const newState = Array.from(crypto.getRandomValues(new Uint8Array(32))) const newState = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join("") .join("")
await McpAuth.updateOAuthState(this.mcpName, newState) await Effect.runPromise(this.auth.updateOAuthState(this.mcpName, newState))
return newState return newState
} }
async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> { async invalidateCredentials(type: "all" | "client" | "tokens"): Promise<void> {
log.info("invalidating credentials", { mcpName: this.mcpName, type }) 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) { if (!entry) {
return return
} }
switch (type) { switch (type) {
case "all": case "all":
await McpAuth.remove(this.mcpName) await Effect.runPromise(this.auth.remove(this.mcpName))
break break
case "client": case "client":
delete entry.clientInfo delete entry.clientInfo
await McpAuth.set(this.mcpName, entry) await Effect.runPromise(this.auth.set(this.mcpName, entry))
break break
case "tokens": case "tokens":
delete entry.tokens delete entry.tokens
await McpAuth.set(this.mcpName, entry) await Effect.runPromise(this.auth.set(this.mcpName, entry))
break break
} }
} }
@@ -154,15 +154,22 @@ test("state() generates a new state when none is saved", async () => {
await Instance.provide({ await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
const auth = await Effect.runPromise(
Effect.gen(function* () {
return yield* McpAuth.Service
}).pipe(Effect.provide(McpAuth.defaultLayer)),
)
const provider = new McpOAuthProvider( const provider = new McpOAuthProvider(
"test-state-gen", "test-state-gen",
"https://example.com/mcp", "https://example.com/mcp",
{}, {},
{ onRedirect: async () => {} }, { onRedirect: async () => {} },
auth,
) )
// Ensure no state exists const entryBefore = await Effect.runPromise(
const entryBefore = await McpAuth.get("test-state-gen") McpAuth.Service.use((auth) => auth.get("test-state-gen")).pipe(Effect.provide(McpAuth.defaultLayer)),
)
expect(entryBefore?.oauthState).toBeUndefined() expect(entryBefore?.oauthState).toBeUndefined()
// state() should generate and return a new state, not throw // 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 expect(state.length).toBe(64) // 32 bytes as hex
// The generated state should be persisted // 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) expect(entryAfter?.oauthState).toBe(state)
}, },
}) })
@@ -186,16 +195,26 @@ test("state() returns existing state when one is saved", async () => {
await Instance.provide({ await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
const auth = await Effect.runPromise(
Effect.gen(function* () {
return yield* McpAuth.Service
}).pipe(Effect.provide(McpAuth.defaultLayer)),
)
const provider = new McpOAuthProvider( const provider = new McpOAuthProvider(
"test-state-existing", "test-state-existing",
"https://example.com/mcp", "https://example.com/mcp",
{}, {},
{ onRedirect: async () => {} }, { onRedirect: async () => {} },
auth,
) )
// Pre-save a state // Pre-save a state
const existingState = "pre-saved-state-value" 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 // state() should return the existing state
const state = await provider.state() const state = await provider.state()