yearning2-mcp
by guanjwen
README.md
# yearning2-mcp
把 **Yearning 2.x**(SQL 审核平台)接入 MCP,让 Claude / Cursor / WorkBuddy 这类 AI 客户端
能直接查平台数据、**提工单**、**执行只读查询**。
- **零第三方依赖** —— 只用 Python 标准库。`pip install` 不会拖进 requests / pydantic 那一套
- **能力分三层,边界在服务端硬拦** —— 只读 / 提交 / 拦截,不靠提示词约束、也没有 `--force` 后门
- **不碰审批** —— 审批类接口一律拒绝。工单只提交,执行与否由人决定
- **提到线上之前先过闸门** —— 数据源授权、审核人合法、SQL 类型自洽、两步确认,四道都过才发请求
- **适配 2.x** —— 登录走根路径 `/ldap` · `/login`,业务接口在 `/api/v2/*`
---
## ⚠️ 只适用于 Yearning 2.x
如果你用的是 **Yearning 3.x**,本项目不适用 —— 3.x 的查询走 WebSocket + msgpack,
登录接口与静态资源打包方式都不一样,2.x 的接口形态在这上面跑不通。
怎么判断自己用的是哪个版本:打开 Yearning 首页看静态资源路径。形如
`/front/assets/js/chunk-*.js`(webpack 打包)的是 **2.x**;`app.<hash>.js` 那种打包风格的是 3.x。
---
## 快速开始
### 1. 安装
```bash
pip install yearning2-mcp # 或
uvx yearning2-mcp # 不安装,直接跑
```
### 2. 配置凭据
**推荐**:写一份配置文件,别把密码塞进客户端的配置里(那通常是明文、还会被同步到别处)。
```bash
mkdir -p ~/.yearning
cat > ~/.yearning/config.json <<'JSON'
{
"endpoint": "http://yearning.example.com:8000",
"username": "your-account",
"password": "your-password",
"login_mode": "ldap"
}
JSON
```
### 3. 接进 MCP 客户端
Claude Desktop 的 `claude_desktop_config.json`、Cursor 的 `mcp.json`、WorkBuddy 的
`~/.workbuddy/mcp.json` 都是同一个结构:
```json
{
"mcpServers": {
"yearning2": {
"command": "yearning2-mcp"
}
}
}
```
用 `uvx` 的话把 `command` 换成 `"uvx"`、`args` 加 `["yearning2-mcp"]`。
不想写配置文件,也可以用环境变量(**优先级高于配置文件**):
```json
{
"mcpServers": {
"yearning2": {
"command": "yearning2-mcp",
"env": {
"YEARNING_ENDPOINT": "http://yearning.example.com:8000",
"YEARNING_USERNAME": "your-account",
"YEARNING_PASSWORD": "your-password"
}
}
}
}
```
### 4. 先自检,再信任
```bash
yearning2-mcp --check-config # 看解析到的配置对不对(不会打印密码)
yearning2-mcp --selftest # 连通性 / 登录 / 权限 / 拦截规则,一次跑完
```
两个都过了,再回到 MCP 客户端里把这个 server **信任 / 启用**。多数客户端需要重启一次才加载新 server。
---
## 工具清单
14 个工具,按用途分三组。
### 只读(8 个,只发 GET)
| 工具 | 作用 |
|---|---|
| `yearning_status` | 连通性、登录方式、账号、角色、真实姓名、**各环境实测可提工单的数据源数量**。**排查问题先调这个** |
| `yearning_allowed_sources` | 某环境某类型下我实际可用的数据源清单 + 可选审核人。**提工单前必调** |
| `yearning_system_stats` | 用户数 / 工单总数 / 查询总数 / 数据源数 |
| `yearning_list_environments` | 环境(IDC)列表 |
| `yearning_datasource_usage` | 各数据源查询次数分布(前 30 个) |
| `yearning_board` | 首页公告板配置 |
| `yearning_orders` | 工单执行明细分页,可按环境 / 数据源 / 时间过滤 |
| `yearning_api_get` | 受控的只读 GET 透传,用于读上面没覆盖的只读接口 |
### 查询(3 个,走「先提工单、批准后查询」的官方流程)
| 工具 | 作用 | 需要确认 |
|---|---|---|
| `yearning_submit_query_order` | 申请某个环境的查询权限(进审核流程) | ✅ 两步 |
| `yearning_query_status` | 看查询工单状态:`1` 已生效 / `2` 待审核 / `3` 已结束 | — |
| `yearning_run_query` | 执行只读 SQL(`SELECT` / `SHOW` / `DESC` / `EXPLAIN`) | — |
### 工单(3 个)
| 工具 | 作用 | 需要确认 |
|---|---|---|
| `yearning_submit_order` | 提交 DDL / DML 工单(进审核流程,**不会立即执行**) | ✅ 两步 |
| `yearning_my_orders` | 我提交过的工单列表,可按状态 / 关键字 / 日期区间筛 | — |
| `yearning_revoke` | 撤销我自己的待审工单 / 结束我自己的查询权限 | ✅ 两步 |
接好之后不需要记工具名,直接说人话就行:
| 你说 | 会调 |
|---|---|
| 「Yearning 能连上吗」/「我有什么权限」 | `yearning_status` |
| 「我在 prod 能提哪些库的工单」 | `yearning_allowed_sources` |
| 「Yearning 上有多少工单和用户」 | `yearning_system_stats` |
| 「列一下 Yearning 有哪些环境」 | `yearning_list_environments` |
| 「哪个库被查得最多」 | `yearning_datasource_usage` |
| 「查工单,第 2 页每页 20 条」 | `yearning_orders` |
| 「把 user 表加个索引,提个工单」 | `yearning_submit_order`(先给预览,你点头才提交) |
| 「帮我查一下 xxx 库的订单表有多少行」 | `yearning_query_status` → `yearning_run_query` |
| 「我刚提的那个工单批了吗」 | `yearning_my_orders` |
| 「我今天的工单」 | `yearning_my_orders`(带 `since` / `until`) |
| 「我有哪些执行失败的工单」 | `yearning_my_orders`(`status="执行失败"`) |
| 「撤了刚才那个工单」 | `yearning_revoke` |
---
## 安全模型
拦截与闸门写在 `src/yearning2_mcp/safety.py` 与 `writes.py`,是**执行层**而不是提示词。
调用方(包括模型本身)绕不过去,除非改这两个文件 —— 属刻意行为,改完请同步改本文档与 `SECURITY.md`。
### 三层能力
| 层 | 入口 | 说明 |
|---|---|---|
| **只读 READ** | `yearning_api_get` 及 7 个只读工具 | 只发 GET。写操作型 GET(`fetch/undo`、`query/results`、`query/refer`)在这里被拒,防止从只读通道溜进写意图 |
| **提交 WRITE** | 6 个专用工具 | **不接受任意路径**:方法+路径写死在 `WRITE_ACTIONS` 白名单里,调用方只能提供业务参数 |
| **拦截 BLOCKED** | 无入口 | 审批类(`/api/v2/audit/**`)、管理类(`/api/v2/manage/**`,只放行只读的 `manage/group`)、泄露密码哈希的 `fetch/perform`、以及会绕过审核直接执行 SQL 的 `fetch/test` 等 |
### 四道闸门(提工单 / 执行查询时)
1. **数据源授权** —— 用 `GET /api/v2/fetch/source?idc=&tp=` 取真实授权清单,数据源必须命中。
命中的是**清单里的原始字符串**(线上存在 `" db-readonly"` 这种带前导空格的脏记录,
比对时 strip、发出去时原样)。
2. **审核人合法** —— `assigned` 必须来自服务端给的可选审核人清单。留空且清单非空时,直接把可选值列出来。
3. **SQL 与工单类型自洽** —— `ddl` 工单必须全是 DDL 语句,`dml` 工单必须全是 DML。
`SELECT` 提工单会被拒(该走查询工单),DDL/DML 混写按 DML 归类。
4. **两步确认** —— 提工单、撤单、结束查询权限这四类动作**默认只返回预览**,
要带 `confirm=true` 再调一次才真发请求。`yearning_run_query` 不在其中:
它是只读且可重复执行的,每次都要求确认会让探索式查询没法用 —— 它的保护来自
「SQL 只读性校验」+「数据源必须属于已批准查询工单的环境」这两条。
### 执行查询额外补的洞
`POST /api/v2/query/results` 只看「有没有生效中的查询工单」,**不校验数据源属于哪个环境**。
批准了 A 环境照样能查 B 环境的库。`yearning_run_query` 因此先读查询工单的环境,
再要求数据源落在该环境的 `query` 授权清单里。
### 只读 SQL 的三条判定规则
`writes.ensure_read_only()` 不靠大黑词表(那会把 `SELECT event, set_at FROM logs` 误杀),
而是三条精确规则:
1. 每条语句的**首词**必须是只读起始词(`SELECT` / `WITH` / `SHOW` / `DESC` / `DESCRIBE` / `EXPLAIN` / `TABLE` / `VALUES` / `HELP`)
2. `WITH` / `EXPLAIN` 这类复合前缀,整条语句里不得再出现写动词
3. 不得出现文件读写词(`OUTFILE` / `DUMPFILE` / `LOAD_FILE`)、危险函数(`SLEEP` / `BENCHMARK` / `GET_LOCK`)
与锁语义(`FOR UPDATE` / `FOR SHARE` / `LOCK IN`)
判定前先做单趟状态机去噪声:注释、字符串字面量、反引号标识符里的内容都不参与判断 ——
否则 `SELECT '--' AS x` 会被当成注释截断。语句数上限 50 条。
### 边界强制在哪里
三层边界都在 `safety.py` / `writes.py` 的**执行层**强制,不是提示词约束:
拒绝判定发生在任何 HTTP 请求发出**之前**;写动作只能从 `WRITE_ACTIONS` 白名单出去,
方法 + 路径写死,调用方只能填业务参数、无法自定义目标路径。
---
## 配置参考
### 查找顺序(命中即停)
1. `--config` 指定的文件
2. 环境变量 `YEARNING_CONFIG` 指向的文件(两者都是显式指定:文件不存在会直接报错,不会静默回落)
3. `~/.yearning/config.json`
4. `./yearning.config.json`
### 环境变量(始终优先于文件)
| 变量 | 说明 |
|---|---|
| `YEARNING_ENDPOINT` | 地址,如 `http://yearning.example.com:8000`。缺协议时自动补 `http://` |
| `YEARNING_USERNAME` | 登录账号 |
| `YEARNING_PASSWORD` | 登录密码 |
| `YEARNING_LOGIN_MODE` | `ldap`(默认)或 `local` —— 决定先试 `/ldap` 还是 `/login` |
| `YEARNING_CONFIG` | 配置文件路径 |
| `YEARNING_SESSION` | 会话缓存路径,默认 `~/.yearning/session.json` |
| `YEARNING_MCP_LOG` | 日志路径,默认 `~/.yearning/mcp.log`;设 `off` 关闭 |
### 运行期文件
| 路径 | 内容 |
|---|---|
| `~/.yearning/session.json` | JWT + 真实姓名等会话缓存(POSIX 下权限 600)。按 endpoint 隔离,换实例不复用 |
| `~/.yearning/mcp.log` | 运行日志,排障看这个。超过 2MB 自动轮转 |
---
## 命令行
```bash
yearning2-mcp # 启动 stdio MCP 服务(MCP 客户端默认这么调)
yearning2-mcp --check-config # 打印解析到的配置,不回显密码
yearning2-mcp --selftest # 对真实实例自检:TCP / 登录 / 身份 / 权限 / 只读接口 / 拦截规则
yearning2-mcp --selftest --report report.txt
yearning2-mcp --print-tools # 以 JSON 打印工具清单
yearning2-mcp --version
```
`--selftest` 的退出码:`0` 全部通过,`1` 连接或登录失败,`2` 配置有问题。
---
## 从源码跑
本项目用 `src/` 布局,所以要先装一次才能以模块方式启动:
```bash
git clone https://github.com/guanjwen/yearning2-mcp.git
cd yearning2-mcp
pip install -e . # 或 pip install .
yearning2-mcp --print-tools # 确认工具清单生成正常
yearning2-mcp --selftest # 对真实实例自检
```
---
## 兼容性
| 项 | 情况 |
|---|---|
| Yearning | 2.3.x(路由表取自上游 `cookieY/Yearning` tag `2.3.5` 的 `src/router/router.go`,并在真实部署上逐条实测对齐) |
| 认证 | LDAP 已实测;本地账号走 `POST /login`,同一套逻辑 |
| 只读能力 | 全部接口已在真实部署上实测 |
| 提工单 | **已实测到预览与全部闸门**(含 4 条负向用例:越权数据源、非法审核人、类型不匹配、缺 confirm)。真实提交会真的产生线上工单,没有在别人的生产环境上做;提交与预览复用同一份请求体构造 |
| 执行查询 | 已实测闸门与拒绝路径;查询通道本身需要一条已批准的查询工单才能走通 |
| 审批 / 管理 | **不提供,也不打算提供** |
| Python | 3.9 ~ 3.14(3.13 / 3.14 已实测;CI 覆盖全矩阵) |
| 平台 | Windows / Linux / macOS(纯标准库,无平台相关代码) |
| 第三方依赖 | 无 |
---
## 常见问题
| 现象 | 原因 / 处理 |
|---|---|
| 工具报「无法连接」 | 内网地址,先连上 VPN。客户端会自动绕开系统代理(`HTTP_PROXY` 在这里是坑) |
| 工具报「登录失败」 | 账号密码错,或 LDAP 不可达。用 `--check-config` 确认凭据来源 |
| 返回字符串 `Illegal` | 子路径写错了。这是 Yearning 对错误子路径的兜底响应,**不是报错** |
| 报「环境没有添加流程」 | 该环境没配审批流程,`fetch/source` 直接返空 —— 提工单、查询都做不了,需要管理员配流程 |
| `yearning_allowed_sources` 返回空清单 | 你的账号在该环境该类型下没有授权。`yearning_status` 里的数量也会是 0 |
| `yearning_run_query` 报「没有生效中的查询工单」 | 先去 `yearning_submit_query_order` 申请,等管理员批准(`yearning_query_status` 看到 `1`) |
| 查询状态第一次显示已生效、再查变成已结束 | 上游行为:`query/status` 每次调用都会检查窗口是否过期,过期就顺手把状态改成 `3`,但返回体给的是**改动前的旧值**。不是工具抽风 |
| 提工单返回「预览」而不是「已提交」 | 这是设计如此。确认无误后带 `confirm=true` 再调一次 |
| MCP 客户端里看不到工具 | 确认配置是合法 JSON;多数客户端需要重启才加载新 server |
| 改了服务端代码不生效 | MCP server 是长驻子进程,不会热重载。改完要重启 MCP 客户端 |
---
## 平台侧的安全问题(非本项目缺陷)
在真实环境中实测时发现的三个问题,**都属于 Yearning 服务端**,本项目只能在自己这一侧补闸门。
注意:**绕过本 MCP 直接调这些接口,一样能命中。**
1. **`GET /api/v2/fetch/perform` 以普通账号身份即可调用,响应体里带着执行人的 PBKDF2 密码哈希。**
哈希不可逆,但 12 万轮 PBKDF2 对弱口令可以离线爆破,属未授权敏感信息泄露。
建议在 `FetchPerformList` 的返回结构里去掉 `password` 字段,或把该接口收进相应权限组。
本项目已主动把该接口拦掉,不让它经 MCP 进入对话记录。
2. **提工单接口(`SQLReferToOrder` / `ReferQueryOrder`)在服务端一行权限校验都没有。**
`ddl_source` / `dml_source` 的过滤只发生在 `GET /api/v2/fetch/source` 里 ——
也就是前端下拉框。直接 `POST /api/v2/common/order` 可以给任意数据源提工单。
**本项目补上了这道闸门**(闸门 1),但服务端该补的还是要补。
3. **`/api/v2/query/results` 只看「有没有生效中的查询工单」,不校验数据源属于哪个环境。**
批准了 A 环境,就能拿它查 B 环境的库。**本项目在 `yearning_run_query` 里补了环境归属校验**,
同样,服务端该补的还是要补。
---
## 贡献
见 [CONTRIBUTING.md](CONTRIBUTING.md)。核心约定两条:**保持零依赖**、
**写操作必须走白名单 + 闸门,且不引入任何审批类能力**。
## 许可
[MIT](LICENSE)。
本项目是独立实现的 HTTP 客户端,**不包含** Yearning 的任何代码。
上游 [cookieY/Yearning](https://github.com/cookieY/Yearning) 采用 AGPL-3.0,两者互不影响。
## 致谢
- [cookieY/Yearning](https://github.com/cookieY/Yearning) —— 接口形态与路由规则来自其 2.3.5 版源码
- [Model Context Protocol](https://modelcontextprotocol.io/) —— 协议规范
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues