Skip to main content
Glama
README.md
# judge0-mcp

把 [Judge0](https://github.com/judge0/judge0) 评测栈包装成 **MCP 服务的本地算法题评测系统**。

它向 MCP 客户端(如 VS Code Copilot、Claude Desktop、WorkBuddy 等)暴露 7 个工具,让大模型完成
「读题 → 样例自测 → 全量隐藏数据提交」的完整评测闭环;判题参数、提交次数、数据访问边界等
均由服务端强制,模型无法绕过。适用于**大模型编程能力评测**、算法教学实验与自动化判题场景。

## 特性

- **7 个 MCP 工具**:题库浏览、题面/样例下发、样例自测、全量提交、结果查询、统计聚合。
- **判题参数服务端注入**:语言 id、编译选项、CPU/wall 时限、内存全部取自配置与题库,调用方传入一律无效。
- **提交约束**:按模式限制每题提交次数;完全相同的代码经 MD5 去重后拒绝且**不消耗**次数。
- **数据边界**:隐藏测试数据不进入模型工作区;提交结果只回传状态/耗时/内存/分数,**绝不回传 `expected_output`**。
- **工作区防护**:题面与样例只能拷入 `AGENT_WORKSPACE` 内,拒绝路径穿越。
- **评测机故障处理**:Judge0 返回 Internal Error 时自动重试,整批仍故障则**不消耗**提交次数。
- **实验记录**:每次工具调用自动写入 JSONL(`runs/<run_id>.jsonl`),内置统计脚本一键出报告。
- **配置与题库解耦**:基础设施在 `config.json`,题库在 `problems.json`,代码内不写死题目。
- **可移植**:路径全部可配置(配置文件 + 环境变量),无任何机器相关硬编码。

## 架构

```mermaid
flowchart LR
    A["MCP 客户端<br/>(Copilot / Claude / WorkBuddy)"] -- "stdio (JSON-RPC)" --> B["server.py<br/>FastMCP 服务"]
    B -- "HTTP :2358" --> C["Judge0 Server"]
    C -- "队列" --> D["Judge0 Workers"]
    D -- "isolate 沙箱" --> E["编译 & 执行<br/>Python 3.8.1 / GCC 9.2.0"]
    B -. "读题面/样例" .-> F["statement/"]
    B -. "读隐藏数据" .-> G["data/"]
    B -. "写实验记录" .-> H["runs/*.jsonl"]
```

- Judge0 通常部署在 **WSL2 / Linux**(Docker),MCP 服务可跑在 Windows 或同一 Linux 环境。
- 隐藏测试数据只由服务端读取,模型只能看到判题结论。

## 目录结构

```
.
├── server.py                 # ★ MCP 服务主程序(7 工具)
├── config.json               # 基础设施配置(路径/并发/约束/语言表/题面头部模板)
├── problems.example.json     # 题库模板(复制为 problems.json 后填入真实题目)
├── problems.json             # 题库元数据(本地资产,不随仓库发布)
├── analyze_runs.py           # 实验统计脚本(聚合 runs/*.jsonl)
├── requirements.txt          # Python 依赖
├── mcp.example.json          # MCP 客户端配置模板
├── docs/agent-prompt.md      # 被测模型的系统提示词参考
├── tools/pack_dataset.py     # 数据集打包脚本(题库材料 → 带校验清单的 zip)
├── statement/                # 题面 + 公开样例压缩包(本地资产)
├── data/<id>/                # 隐藏测试数据(本地资产,不入版本库)
├── judge0-v1.13.1/           # Judge0 部署包(compose + 启动脚本 + 配置模板)
├── cache/samples/<id>/       # 启动时预解压的样例(运行时生成)
└── runs/<run_id>.jsonl       # 实验记录(运行时生成)
```

> **为什么 `statement/` 与 `data/` 存在但不在版本库里**:题面、样例与测试数据版权归原始出题方所有,
> 通过 `.gitignore` 排除、由使用者自行准备。两者都放在仓库目录内,使相对路径可预期、开箱即用;
> 位置可通过 `config.json` 或环境变量重定向。

## 前置条件

| 项 | 要求 |
|----|------|
| Python | ≥ 3.10(开发验证环境为 CPython 3.13.5) |
| Judge0 | 可访问的 Judge0 CE 1.13.1 实例(默认 `http://localhost:2358`) |
| Docker | 若自行部署 Judge0:Docker Engine ≥ 20(含 `docker compose` v2);磁盘 ≥ 15 GB |
| 题库材料 | 题面 Markdown、公开样例压缩包、隐藏测试数据(见下节) |

## 快速开始

### 1. 准备 Judge0 后端

若已有可用实例,直接跳过。若需自建,本仓库在 `judge0-v1.13.1/` 提供了自包含部署包:

```bash
cd judge0-v1.13.1
bash start_judge0.sh        # 首次运行会生成带随机密码的 judge0.conf,然后拉镜像并启动
```

脚本会依次完成:生成 `judge0.conf` → 环境检查 → 拉取镜像 → 启动 db/redis → 等待 postgres →
启动 server/workers → 等待 API → **真实提交冒烟验证**。看到以下输出即表示评测链路可用:

```
==> ✅ Judge0 已就绪, 评测链路正常 (http://localhost:2358)
```

> WSL2 下 cgroup v2 与官方 Judge0 镜像的 isolate 沙箱不兼容(所有提交返回 Internal Error),
> 本部署包已改用社区兼容镜像,详见 `judge0-v1.13.1/README.md`。

验证服务:

```bash
curl -s http://localhost:2358/about          # 版本信息
curl -s http://localhost:2358/languages      # 语言列表
```

### 2. 创建 Python 环境

```powershell
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
```

### 3. 准备题库与数据

题库材料不入库,需要自行放置:

```powershell
Copy-Item problems.example.json problems.json    # 然后填入真实题目元数据
```

| 材料 | 放置位置 | 说明 |
|------|----------|------|
| 题库元数据 | `problems.json` | 复制 `problems.example.json` 后填写(字段说明见模板内 `_readme`) |
| 题面 + 公开样例 | `statement/` | `<id>.md` 与 `<id>.zip`,zip 内含 `<id>N.in` / `<id>N.ans` |
| 隐藏测试数据 | `paths.data_dir`(默认仓库内的 `data/`) | 每题目录下 `<id>N.in` / `<id>N.ans` |

路径均可通过 `config.json` 或环境变量重定向(见「环境变量」)。若已有打包好的数据集,
用 `tools/pack_dataset.py` 生成的 zip 解包即可(该包内含 `MANIFEST.sha256` 校验清单)。

### 4. 接入 MCP 客户端

复制 `mcp.example.json`,把占位符替换为实际路径,然后填入客户端的 MCP 配置:

```jsonc
{
  "mcpServers": {
    "judge0-mcp": {
      "command": "<REPO_ROOT>/.venv/Scripts/python.exe",   // 必须用装了依赖的 venv 解释器
      "args": ["<REPO_ROOT>/mcp-server/server.py"],
      "env": {
        "AGENT_WORKSPACE": "<WORKSPACE_DIR>",              // 模型工作区(题面/样例拷入处)
        "MODEL_NAME": "<MODEL_NAME>",                      // 当次被测模型名(实验记录维度)
        "MODE": "iterative",                               // iterative | one-shot
        "RUN_ID": "<RUN_ID>",                              // 每次实验唯一 id(JSONL 文件名)
        "JUDGE0_BASE": "http://localhost:2358"
      }
    }
  }
}
```

> Agent 无法篡改这些变量(服务端只读 env),因此 `MODEL_NAME` / `RUN_ID` / `MODE` 是可靠的统计维度。
> 每次正式实验前记得更换 `RUN_ID` 与 `MODEL_NAME`,并重启 MCP 服务进程。

### 5. 自检

```powershell
# 依赖与工具注册
.\.venv\Scripts\python.exe -c "import sys; sys.path.insert(0,'mcp-server'); import server, asyncio; print(asyncio.run(server.mcp.list_tools()))"
```

随后在 MCP 客户端中让模型执行一次完整闭环:`list_problems` → `init_problem` → `run_test`(样例 AC)
→ `submit_code`(满分)。若通过,说明接入成功。

## 工具参考

| 工具 | 签名 | 说明 |
|------|------|------|
| 题库列表 | `list_problems()` | 返回每题 `problem_id`、`name`、点数、分值、时空限制、可用语言、`init_state` |
| 初始化题目 | `init_problem(problem_id, target_dir=None)` | 把题面与公开样例拷入工作区,题面头部注入【评测约定】;幂等 |
| 题目详情 | `get_problem(problem_id)` | 元数据白名单(含 `io_mode`、`sample_groups`),不含题面正文与内部元数据 |
| 样例自测 | `run_test(problem_id, code, lang, sample_no=None, stdin=None)` | `sample_no` 判定样例;`stdin` 只运行不判题。返回 `verdict`/`stdout`/`time`/`memory`/CE 编译输出 |
| 正式提交 | `submit_code(problem_id, code, lang)` | 全量隐藏测试点批测,返回逐点状态、分数、`final_status`、`failed_summary` |
| 结果查询 | `get_result(submission_id)` | 仅用于 `submit_code` 返回 pending 时 |
| 统计聚合 | `get_statistics()` | 聚合 `runs/*.jsonl`(供研究者,不建议被测模型调用) |

`lang` 取值由 `config.json` 的 `languages` 定义(默认 `python3` / `cpp`);
**不接受语言 id 或编译参数**,传入会报错。

### 服务端强制约束

| 约束 | 行为 |
|------|------|
| 提交次数 | `iterative` 模式每题最多 3 次;`one-shot` 模式仅 1 次。超出直接拒绝 |
| 重复代码 | MD5 去重,命中则拒绝且**不消耗**次数 |
| 判题参数 | 语言 id / 编译选项 / 时限 / 内存全部由服务端注入 |
| IO 预检 | 检测到 `freopen` / `ifstream` / `fstream` 等文件 IO 时返回 `warnings`(仅警告,不拒绝) |
| 样例自测 | `iterative` 不限次(同题第 10 次起软提醒);`one-shot` **硬拒绝** |
| 评测机故障 | 单点自动重试;整批仍有 IE/EF 时返回 `machine_failure: true`,**不消耗**次数、不登记 MD5 |
| 隐藏数据 | 只读不写;返回值不含 `expected_output` |
| 工作区 | `init_problem` 目标必须位于 `AGENT_WORKSPACE` 内,否则拒绝 |
| 批测 | 并发上限 `judge0.max_workers`;超过 `judge0.batch_timeout_seconds` 返回 `pending` |
| 记录 | 每次工具调用追加写入 `runs/<run_id>.jsonl` |

## 配置

### config.json

| 段 | 内容 |
|----|------|
| `server` | MCP 服务名、版本、`instructions`(提供给客户端的工具使用说明) |
| `paths` | `problems_file` / `statement_dir` / `cache_dir` / `runs_dir` / `data_dir`(相对本文件;可被环境变量覆盖) |
| `judge0` | `base_url`、超时与轮询参数、`max_workers`、WSL 回退开关、状态码映射、MLE 归类阈值 |
| `constraints` | 提交次数上限、`run_test` 软提醒阈值、故障重试策略、失败类型优先级、IO 预检正则 |
| `languages` | 语言槽位名 → `id` / `display` / `compiler_options` |
| `statement_header` | 题面头部【评测约定】模板(`{languages}`、`{compile_options}`、`{cpu_limit}`、`{wall_limit}`、`{memory_mb}` 等占位符) |

### 环境变量

| 变量 | 作用 |
|------|------|
| `AGENT_WORKSPACE` | 模型工作区根目录;`init_problem` 只能写入此目录内。**未设置时拒绝 init** |
| `MODEL_NAME` | 被测模型名,写入 JSONL 供统计归组 |
| `MODE` | `iterative` 或 `one-shot`,决定提交上限与 `run_test` 可用性 |
| `RUN_ID` | 本次实验 id,决定 JSONL 文件名 |
| `JUDGE0_BASE` | Judge0 地址(默认取 `config.json` 的 `judge0.base_url`) |
| `JUDGE0_CONFIG_FILE` | 指定的 `config.json` 路径 |
| `JUDGE0_PROBLEMS_FILE` | 题库文件路径 |
| `JUDGE0_STATEMENT_DIR` | 题面与样例目录 |
| `JUDGE0_CACHE_DIR` | 样例缓存目录 |
| `JUDGE0_RUNS_DIR` | 实验记录目录 |
| `JUDGE0_DATA_DIR` | 隐藏测试数据目录(默认 `data/`) |

> 路径优先级:**环境变量 > config.json > 内置默认**。

## 评测约定与规格

服务端在 `init_problem` 时把以下约定注入题面头部(内容由 `config.json` 的 `statement_header` 渲染):

- 评测一律使用**标准 stdin/stdout**,禁止文件 IO;原题面中「见选手目录下的 `x.in`」等说法一律忽略。
- 列出可用语言及其**运行时版本**,以及 C++ 的**固定编译选项**。
- 列出 CPU / wall 时限与内存上限。
- 说明公开样例位置(工作区内的 `sample/`)。

### 编译选项

C++ 统一使用 `-O2 -std=c++14 -static`(与官方评测环境一致)。若某题确实需要其他标准,
可在 `problems.json` 中用 `compiler_options` 按题覆盖,无需改代码。

### 本地放宽与 Judge0 语义差异

| 项 | 说明 |
|----|------|
| 时限 | 本地评测机通常慢于官方评测机,可在 `problems.json` 放宽 `time_limit_cpu` / `time_limit_wall`;官方原始值可另存字段供报告对照 |
| 内存 | Judge0 沙箱有 `max_memory_limit` 硬上限(默认部署下为 512000 KB),题库中的 `memory_limit_kb` 不得超过它 |
| 比对方式 | Judge0 为 **token 级**比对,容忍行尾空白与末尾换行 |
| MLE 归类 | Judge0 1.13.1 **无独立 MLE 状态**:超内存表现为 SIGKILL(`exit_code=137`)或内存逼近上限,服务端结合二者归类为 `MLE` |
| CE 延迟 | `compile_output` 可能延迟返回,服务端按 token 轮询 |
| 编码 | 提交与查询统一使用 `base64_encoded=true`(编译输出可能含非 UTF-8) |

## 实验模式

| 模式 | 语义 | `run_test` | 每题提交上限 |
|------|------|-----------|--------------|
| `iterative` | 联网做题环境:可用评测机对公开样例判题,据此迭代 | 允许(第 10 次起软提醒) | 3 |
| `one-shot` | 竞赛环境:评测机只运行最终提交 | **禁止**(硬拒绝) | 1 |

两种模式下模型都可以在本地工作区自行编译、运行与构造边界数据;区别只在于**是否允许调用评测机自测**。
详细的被测模型提示词见 `docs/agent-prompt.md`。

## 统计与取数

每次工具调用都会追加写入 `runs/<run_id>.jsonl`(每行一条 JSON),关键字段:

`ts`、`run_id`、`model_name`、`mode`、`tool`、`problem_id`、`args_digest`、`verdict`、`score`、
`submit_count`、`test_summary`、`elapsed_seconds`,以及按工具附加的 `sample_no`、`run_test_count`、
`pending`、`submission_id`、`machine_failure`、`ie_points`、`first_error_type` 等。

一键生成报告:

```powershell
.\.venv\Scripts\python.exe analyze_runs.py                 # 默认聚合 mcp-server/runs/
.\.venv\Scripts\python.exe analyze_runs.py --runs <dir>    # 指定目录
```

输出 8 段:`[1]` 各题提交分布 · `[2]` 模型×模式的题级最终结果 · `[3]` `final_status` 分布 ·
`[4]` `first_error_type` 分布 · `[5]` 单题 `run_test` 次数分布 · `[6]` 模型×模式汇总 ·
`[7]` run 明细 · `[8]` 每题完成用时(首次 init → 最终提交)。

> 口径说明:通过率与分数按「题级最终结果」= 该 `(run, 题)` 最后一次 `submit_code` 计算;
> AC 判定会结合 `score` 与失败摘要校准,不单纯信任记录中的 `verdict` 字段。

## 如何新增或调整一道题

1. **题面**:把 Markdown 放到 `statement/<id>.md`。
2. **公开样例**:打包为 `statement/<id>.zip`,内含 `<id>1.in` / `<id>1.ans` …(可带子目录,服务端会展平)。
3. **隐藏数据**:放到 `<data_dir>`(默认仓库内的 `data/`)下的 `<id>/` 目录,命名 `<id>1.in` / `<id>1.ans` …,
   编号从 1 连续到 `test_point_count`。
4. **题库元数据**:在 `problems.json` 增加一个键(键名即 `problem_id`,需与上面文件名前缀一致),
   字段含义见 `problems.example.json` 的 `_readme`。
5. **自检**:
   - 启动服务,确认无「样例缓存初始化失败」告警;
   - `list_problems()` 能看到新题;
   - `init_problem("<id>")` 后工作区有 `题面.md`(含注入头部)与 `sample/`;
   - 用一份正确解法 `submit_code` 应为满分,用一份错误解法应得到预期失败类型。

> 调整时限/内存只需改 `problems.json` 对应字段;调整语言或编译选项改 `config.json` 的 `languages`
> (按题覆盖用 `problems.json` 的 `compiler_options`)。

## 常见问题

- **客户端里看不到工具 / 启动即报错**:确认 `command` 指向装了依赖的 venv 解释器(系统 Python
  可能缺少 `fastmcp`);查看服务端 stderr 有无 `[fatal]` 或 `[warn]` 输出。
- **`[fatal] 未找到题库文件`**:从 `problems.example.json` 复制出 `problems.json`,或用
  `JUDGE0_PROBLEMS_FILE` 指定路径。
- **`init_problem` 报「未配置 AGENT_WORKSPACE」**:在 MCP 配置的 `env` 中设置该变量。
- **样例缓存初始化失败**:检查 `statement/` 下的 `.zip` 是否完整(坏包会在启动时告警)。
- **所有提交返回 Internal Error(13)**:Judge0 沙箱与运行环境不兼容(WSL2 + cgroup v2 常见),
  详见 `judge0-v1.13.1/README.md` 的处理办法。
- **`localhost:2358` 突然不可达(WSL2 端口转发失效)**:服务端会自动尝试 WSL 的真实 IP
  (由 `judge0.wsl_fallback` 控制),日志会打印切换后的地址。
- **清空某次实验记录**:删除 `runs/<run_id>.jsonl` 即可。

## 许可证

本仓库以 **GNU General Public License v3.0(GPL-3.0)** 发布,完整条款见 [LICENSE](LICENSE)。

之所以采用 GPL-3.0:本仓库的 `judge0-v1.13.1/docker-compose.yml` 与
`judge0-v1.13.1/judge0.conf.example` 改编自 [Judge0](https://github.com/judge0/judge0) 的同名文件
(Judge0 以 GPL-3.0 发布,Copyright (C) 2016-2020 Herman Zvonimir Došilović)。GPL-3.0 要求分发
修改版本时标注修改并整体以同一许可授权,故本仓库整体适用 GPL-3.0。

### 第三方组件

| 组件 | 许可 | 在本仓库中的形态 |
|------|------|------------------|
| [Judge0](https://github.com/judge0/judge0) | GPL-3.0 | 以 Docker 镜像方式运行(源码不在本仓库);`docker-compose.yml`、`judge0.conf.example` 为其文件的修改版(修改声明见文件头部与 `judge0-v1.13.1/README.md`) |
| [FastMCP](https://github.com/jlowin/fastmcp) | Apache-2.0 | Python 依赖(见 `requirements.txt`) |
| [HTTPX](https://github.com/encode/httpx) | BSD-3-Clause | Python 依赖(见 `requirements.txt`) |
| 题面 / 样例 / 测试数据 | 归原始出题方所有 | **不包含**在本仓库中(由 `.gitignore` 排除,属本地材料) |

### 免责

本程序不提供任何担保,详见 GPL-3.0 第 15 条(Disclaimer of Warranty)与第 16 条
(Limitation of Liability)。