fix(mcp): cancel timed out oauth refresh

This commit is contained in:
Aiden Cline
2026-06-13 11:28:54 -05:00
parent ee5ee61db5
commit ecf550c88c
3 changed files with 27 additions and 8 deletions
+6 -2
View File
@@ -230,11 +230,15 @@ export const layer = Layer.effect(
auth,
)
authProvider = provider
const controller = new AbortController()
yield* Effect.tryPromise(() =>
withTimeout(
provider.refreshTokensIfExpired(mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined),
provider.refreshTokensIfExpired(
mcp.headers ? createFetchWithInit(fetch, { headers: mcp.headers }) : undefined,
controller.signal,
),
connectTimeout,
),
).finally(() => controller.abort()),
).pipe(Effect.ignore)
}
+6 -3
View File
@@ -128,7 +128,7 @@ export class McpOAuthProvider implements OAuthClientProvider {
)
}
async refreshTokensIfExpired(fetchFn?: FetchLike): Promise<boolean> {
async refreshTokensIfExpired(fetchFn?: FetchLike, signal?: AbortSignal): Promise<boolean> {
const entry = await Effect.runPromise(this.auth.getForUrl(this.mcpName, this.serverUrl))
if (!entry?.tokens?.refreshToken) return false
if (!entry.tokens.expiresAt) return false
@@ -137,14 +137,17 @@ export class McpOAuthProvider implements OAuthClientProvider {
const clientInformation = await this.clientInformation()
if (!clientInformation) return false
const info = await discoverOAuthServerInfo(this.serverUrl, { fetchFn })
const request = signal
? (url: string | URL, init?: RequestInit) => (fetchFn ?? fetch)(url, { ...init, signal })
: fetchFn
const info = await discoverOAuthServerInfo(this.serverUrl, { fetchFn: request })
await this.saveTokens(
await refreshAuthorization(info.authorizationServerUrl, {
metadata: info.authorizationServerMetadata,
clientInformation,
refreshToken: entry.tokens.refreshToken,
resource: await selectResourceURL(this.serverUrl, this, info.resourceMetadata),
fetchFn,
fetchFn: request,
}),
)
return true
+15 -3
View File
@@ -54,6 +54,7 @@ let transportCloseCount = 0
// Captures the opts passed to each MockStdioTransport, keyed by lastCreatedClientName
const stdioOptsByName = new Map<string, any>()
let refreshAuthorizationCalls = 0
let refreshAborted = false
function getOrCreateClientState(name?: string): MockClientState {
const key = name ?? "default"
@@ -253,6 +254,7 @@ beforeEach(() => {
clientCreateCount = 0
transportCloseCount = 0
refreshAuthorizationCalls = 0
refreshAborted = false
})
// Import after mocks
@@ -307,14 +309,23 @@ it.live("McpOAuthProvider refreshes expired stored tokens", () =>
)
it.instance(
"remote connect bounds expired token refresh by mcp timeout",
"remote connect cancels expired token refresh after mcp timeout",
() =>
Effect.acquireUseRelease(
Effect.sync(() => {
const original = McpOAuthProvider.prototype.refreshTokensIfExpired
McpOAuthProvider.prototype.refreshTokensIfExpired = () => {
McpOAuthProvider.prototype.refreshTokensIfExpired = (_fetchFn, signal) => {
refreshAuthorizationCalls++
return new Promise(() => {})
return new Promise((_, reject) => {
signal?.addEventListener(
"abort",
() => {
refreshAborted = true
reject(signal.reason)
},
{ once: true },
)
})
}
return original
}),
@@ -331,6 +342,7 @@ it.instance(
expect(statusName(result.status, "remote-timeout")).toBe("connected")
expect(refreshAuthorizationCalls).toBe(1)
expect(refreshAborted).toBe(true)
}),
),
(original) =>