Skip to content
Merged
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
10 changes: 4 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,15 @@ The Interactive LeetCode MCP includes AI agent guidance through MCP Prompts to c

### Features

**Automatic Workspace Setup:**
When you fetch a problem, the MCP guides Claude to:
**Workspace Setup:**
When learning mode is active, Claude will:

- Create a workspace file named `{problem-slug}.{extension}`
- Paste the code template into the file
- Set up proper naming conventions (e.g., Java class names)

**Learning-Guided Mode:**
The MCP enforces pedagogical best practices:
When active, Claude follows these guidelines:

- Provides progressive hints (4 levels) before revealing solutions
- Asks guiding questions about approach and complexity
Expand All @@ -211,15 +211,13 @@ Guides you through the complete cycle:

### How to Use Learning Mode

The learning mode is always active. When working with LeetCode problems:
To activate learning mode, tell Claude you want to practice with guidance — for example, "Let's practice in learning mode" or "I want to learn two-sum with hints." Once active:

1. **Fetch a problem** to see the description and get workspace setup guidance
2. **Ask for hints** rather than solutions ("Give me a hint")
3. **Implement your solution** with progressive guidance
4. **Request the solution** only when you want to compare with optimal approach ("Show me the solution")

Claude will automatically follow learning-mode guidelines thanks to the MCP prompts.

## Troubleshooting

**"Not authorized" or "Invalid credentials" error**
Expand Down
245 changes: 243 additions & 2 deletions src/leetcode/leetcode-global-service.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import axios, { AxiosError } from "axios";
import { Credential, LeetCode } from "leetcode-query";
import {
LeetCodeCheckResponse,
LeetCodeSubmitResponse,
SubmissionResult
} from "../types/submission.js";
import logger from "../utils/logger.js";
import { SEARCH_PROBLEMS_QUERY } from "./graphql/search-problems.js";
import { SOLUTION_ARTICLE_DETAIL_QUERY } from "./graphql/solution-article-detail.js";
import { SOLUTION_ARTICLES_QUERY } from "./graphql/solution-articles.js";
import { LeetcodeServiceInterface } from "./leetcode-service-interface.js";

const LANGUAGE_MAP: Record<string, string> = {
java: "java",
python: "python3",
python3: "python3",
cpp: "cpp",
"c++": "cpp",
javascript: "javascript",
js: "javascript",
typescript: "typescript",
ts: "typescript"
};

/**
* LeetCode Global API Service Implementation
*
* This class provides methods to interact with the LeetCode Global API
*/
export class LeetCodeGlobalService implements LeetcodeServiceInterface {
private readonly leetCodeApi: LeetCode;
Expand Down Expand Up @@ -340,6 +356,191 @@ export class LeetCodeGlobalService implements LeetcodeServiceInterface {
});
}

async submitSolution(
problemSlug: string,
code: string,
language: string
): Promise<SubmissionResult> {
if (!this.isAuthenticated()) {
return {
accepted: false,
errorMessage: "Not authorized. Please run authorization first.",
statusMessage: "Authorization Required"
};
}

// Map language to LeetCode's expected format
const leetcodeLang = LANGUAGE_MAP[language.toLowerCase()];
if (!leetcodeLang) {
return {
accepted: false,
errorMessage: `Unsupported language: ${language}`,
statusMessage: "Invalid Language"
};
}

const baseUrl = "https://leetcode.com";

try {
// First, get the numeric question ID
const questionId = await this.getQuestionId(problemSlug, baseUrl);

// Submit solution
const submitUrl = `${baseUrl}/problems/${problemSlug}/submit/`;

const submitResponse = await axios.post<LeetCodeSubmitResponse>(
submitUrl,
{
lang: leetcodeLang,
question_id: questionId,
typed_code: code
},
{
headers: {
"Content-Type": "application/json",
Cookie: `csrftoken=${this.credential.csrf}; LEETCODE_SESSION=${this.credential.session}`,
"X-CSRFToken": this.credential.csrf,
Referer: `${baseUrl}/problems/${problemSlug}/`
}
}
);

const submissionId = submitResponse.data.submission_id;

// Poll for results
const checkUrl = `${baseUrl}/submissions/detail/${submissionId}/check/`;
let attempts = 0;
const maxAttempts = 30;

while (attempts < maxAttempts) {
// Wait 1 second between polls
await new Promise((resolve) => setTimeout(resolve, 1000));

const checkResponse = await axios.get<LeetCodeCheckResponse>(
checkUrl,
{
headers: {
Cookie: `csrftoken=${this.credential.csrf}; LEETCODE_SESSION=${this.credential.session}`
}
}
);

const result = checkResponse.data;

if (
result.state !== "SUCCESS" &&
result.state !== "PENDING" &&
result.state !== "STARTED"
) {
return {
accepted: false,
statusMessage: "Error",
errorMessage: `Unexpected submission state: \${result.state}`
};
}

// Check if processing is complete
if (result.state === "SUCCESS") {
const accepted = result.status_msg === "Accepted";

if (accepted) {
return {
accepted: true,
runtime: result.runtime,
memory: result.memory,
statusMessage: "Accepted"
};
} else {
// Failed - extract test case info
let failedTestCase = "";
if (result.input) {
failedTestCase = `Input: ${result.input}`;
if (result.expected_answer && result.code_answer) {
failedTestCase += `\\nExpected: ${JSON.stringify(result.expected_answer)}`;
failedTestCase += `\\nGot: ${JSON.stringify(result.code_answer)}`;
}
}

return {
accepted: false,
statusMessage: result.status_msg,
failedTestCase,
errorMessage: result.std_output
};
}
}

attempts++;
}

// Timeout
return {
accepted: false,
statusMessage: "Timeout",
errorMessage: "Submission check timed out after 30 seconds"
};
} catch (error) {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;

if (axiosError.response?.status === 401) {
return {
accepted: false,
statusMessage: "Unauthorized",
errorMessage: "Session expired. Please re-authorize."
};
}

return {
accepted: false,
statusMessage: "Submission Failed",
errorMessage: axiosError.message
};
}

return {
accepted: false,
statusMessage: "Error",
errorMessage:
error instanceof Error ? error.message : String(error)
};
}
}

