Skip to main content
Glama
README.md
# ocr-mcp-server

给 [OpenCodeReview](https://github.com/alibaba/open-code-review) 补上**确定性分析**这条腿。

OpenCodeReview 是一个 LLM 驱动的代码审查 CLI。它的架构是「确定性管线 + LLM Agent」,
但审查结论本身完全来自 LLM——模型没注意到的缺陷,它就漏掉了。项目 README 自己承认
这是有意的取舍:

> Recall is explicitly lower than that of general-purpose agents — a deliberate
> trade-off favoring precision over noise.

本服务通过 OpenCodeReview 官方支持的 **MCP 扩展点**暴露 Semgrep,让审查 agent 在推理
之外多一条「规则匹配」的路径。规则匹配不是猜测——命中就是命中。

---

## 核心设计:只报 diff 里的行

这是本项目最有价值的部分,也是实测逼出来的。

OpenCodeReview **一次只审一个文件的 diff**,且它的 prompt 明确禁止评论 diff 之外的
内容(见其 `tools.md`:*「收集该上下文时发现的任何问题按设计都会被忽略」*)。

一个报「整个文件」的扫描器因此是有害的:它把大量 agent 必须丢弃的命中塞进上下文,
而漏过去的那部分会变成**对一个 PR 从未改动过的代码的评论**。所以本服务的默认工具是
`semgrep_scan_diff`,它先解析 `git diff --unified=0` 取出新侧改动行区间,扫完再按区间过滤:

```
base..head  ──►  git diff --unified=0  ──►  {file: [(start, end), ...]}
                                              │
              semgrep --json  ──► findings ──►│──► 只保留落在区间内的
                                              ▼
                                        可被 agent 直接采用的结果
```

被过滤掉的数量会一并返回(`... N match(es) outside the changed lines were suppressed.`),
这样「扫描干净」和「扫描被截断」不会被混为一谈。

## 实测:这个方法在 AACR-Bench 上的边界

OpenCodeReview 的官方基准 [AACR-Bench](https://github.com/alibaba/aacr-bench) 有 196 个
真实 PR、1505 条由 80+ 资深工程师交叉验证的标注。我用它的前 5 个样本实测了确定性分析的
覆盖面:

| 仓库 | 参考标注 | semgrep 命中 | 命中位置 |
|---|---|---|---|
| vllm-project/vllm | 1 | **0** | — |
| lvgl/lvgl | 5 | 13 | 12 条在 `.github/workflows`,与标注无关 |
| FreeCAD/FreeCAD | 3 | 6 | 全在构建脚本 |
| elastic/elasticsearch | 5 | **0** | — |
| astral-sh/uv | 6 | 未测(仓库未克隆) | — |

**参考标注所在的文件,semgrep 一条都没报。**

原因不是工具失效——同一套代码在一个受控的两提交仓库上,把 SQL 注入、`shell=True`、
MD5 存密码三个缺陷全部报出(见下方冒烟测试输出)。原因是**分布不重叠**:

- **AACR-Bench 的标注是人工 reviewer 级的语义评论**:方法命名误导、注释与实现不符、
  缓存失效、设计冗余。
- **Semgrep 是模式匹配器**:它能找的是语法与数据流层面的已知缺陷模式。

因此在这个基准上,直接接入确定性分析**不会提升 Recall,反而会显著拉低 Precision**。
这个负结果写在这里,是因为它划定了方法的适用边界——把模式匹配器接到语义级评审基准上
是个错配,而这件事在动手之前并不显然。

> **未完成**:在模式匹配能赢的场景(例如「安全修复提交的父提交」这类缺陷检出任务)
> 上的量化结果尚未测量。本仓库不声称任何 Recall 改进数字。

## 工具

| 工具 | 用途 |
|---|---|
| `semgrep_scan_diff` | **首选。** 只扫 `base..head` 改动的行 |
| `semgrep_scan` | 扫整个文件,用于 diff 之外的自查 |
| `semgrep_status` | 报告分析器是否可用、版本、规则集与限额 |

返回格式刻意紧凑——每条命中一行,外加可直接用作评论锚点的源码片段:

```
app.py
  L9       ERROR   sqlalchemy.security.sqlalchemy-execute-raw-query  Avoiding SQL string concatenation...  [CWE-89]
          | return conn.execute("SELECT * FROM users WHERE name = '" + name + "'").fetchone()
  L17      ERROR   security.audit.subprocess-shell-true  Found 'subprocess' function 'run' with 'shell=True'.  [CWE-78]
          | return subprocess.run(f"echo {user_input}", shell=True)
```

## 实现中发现的坑

### 1. `--config=auto` 与 `--metrics=off` 互斥

Semgrep 拒绝在关闭遥测时解析 `auto` 规则集:

```
Cannot create auto config when metrics are off.
Please allow metrics or run with a specific config.
```

审查沙箱里不该向外发遥测,所以默认钉死 `p/default`。副作用是好的:**钉死的规则集让
基准可复现**,而整个项目的意义就在于测出可比较的差值。

### 2. Semgrep 的 `extra.lines` 字段不可信

实测 semgrep 1.177.0:对同一文件里两条毫不相关的命中(`shell=True` 与 MD5 存密码),
`extra.lines` 都返回同一个字符串 `"requires login"`——而这个字符串在被扫描的文件里
根本不存在。

所以源码片段不取 Semgrep 的字段,改为**按报出的行号自己读文件**。多一次文件读取,
换来不依赖一个未文档化、跨版本可能变化的行为。

## 安装

```bash
uv tool install semgrep
git clone <this repo> && cd ocr-mcp-server
uv sync
```

## 接入 OpenCodeReview

```bash
ocr config set mcp_servers.semgrep.command uvx
ocr config set mcp_servers.semgrep.args '["--from", "/abs/path/to/ocr-mcp-server", "ocr-mcp-server"]'
ocr config set mcp_servers.semgrep.tools '["semgrep_scan_diff", "semgrep_scan", "semgrep_status"]'
```

工具名刻意带 `semgrep_` 前缀:OpenCodeReview 的内置工具与 MCP 工具**共享同一命名空间**,
重名会被静默跳过(`file_read`、`code_search` 等已被占用),不报错。

## 配置

| 环境变量 | 默认值 | 说明 |
|---|---|---|
| `OCR_MCP_SEMGREP_BIN` | `semgrep` | 分析器可执行文件 |
| `OCR_MCP_SEMGREP_CONFIG` | `p/default` | 规则集。可用 `p/security-audit`、`p/python` 或本地规则文件 |
| `OCR_MCP_TIMEOUT` | `120` | 单次扫描超时(秒) |
| `OCR_MCP_MAX_FINDINGS` | `50` | 单次返回的命中上限 |

## 测试

```bash
uv run pytest -q                      # 56 个单元测试
uv run python scripts/smoke_test.py   # 端到端:经 MCP 协议真实调用
```

单元测试用桩替换子进程,覆盖 Semgrep 缺失 / 超时 / 输出损坏 / 正常返回四种情况,
并验证路径穿越(`../`、绝对路径、空字节)被拒绝。`test_diff.py` 里的解析测试跑在
**真实的临时 git 仓库**上,而不是手写的 diff 文本。

冒烟测试走真实 stdio 传输启动服务,并在一个真实的两提交仓库上调用工具——它抓到过
单元测试抓不到的 bug:`--config=auto` 与 `--metrics=off` 的冲突只在真实子进程里暴露。

## 技术栈

Python · MCP (Model Context Protocol) · Semgrep · uv · pytest