Skip to content

Commit 8c915e7

Browse files
authored
Merge pull request #11 from Gerrit1999/feat/run-submit-mcp-tools
feat(submissions): add run_code and submit_solution tools
2 parents ba62cbd + c777de3 commit 8c915e7

9 files changed

Lines changed: 882 additions & 0 deletions

File tree

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,13 @@ For LeetCode China site, modify the `--site` parameter to `cn`.
152152
| **get_problem_progress** |||| Retrieves current user's problem-solving progress |
153153
| **get_all_submissions** |||| Retrieves current user's submission history |
154154

155+
### Submissions
156+
157+
| Tool | Global | CN | Auth Required | Description |
158+
| ------------------- | :----: | :-: | :-----------: | ------------------------------------------------------------- |
159+
| **run_code** |||| Runs code for a problem and polls `/check/` until finished |
160+
| **submit_solution** |||| Submits code for a problem and polls `/check/` until finished |
161+
155162
### Notes
156163

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

242+
### Submissions
243+
244+
- **run_code** - Runs code for a specific problem and waits until finished (requires authentication)
245+
246+
- `titleSlug`: The URL slug/identifier of the problem (string, required)
247+
- `lang`: Programming language (string enum, required)
248+
- `typedCode`: Source code to run (string, required)
249+
- `dataInput`: Custom input to run (string, optional)
250+
- `timeoutMs`: Polling timeout in milliseconds (number, optional, default: 120000)
251+
- `pollIntervalMs`: Polling interval in milliseconds (number, optional, default: 1500)
252+
253+
- **submit_solution** - Submits code for a specific problem and waits until finished (requires authentication)
254+
255+
- `titleSlug`: The URL slug/identifier of the problem (string, required)
256+
- `lang`: Programming language (string enum, required)
257+
- `typedCode`: Source code to submit (string, required)
258+
- `timeoutMs`: Polling timeout in milliseconds (number, optional, default: 120000)
259+
- `pollIntervalMs`: Polling interval in milliseconds (number, optional, default: 1500)
260+
235261
### Notes
236262

237263
- **search_notes** - Searches for user notes on LeetCode China

README_zh-CN.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,13 @@ node build/index.js --site cn
153153
| **get_problem_progress** |||| 获取用户的答题进度 |
154154
| **get_all_submissions** |||| 获取用户提交的分页列表 |
155155

156+
### 提交 / 运行
157+
158+
| 工具 | 全球站 | 中国站 | 需要认证 | 描述 |
159+
| ------------------- | :----: | :----: | :------: | --------------------------------- |
160+
| **run_code** |||| 运行代码并轮询 `/check/` 直到结束 |
161+
| **submit_solution** |||| 提交代码并轮询 `/check/` 直到结束 |
162+
156163
### 笔记
157164

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

243+
### 提交 / 运行
244+
245+
- **run_code** - 运行指定题目的代码并等待结束(需要认证)
246+
247+
- `titleSlug`:题目的 URL 标识符(字符串,必需)
248+
- `lang`:编程语言(字符串枚举,必需)
249+
- `typedCode`:要运行的源码(字符串,必需)
250+
- `dataInput`:自定义运行输入(字符串,可选)
251+
- `timeoutMs`:轮询超时毫秒数(数字,可选,默认:120000)
252+
- `pollIntervalMs`:轮询间隔毫秒数(数字,可选,默认:1500)
253+
254+
- **submit_solution** - 提交指定题目的代码并等待结束(需要认证)
255+
256+
- `titleSlug`:题目的 URL 标识符(字符串,必需)
257+
- `lang`:编程语言(字符串枚举,必需)
258+
- `typedCode`:要提交的源码(字符串,必需)
259+
- `timeoutMs`:轮询超时毫秒数(数字,可选,默认:120000)
260+
- `pollIntervalMs`:轮询间隔毫秒数(数字,可选,默认:1500)
261+
236262
### 笔记
237263

238264
- **search_notes** - 搜索 LeetCode 中国站上的用户笔记

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { registerContestTools } from "./mcp/tools/contest-tools.js";
1313
import { registerNoteTools } from "./mcp/tools/note-tools.js";
1414
import { registerProblemTools } from "./mcp/tools/problem-tools.js";
1515
import { registerSolutionTools } from "./mcp/tools/solution-tools.js";
16+
import { registerSubmissionTools } from "./mcp/tools/submission-tools.js";
1617
import { registerUserTools } from "./mcp/tools/user-tools.js";
1718
import logger from "./utils/logger.js";
1819

