Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ bun.lockb
coverage/
.vitest/

# Auth cookies (may contain session secrets)
leetcode-cookies.json

CLAUDE.md
.claude/
.specify/
Expand Down
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,49 @@ The build step produces `dist/mcp/index.js`, the entry point used by MCP clients

To run only a subset of tools, append the module name (`users`, `problems`, or `discussions`) as an extra argument or set the `MCP_SERVER_MODE` environment variable.

### Authenticated requests (optional)

By default, the MCP server makes unauthenticated requests to the LeetCode GraphQL API. To access user-specific data (submission history, private problem lists, contest participation, etc.), you can provide session cookies via a JSON file:

1. Extract your LeetCode session cookies using the included helper script:

```bash
npx playwright install chromium # first time only
npx ts-node scripts/extract-cookies.ts ./leetcode-cookies.json
```

This opens a browser window — log in to LeetCode and the script will automatically detect your session and save the cookies. The output file is set to `600` permissions (owner-only read/write).

Alternatively, create the JSON file manually with cookies from your browser's developer tools:

```json
{
"LEETCODE_SESSION": "<your session cookie>",
"csrftoken": "<your csrf token>",
"cf_clearance": "<your cf_clearance cookie>"
}
```

2. Set the `LEETCODE_COOKIES_FILE` environment variable in your MCP client config:

```json
{
"mcpServers": {
"leetcode-suite": {
"command": "node",
"args": ["C:\\path\\to\\alfa-leetcode-api\\dist\\mcp\\index.js"],
"env": {
"LEETCODE_COOKIES_FILE": "C:\\path\\to\\leetcode-cookies.json"
}
}
}
}
```

When the env var is set, the server injects `Cookie` and `x-csrftoken` headers into all GraphQL requests. When unset, it falls back to unauthenticated mode silently.

> **Note:** The `LEETCODE_SESSION` cookie typically expires after ~14 days. Re-run the extraction script to refresh it.

### MCP Inspector

