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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,13 @@ For LeetCode China site, modify the `--site` parameter to `cn`.
| **get_problem_progress** | ✅ | ✅ | ✅ | Retrieves current user's problem-solving progress |
| **get_all_submissions** | ✅ | ✅ | ✅ | Retrieves current user's submission history |

### Submissions

| Tool | Global | CN | Auth Required | Description |
| ------------------- | :----: | :-: | :-----------: | ------------------------------------------------------------- |
| **run_code** | ✅ | ✅ | ✅ | Runs code for a problem and polls `/check/` until finished |
| **submit_solution** | ✅ | ✅ | ✅ | Submits code for a problem and polls `/check/` until finished |

### Notes

| Tool | Global | CN | Auth Required | Description |
Expand Down Expand Up @@ -232,6 +239,25 @@ For LeetCode China site, modify the `--site` parameter to `cn`.
- `status`: Submission status filter (enum: "AC", "WA", optional, CN only)
- `lastKey`: Pagination token for retrieving next page (string, optional, CN only)

### Submissions

- **run_code** - Runs code for a specific problem and waits until finished (requires authentication)

- `titleSlug`: The URL slug/identifier of the problem (string, required)
- `lang`: Programming language (string enum, required)
- `typedCode`: Source code to run (string, required)
- `dataInput`: Custom input to run (string, optional)
- `timeoutMs`: Polling timeout in milliseconds (number, optional, default: 120000)
- `pollIntervalMs`: Polling interval in milliseconds (number, optional, default: 1500)

- **submit_solution** - Submits code for a specific problem and waits until finished (requires authentication)

- `titleSlug`: The URL slug/identifier of the problem (string, required)
- `lang`: Programming language (string enum, required)
- `typedCode`: Source code to submit (string, required)
- `timeoutMs`: Polling timeout in milliseconds (number, optional, default: 120000)
- `pollIntervalMs`: Polling interval in milliseconds (number, optional, default: 1500)

### Notes

- **search_notes** - Searches for user notes on LeetCode China
Expand Down
26 changes: 26 additions & 0 deletions README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ node build/index.js --site cn
| **get_problem_progress** | ✅ | ✅ | ✅ | 获取用户的答题进度 |
| **get_all_submissions** | ✅ | ✅ | ✅ | 获取用户提交的分页列表 |

### 提交 / 运行

| 工具 | 全球站 | 中国站 | 需要认证 | 描述 |
| ------------------- | :----: | :----: | :------: | --------------------------------- |
| **run_code** | ✅ | ✅ | ✅ | 运行代码并轮询 `/check/` 直到结束 |
| **submit_solution** | ✅ | ✅ | ✅ | 提交代码并轮询 `/check/` 直到结束 |

### 笔记

| 工具 | 全球站 | 中国站 | 需要认证 | 描述 |
Expand Down Expand Up @@ -233,6 +240,25 @@ node build/index.js --site cn
- `status`:提交状态过滤器(枚举:"AC"、"WA",可选,仅中国站)
- `lastKey`:用于检索下一页的分页令牌(字符串,可选,仅中国站)

### 提交 / 运行

- **run_code** - 运行指定题目的代码并等待结束(需要认证)

- `titleSlug`:题目的 URL 标识符(字符串,必需)
- `lang`:编程语言(字符串枚举,必需)
- `typedCode`:要运行的源码(字符串,必需)
- `dataInput`:自定义运行输入(字符串,可选)
- `timeoutMs`:轮询超时毫秒数(数字,可选,默认:120000)
- `pollIntervalMs`:轮询间隔毫秒数(数字,可选,默认:1500)

- **submit_solution** - 提交指定题目的代码并等待结束(需要认证)

- `titleSlug`:题目的 URL 标识符(字符串,必需)
- `lang`:编程语言(字符串枚举,必需)
- `typedCode`:要提交的源码(字符串,必需)
- `timeoutMs`:轮询超时毫秒数(数字,可选,默认:120000)
- `pollIntervalMs`:轮询间隔毫秒数(数字,可选,默认:1500)

### 笔记

- **search_notes** - 搜索 LeetCode 中国站上的用户笔记
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerContestTools } from "./mcp/tools/contest-tools.js";
import { registerNoteTools } from "./mcp/tools/note-tools.js";
import { registerProblemTools } from "./mcp/tools/problem-tools.js";
import { registerSolutionTools } from "./mcp/tools/solution-tools.js";
import { registerSubmissionTools } from "./mcp/tools/submission-tools.js";
import { registerUserTools } from "./mcp/tools/user-tools.js";
import logger from "./utils/logger.js";

