refactor(mcp): remove mcp auth async facade exports (#22338)
This commit is contained in:
@@ -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)...")
|
||||||
|
|||||||
@@ -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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user