Use the Inspector to debug tools locally:
Expand Down
104 changes: 102 additions & 2 deletions mcp/leetCodeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ import {
userQuestionProgressQuery,
userContestRankingInfoQuery,
skillStatsQuery,
submissionDetailsQuery,
streakCounterQuery,
userStatusQuery,
questionNoteQuery,
updateNoteMutation,
addToFavoriteMutation,
removeFromFavoriteMutation,
favoritesListsQuery,
problemStatusQuery,
} from '../src/GQLQueries';
import type {
DailyProblemData,
Expand All @@ -42,8 +51,17 @@ import type {
TrendingDiscussionObject,
UserData,
} from '../src/types';
import { executeGraphQL } from './serverUtils';
import { SubmissionArgs, CalendarArgs, ProblemArgs, DiscussCommentsArgs, Variables } from './types';
import { executeGraphQL, requireAuth } from './serverUtils';
import {
SubmissionArgs,
CalendarArgs,
ProblemArgs,
DiscussCommentsArgs,
Variables,
SubmissionDetailArgs,
QuestionNoteArgs,
ToggleFavoriteArgs,
} from './types';

// Builds GraphQL variables by filtering out undefined, null, and NaN values.
function buildVariables(input: Record<string, unknown>): Variables {
Expand Down Expand Up @@ -234,3 +252,85 @@ export async function getUserContestRankingInfo(username: string) {
export async function getUserProgressRaw(username: string) {
return executeGraphQL(userQuestionProgressQuery, { username });
}

// ── Auth-required tools ─────────────────────────────────────────────

// Retrieves authenticated user status (username, premium, checkedIn, notifications).
export async function getUserStatus() {
requireAuth();
const data = (await executeGraphQL(userStatusQuery, {})) as { userStatus: Record<string, unknown> };
return data.userStatus;
}

// Retrieves the daily streak counter for the authenticated user.
export async function getUserStreak() {
requireAuth();
const data = (await executeGraphQL(streakCounterQuery, {})) as { streakCounter: Record<string, unknown> };
return data.streakCounter;
}

// Retrieves the authenticated user's favorite/bookmark lists with problem IDs.
export async function getUserFavorites() {
requireAuth();
const data = (await executeGraphQL(favoritesListsQuery, {})) as {
favoritesLists: { allFavorites: unknown[] };
};
return data.favoritesLists.allFavorites;
}

// Retrieves full submission details including source code.
export async function getSubmissionDetails(args: SubmissionDetailArgs) {
requireAuth();
const data = (await executeGraphQL(submissionDetailsQuery, {
submissionId: args.submissionId,
})) as { submissionDetails: Record<string, unknown> };
return data.submissionDetails;
}

// Retrieves the user's personal note on a problem.
export async function getProblemNote(titleSlug: string) {
requireAuth();
const data = (await executeGraphQL(questionNoteQuery, { titleSlug })) as {
question: { questionId: string; questionFrontendId: string; title: string; titleSlug: string; note: string | null };
};
return data.question;
}

// Creates or updates a personal note on a problem.
export async function updateProblemNote(args: QuestionNoteArgs) {
requireAuth();
const data = (await executeGraphQL(updateNoteMutation, {
titleSlug: args.titleSlug,
content: args.note ?? '',
})) as { updateNote: { question: Record<string, unknown> } };
return data.updateNote.question;
}

// Adds a problem to a favorites list.
export async function addProblemToFavorite(args: ToggleFavoriteArgs) {
requireAuth();
const data = (await executeGraphQL(addToFavoriteMutation, {
favoriteIdHash: args.favoriteIdHash,
questionId: args.questionId,
})) as { addQuestionToFavorite: { ok: boolean } };
return { ok: data.addQuestionToFavorite.ok };
}

// Removes a problem from a favorites list.
export async function removeProblemFromFavorite(args: ToggleFavoriteArgs) {
requireAuth();
const data = (await executeGraphQL(removeFromFavoriteMutation, {
favoriteIdHash: args.favoriteIdHash,
questionId: args.questionId,
})) as { removeQuestionFromFavorite: { ok: boolean } };
return { ok: data.removeQuestionFromFavorite.ok };
}

// Retrieves solve status for a specific problem (lighter than full select).
export async function getProblemStatus(titleSlug: string) {
requireAuth();
const data = (await executeGraphQL(problemStatusQuery, { titleSlug })) as {
question: Record<string, unknown>;
};
return data.question;
}
85 changes: 85 additions & 0 deletions mcp/modules/problemTools.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import {
addProblemToFavorite,
getDailyProblem,
getDailyProblemLegacy,
getDailyProblemRaw,
getOfficialSolution,
getProblemNote,
getProblemSet,
getProblemStatus,
getSelectProblem,
getSelectProblemRaw,
getSubmissionDetails,
removeProblemFromFavorite,
updateProblemNote,
} from '../leetCodeService';
import { runTool } from '../serverUtils';
import { ToolModule } from '../types';
Expand Down Expand Up @@ -92,5 +98,84 @@ export class ProblemToolsModule implements ToolModule {
},
async () => runTool(() => getDailyProblemLegacy()),
);

// ── Auth-required tools ───────────────────────────────────────────

server.registerTool(
'leetcode_submission_details',
{
title: 'Submission Details',
description: '[Auth Required] Full submission: source code, runtime, memory, percentiles, errors',
inputSchema: {
submissionId: z.number().int().positive(),
},
},
async ({ submissionId }) => runTool(() => getSubmissionDetails({ submissionId })),
);

server.registerTool(
'leetcode_problem_note',
{
title: 'Problem Note',
description: "[Auth Required] User's personal note on a problem",
inputSchema: {
titleSlug: z.string(),
},
},
async ({ titleSlug }) => runTool(() => getProblemNote(titleSlug)),
);

server.registerTool(
'leetcode_problem_note_update',
{
title: 'Update Problem Note',
description: '[Auth Required] Create/update personal note on a problem',
inputSchema: {
titleSlug: z.string(),
note: z.string(),
},
},
async ({ titleSlug, note }) => runTool(() => updateProblemNote({ titleSlug, note })),
);

server.registerTool(
'leetcode_problem_favorite_add',
{
title: 'Add to Favorites',
description: '[Auth Required] Add problem to a favorites list',
inputSchema: {
favoriteIdHash: z.string(),
questionId: z.string(),
},
},
async ({ favoriteIdHash, questionId }) =>
runTool(() => addProblemToFavorite({ favoriteIdHash, questionId })),
);

server.registerTool(
'leetcode_problem_favorite_remove',
{
title: 'Remove from Favorites',
description: '[Auth Required] Remove problem from a favorites list',
inputSchema: {
favoriteIdHash: z.string(),
questionId: z.string(),
},
},
async ({ favoriteIdHash, questionId }) =>
runTool(() => removeProblemFromFavorite({ favoriteIdHash, questionId })),
);

server.registerTool(
'leetcode_problem_status',
{
title: 'Problem Status',
description: '[Auth Required] Solve status for a specific problem (ac/notac/null) — lighter than full select',
inputSchema: {
titleSlug: z.string(),
},
},
async ({ titleSlug }) => runTool(() => getProblemStatus(titleSlug)),
);
}
}
32 changes: 32 additions & 0 deletions mcp/modules/userTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import {
getUserContest,
getUserContestHistory,
getUserContestRankingInfo,
getUserFavorites,
getUserProfileAggregate,
getUserProfileCalendarRaw,
getUserProfileRaw,
getUserProfileSummary,
getUserProgress,
getUserProgressRaw,
getUserStatus,
getUserStreak,
} from '../leetCodeService';
import { runTool } from '../serverUtils';
import { ToolModule } from '../types';
Expand Down Expand Up @@ -248,5 +251,34 @@ export class UserToolsModule implements ToolModule {
},
async ({ username }) => runTool(() => getUserProgressRaw(username)),
);

