Skip to content

Commit 474e44e

Browse files
Merge pull request #848 from corbitsdev/cl-7527-fix-cross-session-oauth-token-staleness-in-mcp-provider
Re-read OAuth state from disk across MCP sessions
2 parents d58429d + 0c8cc4d commit 474e44e

3 files changed

Lines changed: 527 additions & 45 deletions

File tree

src/mcp/auth-store.ts

Lines changed: 70 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createHash } from "node:crypto";
22
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
3+
import { readFileSync } from "node:fs";
34
import { homedir } from "node:os";
45
import { dirname, join } from "node:path";
56
import type {
@@ -41,40 +42,88 @@ export function normalizeMCPServerURL(serverURL: string): string {
4142
return url.toString();
4243
}
4344

44-
function authFilePath(identity: MCPAuthIdentity, home: string): string {
45+
export function authFilePath(identity: MCPAuthIdentity, home: string = homedir()): string {
4546
const normalizedURL = normalizeMCPServerURL(identity.serverURL);
4647
const digest = createHash("sha256")
4748
.update(JSON.stringify([identity.serverName, normalizedURL]))
4849
.digest("hex");
4950
return join(mcpAuthDir(home), `${serverDisplaySlug(identity.serverName)}-${digest}.json`);
5051
}
5152

52-
export async function loadAuthState(
53-
identity: MCPAuthIdentity,
54-
home: string = homedir(),
55-
): Promise<MCPAuthState> {
56-
let raw: string;
53+
function isEnoent(err: unknown): boolean {
54+
return (
55+
typeof err === "object" &&
56+
err !== null &&
57+
"code" in err &&
58+
(err as { code?: unknown }).code === "ENOENT"
59+
);
60+
}
61+
62+
function parseAuthState(raw: string): MCPAuthState | undefined {
63+
try {
64+
const parsed: unknown = JSON.parse(raw);
65+
if (typeof parsed === "object" && parsed !== null) return parsed as MCPAuthState;
66+
} catch {
67+
// A corrupt auth file should not wedge the session; treat it as no state and
68+
// let a fresh authorization overwrite it.
69+
}
70+
return undefined;
71+
}
72+
73+
function stateFromRaw(raw: string | undefined): MCPAuthState {
74+
if (raw === undefined) return {};
75+
return parseAuthState(raw) ?? {};
76+
}
77+
78+
function readAuthFileSync(path: string): string | undefined {
5779
try {
58-
raw = await readFile(authFilePath(identity, home), "utf8");
80+
return readFileSync(path, "utf8");
5981
} catch (err) {
60-
if (
61-
typeof err === "object" &&
62-
err !== null &&
63-
"code" in err &&
64-
(err as { code?: unknown }).code === "ENOENT"
65-
) {
66-
return {};
67-
}
82+
if (isEnoent(err)) return undefined;
6883
throw err;
6984
}
85+
}
86+
87+
async function readAuthFile(path: string): Promise<string | undefined> {
7088
try {
71-
const parsed = JSON.parse(raw);
72-
if (typeof parsed === "object" && parsed !== null) return parsed as MCPAuthState;
89+
return await readFile(path, "utf8");
90+
} catch (err) {
91+
if (isEnoent(err)) return undefined;
92+
throw err;
93+
}
94+
}
95+
96+
// Synchronous connect-contract load. Tolerates a missing (ENOENT) or corrupt
97+
// file with empty state, matching loadAuthState; other read errors propagate.
98+
export function loadAuthStateSync(
99+
identity: MCPAuthIdentity,
100+
home: string = homedir(),
101+
): MCPAuthState {
102+
return stateFromRaw(readAuthFileSync(authFilePath(identity, home)));
103+
}
104+
105+
export async function loadAuthState(
106+
identity: MCPAuthIdentity,
107+
home: string = homedir(),
108+
): Promise<MCPAuthState> {
109+
return stateFromRaw(await readAuthFile(authFilePath(identity, home)));
110+
}
111+
112+
// Cache refresh for a live provider: missing, unreadable, or corrupt files
113+
// return undefined so the caller keeps its in-memory mirror. Empty-on-corrupt
114+
// is loadAuthState's connect contract, not cache invalidation.
115+
export function tryLoadAuthStateSync(
116+
identity: MCPAuthIdentity,
117+
home: string = homedir(),
118+
): MCPAuthState | undefined {
119+
let raw: string | undefined;
120+
try {
121+
raw = readAuthFileSync(authFilePath(identity, home));
73122
} catch {
74-
// A corrupt auth file should not wedge the session; treat it as no state and
75-
// let a fresh authorization overwrite it.
123+
return undefined;
76124
}
77-
return {};
125+
if (raw === undefined) return undefined;
126+
return parseAuthState(raw);
78127
}
79128

80129
// pid alone is not unique per call — concurrent saves in one process must not
@@ -174,14 +223,7 @@ async function unlinkAuthFile(path: string): Promise<void> {
174223
try {
175224
await unlink(path);
176225
} catch (err) {
177-
if (
178-
typeof err === "object" &&
179-
err !== null &&
180-
"code" in err &&
181-
(err as { code?: unknown }).code === "ENOENT"
182-
) {
183-
return;
184-
}
226+
if (isEnoent(err)) return;
185227
throw err;
186228
}
187229
}

0 commit comments

Comments
 (0)