@@ -95,6 +96,7 @@ async function main() {
9596
registerContestTools(server, leetcodeService);
9697
registerSolutionTools(server, leetcodeService);
9798
registerNoteTools(server, leetcodeService);
99+
registerSubmissionTools(server, leetcodeService);
98100

99101
registerProblemResources(server, leetcodeService);
100102
registerSolutionResources(server, leetcodeService);

src/leetcode/leetcode-base-service.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,45 @@ export interface LeetCodeBaseService {
153153
*/
154154
isAuthenticated(): boolean;
155155

156+
/**
157+
* Runs code against LeetCode's interpreter (Run) and polls the check endpoint
158+
* until a non-pending state is reached.
159+
*
160+
* Requires authentication.
161+
*/
162+
runCode(params: {
163+
titleSlug: string;
164+
questionId: string;
165+
lang: string;
166+
typedCode: string;
167+
dataInput?: string;
168+
timeoutMs?: number;
169+
pollIntervalMs?: number;
170+
}): Promise<{
171+
start: Record<string, unknown>;
172+
checkUrl: string;
173+
check: Record<string, unknown>;
174+
}>;
175+
176+
/**
177+
* Submits code to LeetCode (Submit) and polls the check endpoint
178+
* until a non-pending state is reached.
179+
*
180+
* Requires authentication.
181+
*/
182+
submitSolution(params: {
183+
titleSlug: string;
184+
questionId: string;
185+
lang: string;
186+
typedCode: string;
187+
timeoutMs?: number;
188+
pollIntervalMs?: number;
189+
}): Promise<{
190+
start: Record<string, unknown>;
191+
checkUrl: string;
192+
check: Record<string, unknown>;
193+
}>;
194+
156195
/**
157196
* Determines if the current service is for the China version of LeetCode.
158197
*

src/leetcode/leetcode-cn-service.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { Credential, LeetCodeCN } from "leetcode-query";
2+
import {
3+
assertRunStartResponse,
4+
assertSubmitStartResponse,
5+
buildLeetCodeHeaders,
6+
buildLeetCodeHttpAuth,
7+
pollCheck,
8+
postJson
9+
} from "../utils/leetcode-http.js";
210
import logger from "../utils/logger.js";
311
import {
412
NOTE_AGGREGATE_QUERY,
@@ -19,12 +27,27 @@ import { LeetCodeBaseService } from "./leetcode-base-service.js";
1927
export class LeetCodeCNService implements LeetCodeBaseService {
2028
private readonly leetCodeApi: LeetCodeCN;
2129
private readonly credential: Credential;
30+
private readonly origin = "https://leetcode.cn";
2231

2332
constructor(leetCodeApi: LeetCodeCN, credential: Credential) {
2433
this.leetCodeApi = leetCodeApi;
2534
this.credential = credential;
2635
}
2736

37+
private getHttpHeaders(titleSlug: string): HeadersInit {
38+
const auth = buildLeetCodeHttpAuth({
39+
session: this.credential.session ?? "",
40+
csrfToken: this.credential.csrf ?? ""
41+
});
42+
43+
const referer = `${this.origin}/problems/${titleSlug}/`;
44+
return buildLeetCodeHeaders({
45+
auth,
46+
origin: this.origin,
47+
referer
48+
});
49+
}
50+
2851
async fetchUserSubmissionDetail(id: number): Promise<any> {
2952
if (!this.isAuthenticated()) {
3053
throw new Error(
@@ -538,6 +561,88 @@ export class LeetCodeCNService implements LeetCodeBaseService {
538561
});
539562
}
540563

564+
async runCode(params: {
565+
titleSlug: string;
566+
questionId: string;
567+
lang: string;
568+
typedCode: string;
569+
dataInput?: string;
570+
timeoutMs?: number;
571+
pollIntervalMs?: number;
572+
}): Promise<{
573+
start: Record<string, unknown>;
574+
checkUrl: string;
575+
check: Record<string, unknown>;
576+
}> {
577+
if (!this.isAuthenticated()) {
578+
throw new Error("Authentication required to run code");
579+
}
580+
581+
const headers = this.getHttpHeaders(params.titleSlug);
582+
const startUrl = `${this.origin}/problems/${params.titleSlug}/interpret_solution/`;
583+
584+
const start = await postJson(
585+
startUrl,
586+
{
587+
data_input: params.dataInput ?? "",
588+
lang: params.lang,
589+
question_id: params.questionId,
590+
typed_code: params.typedCode
591+
},
592+
headers
593+
);
594+
595+
assertRunStartResponse(start, `POST ${startUrl}`);
596+
597+
const checkUrl = `${this.origin}/submissions/detail/${start.interpret_id}/check/`;
598+
const check = await pollCheck(checkUrl, headers, {
599+
timeoutMs: params.timeoutMs,
600+
pollIntervalMs: params.pollIntervalMs
601+
});
602+
603+
return { start, checkUrl, check };
604+
}
605+
606+
async submitSolution(params: {
607+
titleSlug: string;
608+
questionId: string;
609+
lang: string;
610+
typedCode: string;
611+
timeoutMs?: number;
612+
pollIntervalMs?: number;
613+
}): Promise<{
614+
start: Record<string, unknown>;
615+
checkUrl: string;
616+
check: Record<string, unknown>;
617+
}> {
618+
if (!this.isAuthenticated()) {
619+
throw new Error("Authentication required to submit solution");
620+
}
621+
622+
const headers = this.getHttpHeaders(params.titleSlug);
623+
const startUrl = `${this.origin}/problems/${params.titleSlug}/submit/`;
624+
625+
const start = await postJson(
626+
startUrl,
627+
{
628+
lang: params.lang,
629+
question_id: params.questionId,
630+
typed_code: params.typedCode
631+
},
632+
headers
633+
);
634+
635+
assertSubmitStartResponse(start, `POST ${startUrl}`);
636+
637+
const checkUrl = `${this.origin}/submissions/detail/${start.submission_id}/check/`;
638+
const check = await pollCheck(checkUrl, headers, {
639+
timeoutMs: params.timeoutMs,
640+
pollIntervalMs: params.pollIntervalMs
641+
});
642+
643+
return { start, checkUrl, check };
644+
}
645+
541646
isAuthenticated(): boolean {
542647
return (
543648
!!this.credential &&

0 commit comments

Comments
 (0)