Skip to content

Commit c08dc62

Browse files
committed
Close leftover MCP authorization recovery races
1 parent ab7176f commit c08dc62

3 files changed

Lines changed: 355 additions & 43 deletions

File tree

src/mcp/client-auth-policy.test.ts

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ describe("HTTP MCP auth policy", () => {
259259
);
260260
while (tokenExchangeSignals.length === 0) await Promise.resolve();
261261

262-
expect(tokenExchangeSignals[0]).toBe(abort.signal);
262+
expect(tokenExchangeSignals[0]).toBeDefined();
263+
expect(tokenExchangeSignals[0]?.aborted).toBe(false);
263264
abort.abort(new Error("toolset disposed"));
264265
const result = await connection;
265266

@@ -280,12 +281,17 @@ describe("HTTP MCP auth policy", () => {
280281
expect(result.ok).toBe(true);
281282
if (!result.ok) return;
282283
expect(transportOptions).toEqual([
283-
{ authProvider, requestInit: { signal: abort.signal }, fetch: expect.any(Function) },
284+
{
285+
authProvider,
286+
requestInit: { signal: expect.any(AbortSignal) },
287+
fetch: expect.any(Function),
288+
},
284289
]);
285290

286291
const call = result.client.call("ping", {}, abort.signal);
287292
while (tokenRefreshSignals.length === 0) await Promise.resolve();
288-
expect(tokenRefreshSignals[0]).toBe(abort.signal);
293+
expect(tokenRefreshSignals[0]).toBeDefined();
294+
expect(tokenRefreshSignals[0]?.aborted).toBe(false);
289295
abort.abort(new Error("toolset disposed"));
290296
await expect(call).rejects.toThrow("toolset disposed");
291297
expect(tokenRefreshAborts).toBe(1);
@@ -310,7 +316,49 @@ describe("HTTP MCP auth policy", () => {
310316
expect(callbackStarts).toBe(1);
311317
expect(providerCreates).toBe(1);
312318
expect(providerServerURL).toBe("https://custom.example/mcp?mode=full");
313-
expect(transportOptions).toEqual([{ authProvider }]);
319+
expect(transportOptions).toEqual([
320+
{
321+
authProvider,
322+
requestInit: { signal: expect.any(AbortSignal) },
323+
fetch: expect.any(Function),
324+
},
325+
]);
326+
});
327+
328+
test("client close aborts in-flight OAuth without aborting the connect signal", async () => {
329+
const connectAbort = new AbortController();
330+
const result = await connectMCPServer(
331+
{ name: "linear", type: "http", url: "https://mcp.linear.app/mcp" },
332+
{ signal: connectAbort.signal },
333+
);
334+
expect(result.ok).toBe(true);
335+
if (!result.ok) return;
336+
337+
const transport = transportOptions[0] as {
338+
requestInit?: { signal?: AbortSignal };
339+
fetch?: (url: string | URL, init?: RequestInit) => Promise<Response>;
340+
};
341+
expect(transport.requestInit?.signal).toBeDefined();
342+
expect(transport.requestInit?.signal).not.toBe(connectAbort.signal);
343+
expect(transport.fetch).toBeTypeOf("function");
344+
345+
const call = result.client.call("ping", {}, new AbortController().signal);
346+
while (tokenRefreshSignals.length === 0) await Promise.resolve();
347+
expect(tokenRefreshSignals[0]).not.toBe(connectAbort.signal);
348+
expect(tokenRefreshSignals[0]?.aborted).toBe(false);
349+
expect(connectAbort.signal.aborted).toBe(false);
350+
351+
await result.client.close();
352+
353+
await expect(call).rejects.toThrow();
354+
expect(tokenRefreshAborts).toBe(1);
355+
expect(connectAbort.signal.aborted).toBe(false);
356+
expect(transport.requestInit?.signal?.aborted).toBe(true);
357+
const fetchFn = transport.fetch;
358+
expect(fetchFn).toBeTypeOf("function");
359+
if (fetchFn === undefined) return;
360+
await expect(fetchFn("https://auth.test/token")).rejects.toThrow();
361+
expect(connectAbort.signal.aborted).toBe(false);
314362
});
315363
});
316364

0 commit comments

Comments
 (0)