private async getQuestionId(
problemSlug: string,
baseUrl: string
): Promise<string> {
const graphqlQuery = {
query: `
query questionTitle($titleSlug: String!) {
question(titleSlug: $titleSlug) {
questionId
questionFrontendId
}
}
`,
variables: { titleSlug: problemSlug }
};

const response = await axios.post(`${baseUrl}/graphql`, graphqlQuery, {
headers: {
"Content-Type": "application/json",
Cookie: `csrftoken=${this.credential.csrf}; LEETCODE_SESSION=${this.credential.session}`,
"X-CSRFToken": this.credential.csrf,
Referer: `${baseUrl}/problems/${problemSlug}/`
}
});

const question = response.data.data?.question;
if (!question) {
throw new Error(
`Problem slug "\${problemSlug}" not found or invalid.`
);
}
return question.questionId;
}

isAuthenticated(): boolean {
return (
!!this.credential &&
Expand All @@ -348,6 +549,46 @@ export class LeetCodeGlobalService implements LeetcodeServiceInterface {
);
}

async validateCredentials(
csrf: string,
session: string
): Promise<string | null> {
try {
// Make a simple GraphQL query to validate credentials
const graphqlQuery = {
query: `
query globalData {
userStatus {
username
isSignedIn
}
}
`
};

const response = await axios.post(
"https://leetcode.com/graphql",
graphqlQuery,
{
headers: {
"Content-Type": "application/json",
Cookie: `csrftoken=${csrf}; LEETCODE_SESSION=${session}`,
"X-CSRFToken": csrf
}
}
);

// Check if user is signed in and return username
const userStatus = response.data?.data?.userStatus;
if (userStatus?.isSignedIn === true && userStatus?.username) {
return userStatus.username;
}
return null;
} catch {
return null;
}
}

updateCredentials(csrf: string, session: string): void {
this.credential.csrf = csrf;
this.credential.session = session;
Expand Down
25 changes: 25 additions & 0 deletions src/leetcode/leetcode-service-interface.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { SubmissionResult } from "../types/submission.js";

/**
* Base interface for LeetCode API service implementations.
* Defines the common methods that all LeetCode service implementations must provide.
Expand Down Expand Up @@ -179,4 +181,27 @@ export interface LeetcodeServiceInterface {
* @returns Promise resolving to the solution detail data
*/
fetchSolutionArticleDetail(identifier: string): Promise<any>;

/**
* Submits a solution to a problem and polls for the result.
*
* @param problemSlug - The URL slug/identifier of the problem
* @param code - The source code to submit
* @param language - The programming language of the source code
* @returns Promise resolving to the submission result
*/
submitSolution(
problemSlug: string,
code: string,
language: string
): Promise<SubmissionResult>;

/**
* Validates LeetCode credentials by making a test API call.
*
* @param csrf - The CSRF token
* @param session - The session token
* @returns Promise resolving to the username if valid, null otherwise
*/
validateCredentials(csrf: string, session: string): Promise<string | null>;
}
Loading