Expand Down Expand Up @@ -95,6 +96,7 @@ async function main() {
registerContestTools(server, leetcodeService);
registerSolutionTools(server, leetcodeService);
registerNoteTools(server, leetcodeService);
registerSubmissionTools(server, leetcodeService);

registerProblemResources(server, leetcodeService);
registerSolutionResources(server, leetcodeService);
Expand Down
39 changes: 39 additions & 0 deletions src/leetcode/leetcode-base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,45 @@ export interface LeetCodeBaseService {
*/
isAuthenticated(): boolean;

/**
* Runs code against LeetCode's interpreter (Run) and polls the check endpoint
* until a non-pending state is reached.
*
* Requires authentication.
*/
runCode(params: {
titleSlug: string;
questionId: string;
lang: string;
typedCode: string;
dataInput?: string;
timeoutMs?: number;
pollIntervalMs?: number;
}): Promise<{
start: Record<string, unknown>;
checkUrl: string;
check: Record<string, unknown>;
}>;

/**
* Submits code to LeetCode (Submit) and polls the check endpoint
* until a non-pending state is reached.
*
* Requires authentication.
*/
submitSolution(params: {
titleSlug: string;
questionId: string;
lang: string;
typedCode: string;
timeoutMs?: number;
pollIntervalMs?: number;
}): Promise<{
start: Record<string, unknown>;
checkUrl: string;
check: Record<string, unknown>;
}>;

/**
* Determines if the current service is for the China version of LeetCode.
*
Expand Down
105 changes: 105 additions & 0 deletions src/leetcode/leetcode-cn-service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { Credential, LeetCodeCN } from "leetcode-query";
import {
assertRunStartResponse,
assertSubmitStartResponse,
buildLeetCodeHeaders,
buildLeetCodeHttpAuth,
pollCheck,
postJson
} from "../utils/leetcode-http.js";
import logger from "../utils/logger.js";
import {
NOTE_AGGREGATE_QUERY,
Expand All @@ -19,12 +27,27 @@ import { LeetCodeBaseService } from "./leetcode-base-service.js";
export class LeetCodeCNService implements LeetCodeBaseService {
private readonly leetCodeApi: LeetCodeCN;
private readonly credential: Credential;
private readonly origin = "https://leetcode.cn";

constructor(leetCodeApi: LeetCodeCN, credential: Credential) {
this.leetCodeApi = leetCodeApi;
this.credential = credential;
}

private getHttpHeaders(titleSlug: string): HeadersInit {
const auth = buildLeetCodeHttpAuth({
session: this.credential.session ?? "",
csrfToken: this.credential.csrf ?? ""
});

const referer = `${this.origin}/problems/${titleSlug}/`;
return buildLeetCodeHeaders({
auth,
origin: this.origin,
referer
});
}

async fetchUserSubmissionDetail(id: number): Promise<any> {
if (!this.isAuthenticated()) {
throw new Error(
Expand Down Expand Up @@ -538,6 +561,88 @@ export class LeetCodeCNService implements LeetCodeBaseService {
});
}

async runCode(params: {
titleSlug: string;
questionId: string;
lang: string;
typedCode: string;
dataInput?: string;
timeoutMs?: number;
pollIntervalMs?: number;
}): Promise<{
start: Record<string, unknown>;
checkUrl: string;
check: Record<string, unknown>;
}> {
if (!this.isAuthenticated()) {
throw new Error("Authentication required to run code");
}

const headers = this.getHttpHeaders(params.titleSlug);
const startUrl = `${this.origin}/problems/${params.titleSlug}/interpret_solution/`;

const start = await postJson(
startUrl,
{
data_input: params.dataInput ?? "",
lang: params.lang,
question_id: params.questionId,
typed_code: params.typedCode
},
headers
);

assertRunStartResponse(start, `POST ${startUrl}`);

const checkUrl = `${this.origin}/submissions/detail/${start.interpret_id}/check/`;
const check = await pollCheck(checkUrl, headers, {
timeoutMs: params.timeoutMs,
pollIntervalMs: params.pollIntervalMs
});

return { start, checkUrl, check };
}

async submitSolution(params: {
titleSlug: string;
questionId: string;
lang: string;
typedCode: string;
timeoutMs?: number;
pollIntervalMs?: number;
}): Promise<{
start: Record<string, unknown>;
checkUrl: string;
check: Record<string, unknown>;
}> {
if (!this.isAuthenticated()) {
throw new Error("Authentication required to submit solution");
}

const headers = this.getHttpHeaders(params.titleSlug);
const startUrl = `${this.origin}/problems/${params.titleSlug}/submit/`;

const start = await postJson(
startUrl,
{
lang: params.lang,
question_id: params.questionId,
typed_code: params.typedCode
},
headers
);

assertSubmitStartResponse(start, `POST ${startUrl}`);

const checkUrl = `${this.origin}/submissions/detail/${start.submission_id}/check/`;
const check = await pollCheck(checkUrl, headers, {
timeoutMs: params.timeoutMs,
pollIntervalMs: params.pollIntervalMs
});

return { start, checkUrl, check };
}

isAuthenticated(): boolean {
return (
!!this.credential &&
Expand Down
Loading