// ── Auth-required tools ───────────────────────────────────────────

server.registerTool(
'leetcode_user_status',
{
title: 'Authenticated User Status',
description: '[Auth Required] Authenticated user info: username, isPremium, checkedInToday, notifications',
},
async () => runTool(() => getUserStatus()),
);

server.registerTool(
'leetcode_user_streak',
{
title: 'Daily Streak',
description: '[Auth Required] Daily streak: currentStreak, daysSkipped, todayCompleted',
},
async () => runTool(() => getUserStreak()),
);

server.registerTool(
'leetcode_user_favorites',
{
title: 'Favorite Lists',
description: "[Auth Required] User's favorite/bookmark problem lists with problem IDs",
},
async () => runTool(() => getUserFavorites()),
);
}
}
41 changes: 37 additions & 4 deletions mcp/serverUtils.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,51 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { readFileSync } from 'fs';
import { GraphQLParams, GraphQLClientError, ToolResponse, ToolExecutor, ToolModule } from './types';

const GRAPHQL_ENDPOINT = 'https://leetcode.com/graphql';
export const SERVER_VERSION = '1.0.0';

// Asserts that auth cookies are configured. Throws a clear error if not.
export function requireAuth(): void {
const auth = loadAuthData();
if (!auth.cookie) {
throw new Error(
'This tool requires authentication. Set LEETCODE_COOKIES_FILE env var with a path to your cookies JSON file.',
);
}
}

// Loads auth cookies from LEETCODE_COOKIES_FILE env var.
function loadAuthData(): { cookie: string; csrftoken: string } {
const cookieFile = process.env.LEETCODE_COOKIES_FILE;
if (!cookieFile) return { cookie: '', csrftoken: '' };
try {
const data = JSON.parse(readFileSync(cookieFile, 'utf-8'));
const parts: string[] = [];
if (data.LEETCODE_SESSION) parts.push(`LEETCODE_SESSION=${data.LEETCODE_SESSION}`);
if (data.csrftoken) parts.push(`csrftoken=${data.csrftoken}`);
if (data.cf_clearance) parts.push(`cf_clearance=${data.cf_clearance}`);
return { cookie: parts.join('; '), csrftoken: data.csrftoken || '' };
} catch {
return { cookie: '', csrftoken: '' };
}
}

// Executes a GraphQL query against the LeetCode API.
export async function executeGraphQL(query: string, variables: GraphQLParams = {}): Promise<unknown> {
const auth = loadAuthData();
const headers: Record<string, string> = {
'Content-Type': 'application/json',
Referer: 'https://leetcode.com',
};
if (auth.cookie) {
headers['Cookie'] = auth.cookie;
headers['x-csrftoken'] = auth.csrftoken;
}
const requestInit: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Referer: 'https://leetcode.com',
},
headers,
body: JSON.stringify({ query, variables }),
};

Expand Down
Loading