Skip to content

Commit 232eeeb

Browse files
committed
fix(mcp): drop in-memory tokens only on real auth-file deletion
1 parent 1a05e90 commit 232eeeb

3 files changed

Lines changed: 41 additions & 4 deletions

File tree

‎src/mcp/auth-store.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function authFilePath(
6767
);
6868
}
6969

70-
function isEnoent(err: unknown): boolean {
70+
export function isEnoent(err: unknown): boolean {
7171
return (
7272
typeof err === "object" &&
7373
err !== null &&

‎src/mcp/oauth-provider.test.ts‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,34 @@ describe("createOAuthProvider", () => {
253253
expect(await syncValue(provider.tokens())).toBeUndefined();
254254
});
255255

256+
test("keeps live tokens when the auth file is unreadable but drops them on delete", async () => {
257+
const home = await tempHome();
258+
const provider = await createOAuthProvider({
259+
serverName: "linear",
260+
serverURL: linear.serverURL,
261+
redirectUrl: "http://127.0.0.1:1/callback",
262+
onAuthURL: () => undefined,
263+
home,
264+
});
265+
await saveClient(provider, clientInfo(1));
266+
await provider.saveTokens({ access_token: "tok", token_type: "bearer" });
267+
expect((await syncValue(provider.tokens()))?.access_token).toBe("tok");
268+
269+
// A transient stat failure (EACCES here, file still present) must not
270+
// discard live credentials — only a real deletion (ENOENT) does.
271+
const dir = dirname(authFilePath(linear, home));
272+
await chmod(dir, 0o000);
273+
try {
274+
expect((await syncValue(provider.tokens()))?.access_token).toBe("tok");
275+
} finally {
276+
await chmod(dir, 0o700);
277+
}
278+
279+
await deleteAuthState(linear, home);
280+
281+
expect(await syncValue(provider.tokens())).toBeUndefined();
282+
});
283+
256284
test("two overlapping logins on different ports both complete without clobbering verifiers", async () => {
257285
const home = await tempHome();
258286
const a = await createOAuthProvider({

‎src/mcp/oauth-provider.ts‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import type { FetchLike } from "@modelcontextprotocol/sdk/shared/transport.js";
2121
import {
2222
authFilePath,
23+
isEnoent,
2324
tryLoadAuthStateSync,
2425
updateAuthState,
2526
type MCPAuthIdentity,
@@ -161,24 +162,32 @@ export async function createOAuthProvider(
161162
// failed/unreadable file is retried on the next getter call.
162163
const authPath = authFilePath(identity, home);
163164
let seenStamp: string | undefined;
165+
// True once this provider has committed a disk observation. A transient stat
166+
// failure resets the stamp so the next getter retries, but must not erase
167+
// the fact that durable state was seen — otherwise a later real deletion
168+
// (ENOENT) would look like "never saw the file" and orphan live tokens.
169+
let observedDurable = false;
164170
const refreshDurableFromDisk = (): void => {
165171
let stamp: string | undefined;
166172
try {
167173
const stat = statSync(authPath);
168174
stamp = `${String(stat.mtimeMs)}:${String(stat.size)}`;
169-
} catch {
170-
if (seenStamp === undefined) return;
175+
} catch (err) {
176+
if (!observedDurable) return;
177+
seenStamp = undefined;
178+
if (!isEnoent(err)) return;
171179
// The auth file is gone (server removed, logged out elsewhere). Hold no
172180
// orphaned credentials: drop the in-memory tokens too. A later sibling
173181
// save is adopted on the next read via the stamp check above.
174-
seenStamp = undefined;
182+
observedDurable = false;
175183
delete stored.tokens;
176184
return;
177185
}
178186
if (stamp === seenStamp) return;
179187
const next = tryLoadAuthStateSync(identity, home);
180188
if (next === undefined) return;
181189
seenStamp = stamp;
190+
observedDurable = true;
182191
const adoptClient = shouldAdoptClient(stored, next, opts.redirectUrl);
183192
assignTokens(stored, next);
184193
if (adoptClient) assignClient(stored, next);

0 commit comments

Comments
 (0)