Skip to content
Merged
5 changes: 2 additions & 3 deletions scripts/sync-version.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,10 @@ if (fs.existsSync(marketplacePath)) {
const skillPaths = [
"skills/interactive-leetcode-mcp/SKILL.md",
".claude/skills/using-interactive-leetcode-mcp/SKILL.md",
"clawhub-skill/interactive-leetcode-mcp/SKILL.md",
"clawhub-skill/interactive-leetcode-mcp/SKILL.md"
];

const versionPattern =
/@sperekrestova\/interactive-leetcode-mcp@[\w.-]+/g;
const versionPattern = /@sperekrestova\/interactive-leetcode-mcp@[\w.-]+/g;
const versionReplacement = `@sperekrestova/interactive-leetcode-mcp@${version}`;

for (const rel of skillPaths) {
Expand Down
84 changes: 84 additions & 0 deletions src/auth/auth-flow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* Authentication helpers that bridge the on-disk credentials store and the
* in-memory `LeetcodeServiceInterface`.
*
* Closes the silent-logout-on-restart gap where saved credentials existed in
* `~/.leetcode-mcp/credentials.json` but the running server never re-hydrated
* them, so every authenticated tool failed with "Authentication required" until
* the user pasted their cookies again.
*/
import { LeetcodeServiceInterface } from "../leetcode/leetcode-service-interface.js";
import { CredentialsStorage } from "../types/credentials.js";
import { credentialsStorage as defaultStorage } from "../utils/credentials.js";
import logger from "../utils/logger.js";

/** Outcome of an `restoreCredentials` call — useful in tests and logs. */
export type RestoreOutcome =
| { status: "no_credentials" }
| { status: "invalid"; reason: "load_failed" | "expired" }
| { status: "restored"; username: string };

/**
* Loads saved credentials from disk, validates them against LeetCode, and
* pushes them into the running service if they're still good.
*
* Safe to call at server startup; never throws — failures are logged and the
* outcome is returned for callers that want to react.
*/
export async function restoreCredentials(
service: LeetcodeServiceInterface,
storage: CredentialsStorage = defaultStorage
): Promise<RestoreOutcome> {
if (!(await storage.exists())) {
return { status: "no_credentials" };
}

const credentials = await storage.load();
if (!credentials) {
logger.warn(
"Saved credentials file exists but could not be parsed; ignoring."
);
return { status: "invalid", reason: "load_failed" };
}

const username = await applyValidatedCredentials(
service,
credentials.csrftoken,
credentials.LEETCODE_SESSION
);

if (!username) {
logger.info(
"Saved credentials are no longer valid; user will need to re-authenticate."
);
return { status: "invalid", reason: "expired" };
}

logger.info(
"Restored LeetCode session for %s from saved credentials.",
username
);
return { status: "restored", username };
}

/**
* Validates `csrf` / `session` against LeetCode and, on success, pushes them
* into the running service so the very next authenticated tool call works
* without forcing a server restart.
*
* Returns the validated username, or `null` if LeetCode rejected the cookies.
* Trusts the `validateCredentials` interface contract (`Promise<string | null>`)
* and does not catch — any exception thrown by the service propagates.
*/
export async function applyValidatedCredentials(
service: LeetcodeServiceInterface,
csrf: string,
session: string
): Promise<string | null> {
const username = await service.validateCredentials(csrf, session);
if (!username) {
return null;
}
service.updateCredentials(csrf, session);
return username;
}
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import minimist from "minimist";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { restoreCredentials } from "./auth/auth-flow.js";
import { LeetCodeGlobalService } from "./leetcode/leetcode-global-service.js";
import { LeetcodeServiceInterface } from "./leetcode/leetcode-service-interface.js";
import { registerAuthPrompts } from "./mcp/prompts/auth-prompts.js";
Expand Down Expand Up @@ -121,6 +122,11 @@ async function main() {
credential
);

// Re-hydrate saved credentials from disk so authenticated tools work
// immediately after a server restart without forcing the user to paste
// their cookies again.
await restoreCredentials(leetcodeService);

// Register MCP prompts for learning mode and workspace guidance
registerLearningPrompts(server, leetcodeService);

Expand Down
Loading
Loading