Metadata-Version: 2.4
Name: mcp-yearning
Version: 0.3.0
Summary: MCP Server for Yearning SQL audit/review platform (REST API)
Project-URL: Homepage, https://github.com/zhouweico/mcp-yearning
Project-URL: Repository, https://github.com/zhouweico/mcp-yearning
Author: zhouweico
License-Expression: MIT
License-File: LICENSE
Keywords: database,mcp,model-context-protocol,sql-audit,yearning
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.10
Requires-Dist: httpx2>=1.0.0
Requires-Dist: mcp<3.0.0,>=2.0.0
Requires-Dist: msgpack>=1.0.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: starlette>=0.37.0
Requires-Dist: uvicorn>=0.27.0
Requires-Dist: websockets>=12.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: respx>=0.20.0; extra == 'dev'
Requires-Dist: ruff>=0.4.0; extra == 'dev'
Description-Content-Type: text/markdown

# mcp-yearning

Yearning MCP Server - 让 AI 助手能够查询和管理 [Yearning](https://github.com/cookieY/Yearning) SQL 审核平台的工单与查询：浏览数据源与表结构、提交并审核 SQL 工单、执行只读查询等。

基于 Yearning REST / WebSocket API（`/api/v2`，JWT Bearer 认证）；列表类与查询执行走 WebSocket，其余为 REST。

## 特性

- **多协议传输**：`stdio`（默认）、`sse`、`streamable-http`，一套代码适配本地与远程场景
- **接口认证**：HTTP 传输支持 Bearer Token 保护，未授权请求返回 `401`
- **原生账号密码登录**：用户名/密码登录换取 JWT，`401` 时自动重新登录并重放请求，无需手工维护 Token
- **JWT 自动管理**：登录态缓存并在 8 小时过期前（提前 0.5h）主动续登，全程无感
- **REST + WebSocket 统一封装**：列表类接口（我的工单、审核列表、评论）与查询执行走 WebSocket，已封装为「连接→发一帧→收一帧→关闭」的伪 REST；查询执行额外用 msgpack 编解码
- **信封解析**：Yearning 恒返回 HTTP 200 + 信封 `{payload, code:1200, text}`；非 1200 视为业务错误
- **Yearning 原生概念**：直接以 `数据源 / 库 / 表 / SQL 工单 / 审核流程` 组织操作，读写一体
- **危险操作防护**：审核工单（agree）等高危操作带 `destructiveHint` 注解，撤回工单带 `idempotentHint` 注解，均需明确动作参数
- **破坏性操作 MRTR 确认**：同意工单（agree）等高危操作通过 MCP 2.0 Elicitation 机制弹出确认表单，需用户明确同意后才执行
- **MCP Resources**：以 `yearning://` URI 暴露用户信息、数据源列表等只读元数据，客户端可直接读取
- **Stateless HTTP**：支持无状态 HTTP 模式，每次请求独立处理、无会话状态，适合 Serverless / 多副本部署
- **灵活部署**：`uvx` 免安装运行、Docker 构建即用

## 前置准备

准备一个可访问的 Yearning 实例。你需要准备：

- Yearning 地址（如 `http://localhost:8000`）
- 登录用户名 / 密码（常规账号或 LDAP 账号）

> 账号在 Yearning 内的角色决定你能看到哪些数据源、能提交/审核哪些工单；MCP Server 本身不做任何鉴权，仅原样转发请求。

## 快速开始

### MCP 客户端（stdio，本地）

以 Claude Code 为例，在项目 `.mcp.json` 或全局 `~/.claude.json` 中添加：

```json
{
  "mcpServers": {
    "yearning": {
      "type": "stdio",
      "command": "uvx",
      "args": ["mcp-yearning"],
      "env": {
        "YEARNING_URL": "http://localhost:8000",
        "YEARNING_USERNAME": "your-user",
        "YEARNING_PASSWORD": "your-password",
        "YEARNING_LOGIN_TYPE": "general",
        "YEARNING_READ_ONLY": "false"
      }
    }
  }
}
```

> Cursor、OpenCode、Claude Desktop 等客户端的配置格式相同，核心均为 `command: uvx` + `args: ["mcp-yearning"]`，按各客户端语法填入 `YEARNING_*` 环境变量即可。

### Docker（公开镜像，免构建）

已发布公开镜像 `ghcr.io/zhouweico/mcp-yearning:latest`，无需本地构建。下面以 Claude Code 为例，说明如何用 `docker` 命令运行并配置 mcp-yearning。

**方式一：stdio（由客户端拉起容器，适合本地集成）**

在 Claude Code 的 `.mcp.json` 中直接用 `docker` 作为启动命令，客户端会以 stdio 管道与容器内服务通信：

```json
{
  "mcpServers": {
    "yearning": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm", "ghcr.io/zhouweico/mcp-yearning:latest"],
      "env": {
        "YEARNING_URL": "http://your-yearning:8000",
        "YEARNING_USERNAME": "your-user",
        "YEARNING_PASSWORD": "your-password"
      }
    }
  }
}
```

> 必须带 `-i`（保持 stdin 管道），否则容器内的 stdio 服务无法与客户端通信。

**方式二：HTTP + 认证（容器独立运行，客户端远程连接，适合多客户端共享）**

先启动容器：

```bash
docker run -d -p 8080:8080 \
  -e MCP_TRANSPORT=streamable-http \
  -e MCP_AUTH_TOKEN=your-strong-token \
  -e YEARNING_URL=http://your-yearning:8000 \
  -e YEARNING_USERNAME=your-user \
  -e YEARNING_PASSWORD=your-password \
  ghcr.io/zhouweico/mcp-yearning:latest
```

再在 Claude Code 的 `.mcp.json` 中通过 HTTP 连接：

```json
{
  "mcpServers": {
    "yearning": {
      "type": "streamable-http",
      "url": "http://localhost:8080/mcp",
      "headers": {
        "Authorization": "Bearer your-strong-token"
      }
    }
  }
}
```

## 可用工具

### 只读工具（14 个）

按业务域分组排列：元数据 → SQL 工单 → 查询与评论。

| 工具 | 分组 | 说明 | 对应 API |
|------|------|------|----------|
| `yearning_user_info` | 元数据 | 当前用户信息、可查询数据源 | `GET /api/v2/fetch/userinfo` |
| `yearning_list_sources` | 元数据 | 列出有权限的数据源 | `GET /api/v2/fetch/source` |
| `yearning_list_databases` | 元数据 | 数据源下的库列表 | `GET /api/v2/fetch/base` |
| `yearning_list_tables` | 元数据 | 库下的表列表 | `GET /api/v2/fetch/table` |
| `yearning_table_fields` | 元数据 | 表结构（字段 + 索引） | `GET /api/v2/fetch/fields` |
| `yearning_sql_check` | SQL 工单 | 提交前 SQL 审核检测（`order_type`: ddl / dml） | `PUT /api/v2/fetch/test` |
| `yearning_my_orders` | SQL 工单 | 我的工单列表（分页/状态/关键字过滤） | `WS /api/v2/common/list` |
| `yearning_order_detail` | SQL 工单 | 工单详情（SQL 明细 + 完整 SQL） | `GET /api/v2/fetch/detail` + `/fetch/sql` |
| `yearning_order_timeline` | SQL 工单 | 工单审核时间线与流程步骤 | `GET /api/v2/fetch/timeline` + `/fetch/steps` |
| `yearning_rollback_sql` | SQL 工单 | 获取工单回滚 SQL | `GET /api/v2/fetch/roll` |
| `yearning_audit_orders` | SQL 工单 | 待我审核的工单列表 | `WS /api/v2/audit/order/list` |
| `yearning_query_status` | 查询与评论 | 查询审核开关 + 我的查询工单状态 | `GET /api/v2/fetch/is_query` + `/fetch/query_status` |
| `yearning_run_query` | 查询与评论 | 执行只读 SELECT 查询（不修改数据；Yearning 留存查询审计记录） | `WS /api/v2/query/results`（msgpack） |
| `yearning_order_comments` | 查询与评论 | 读取工单评论 | `WS /api/v2/fetch/comment` |

### 写工具（5 个）

按工单生命周期排列：提交 → 撤回 → 审核 → 查询申请 → 评论。

| 工具 | 分组 | 说明 | 对应 API |
|------|------|------|----------|
| `yearning_submit_order` | SQL 工单 | 提交 SQL 工单（DDL/DML） | `POST /api/v2/common/post` |
| `yearning_undo_order` | SQL 工单 | 撤回自己未执行的工单 | `GET /api/v2/fetch/undo` |
| `yearning_audit_order` | SQL 工单 | 审核工单：agree/reject/undo（agree 即批准变更落地，高危） | `POST /api/v2/audit/order/state` |
| `yearning_submit_query_order` | 查询与评论 | 提交数据查询申请 | `POST /api/v2/query/post` |
| `yearning_post_comment` | 查询与评论 | 发表工单评论 | `POST /api/v2/fetch/comment` |

> **未提供的操作**：管理端（admin）能力（用户 / 数据源 / 规则管理）与 AI 辅助（text2sql / advisor）为后续 Phase，未包含在本期；此类操作请通过 Yearning 控制台人工执行。
>
> **只读 / 写的区别**：上表「只读工具」在 `YEARNING_READ_ONLY=true` 下**仍然可用**；「写工具」在该模式下会被**完全排除**——不出现在 `tools/list` 中，Agent 既看不到也无法调用（注册期排除，非运行期拦截）。这样生产环境开启只读后，Agent 只能查询、绝无意外变更工单的风险。
>
> **审核/撤回为危险操作**：`yearning_audit_order`（agree）会批准一条 DDL/DML 变更在数据源执行，`yearning_undo_order` 会撤回工单；调用时务必明确动作参数，避免对话中的误操作直接落到生产。
>
> **MRTR 确认**：`yearning_audit_order`（agree）为破坏性操作，执行前会通过 MCP 2.0 Elicitation 弹出确认表单，需用户明确同意后才执行。若客户端不支持 Elicitation（如 stdio 模式），则降级为直接执行。

## 配置

### 环境变量

**MCP 传输与认证**

| 变量 | 说明 | 默认值 |
|------|------|--------|
| `MCP_TRANSPORT` | 传输协议：`stdio` / `sse` / `streamable-http` | `stdio` |
| `MCP_HOST` | HTTP 传输监听地址（stdio 忽略） | `0.0.0.0` |
| `MCP_PORT` | HTTP 传输监听端口（stdio 忽略） | `8080` |
| `MCP_AUTH_TOKEN` | 设置后启用 Bearer Token 认证，保护 HTTP 接口 | -（不鉴权） |
| `MCP_STATELESS_HTTP` | 启用无状态 HTTP 模式，适合 Serverless 部署（详见下方说明） | `false` |
| `MCP_LOG_LEVEL` | 日志级别：`debug`/`info`/`warning`/`error` | `info` |

**Yearning 连接**

| 变量 | 说明 | 默认值 |
|------|------|--------|
| `YEARNING_URL` | Yearning 地址 | `http://localhost:8000` |
| `YEARNING_USERNAME` | 登录用户名（必填） | - |
| `YEARNING_PASSWORD` | 登录密码（必填） | - |
| `YEARNING_LOGIN_TYPE` | 登录类型：`general` / `ldap` | `general` |
| `YEARNING_TIMEOUT` | 请求超时（秒） | `30` |
| `YEARNING_READ_ONLY` | 只读模式，排除全部写工具（适合生产环境） | `false` |
| `YEARNING_INSECURE` | 跳过 TLS 证书验证，用于自签名证书环境（详见下方说明） | `false` |

> 认证凭证只需用户名/密码：客户端首次请求时自动调用 `POST /api/v2/login`（或 `ldap`）换取 JWT 并缓存；收到 `401` 时先尝试重新登录、失败则重放原请求（最多一次），全程无需人工干预。
>
> 注意区分两类凭证：`MCP_AUTH_TOKEN` 保护本 MCP Server 的 HTTP 接口；`YEARNING_USERNAME` / `YEARNING_PASSWORD` 用于登录 Yearning，两者互不相关。

### Yearning 概念说明

Yearning 的 SQL 审核组织层级为：**数据源（source）> 库（database）> 表（table）> SQL 工单（order）> 审核流程（audit）**。

- 提交一条 SQL 变更需先经 `yearning_sql_check` 检测，再用 `yearning_submit_order` 提单；工单按配置的审核流流转，审核人用 `yearning_audit_order` 放行/驳回。
- 线上查询走 `yearning_run_query`（仅 SELECT），需具备查询权限；部分环境开启查询审核后，查询也需先 `yearning_submit_query_order` 申请。
- 工单状态：0 已驳回 / 1 已同意待执行 / 2 待审核 / 3 已完成 / 4 已终止 / 5 执行中 / 6 已撤回。

### 只读模式

设置 `YEARNING_READ_ONLY=true` 可排除全部写工具，仅允许查询，适合生产环境使用：

```json
{
  "env": {
    "YEARNING_READ_ONLY": "true"
  }
}
```

### TLS 证书验证

本服务基于 httpx2 发起 HTTPS 请求，**默认会验证 TLS 证书**（行为与 httpx 一致）。

- 在使用自签名证书或内部 CA 的环境中，HTTPS 请求会因证书校验失败而报错。此时可设置环境变量 `YEARNING_INSECURE=true` 跳过 TLS 证书验证。
- 该选项适用于开发、测试等使用自签名证书的环境。

```json
{
  "env": {
    "YEARNING_INSECURE": "true"
  }
}
```

> **安全警告**：禁用 TLS 证书验证是不安全的，会使得 HTTPS 连接容易受到中间人攻击。**请勿在生产环境中使用**，生产环境应使用受信任的 CA 签发的有效证书。

## 多协议传输

通过 `MCP_TRANSPORT` 选择传输协议：

- **`stdio`（默认）**：标准输入输出，适合 Claude Code、Cursor 等本地 AI 客户端集成。
- **`sse`**：Server-Sent Events，HTTP 传输，端点 `http://<host>:<port>/sse`。
- **`streamable-http`**：Streamable HTTP，端点 `http://<host>:<port>/mcp`。

以 `streamable-http` 启动示例：

```bash
MCP_TRANSPORT=streamable-http \
MCP_HOST=0.0.0.0 MCP_PORT=8080 \
MCP_AUTH_TOKEN=your-strong-token \
mcp-yearning
```

## 接口认证

设置 `MCP_AUTH_TOKEN` 后，所有 HTTP 请求必须携带正确 Token，否则返回 `401`：

```
Authorization: Bearer <MCP_AUTH_TOKEN>
```

也兼容 `X-Auth-Token` / `X-MCP-Token` 请求头。健康检查端点 `GET /health` 免鉴权，返回 `{"status":"ok"}`，用于容器探活。

> `stdio` 传输为本地进程通信，不涉及网络，无需也不会进行 Token 认证。未设置 `MCP_AUTH_TOKEN` 时 HTTP 接口不鉴权，生产环境请务必配置。
>
> 注意区分两类凭证：`MCP_AUTH_TOKEN` 保护本 MCP Server 的 HTTP 接口；`YEARNING_USERNAME` / `YEARNING_PASSWORD` 用于登录 Yearning，两者互不相关。

## MCP Resources

本服务以 MCP 2.0 Resources 暴露只读元数据，客户端可直接通过 URI 读取，无需调用工具：

| Resource URI | 说明 |
|---|---|
| `yearning://user-info` | 当前登录用户信息（含可查询数据源） |
| `yearning://sources` | 数据源列表 |

> Resources 仅暴露只读数据，不涉及任何写操作。

## Stateless HTTP 模式

设置 `MCP_STATELESS_HTTP=true` 可启用无状态 HTTP 模式，每次请求独立处理、不保留会话状态，适合 Serverless 平台（如 AWS Lambda、阿里云函数计算）或多副本无状态部署：

```bash
MCP_TRANSPORT=streamable-http \
MCP_STATELESS_HTTP=true \
MCP_HOST=0.0.0.0 MCP_PORT=8080 \
mcp-yearning
```

> Stateless 模式下不支持流式响应（SSE stream），每个 HTTP 请求独立完成工具调用后返回。适合短时、无状态的工具调用场景。

## 容器化部署

### 本地构建（Docker）

```bash
# 构建镜像
docker build -t mcp-yearning:latest .

# 以 streamable-http 运行并启用认证
docker run -d --name mcp-yearning -p 8080:8080 \
  -e MCP_TRANSPORT=streamable-http \
  -e MCP_AUTH_TOKEN=your-strong-token \
  -e YEARNING_URL=http://your-yearning:8000 \
  -e YEARNING_USERNAME=your-user \
  -e YEARNING_PASSWORD=your-password \
  mcp-yearning:latest
```

### Docker Compose

复制 `.env.example` 为 `.env` 并按需修改，然后：

```bash
cp .env.example .env
docker compose up -d
```

`docker-compose.yml` 已内置 `build`（基于本地 `Dockerfile` 构建并标记为 `mcp-yearning:latest`）和健康检查（探测 `/health`），以非 root 用户运行，适合本地开发部署。

## 使用场景示例

配置好后，你可以这样和 AI 对话（每条示例后括注主要涉及的工具）：

### 数据源与表结构探查

```
连上 Yearning，告诉我我有哪些数据源可用
```
（`yearning_user_info` + `yearning_list_sources`）

```
看看 order_db 这个数据源下有哪些库，再列出 user 表有哪些字段和索引
```
（`yearning_list_databases` → `yearning_list_tables` → `yearning_table_fields`）

### 提交并跟踪 SQL 变更

```
把这条建表语句在 dev 环境提个工单，先帮我做个 SQL 检测看看有没有问题：
CREATE TABLE t_demo (id INT PRIMARY KEY, name VARCHAR(64));
```
（`yearning_sql_check` → `yearning_submit_order`）

```
我刚提的工单到哪一步了？把审核时间线和当前步骤给我
```
（`yearning_my_orders` → `yearning_order_timeline`）

```
这个工单如果执行出错，回滚 SQL 是什么
```
（`yearning_rollback_sql`）

### 审核人视角

```
列出待我审核的工单
```
（`yearning_audit_orders`）

```
工单 ORD-2026-0001 没问题，帮我通过
```
（`yearning_audit_order`，`tp=agree`）

### 线上只读查询

```
在 order_db 的 user 库里查一下最近 7 天注册的账号，前 100 条
```
（`yearning_run_query`）

```
这个环境的查询为什么被拦了？看看查询审核开关和我的查询工单状态
```
（`yearning_query_status`）

### 协作与审计

```
把 ORD-2026-0001 这个工单的评论都拉出来看看
```
（`yearning_order_comments`）

```
在 ORD-2026-0001 工单下留言：已确认索引已存在，可放行
```
（`yearning_post_comment`）

> 审核 / 撤回等危险操作需明确动作参数；`yearning_audit_order(agree)` 会批准变更在生产数据源执行，请确认无误后再调用。管理端（admin）能力与 AI 辅助工具为后续 Phase，未包含在本期。

## 已知限制

以下为当前实现与 Yearning 接口交互中的已知边界，使用前请留意：

- **WebSocket 工具共 4 个**：`yearning_my_orders`、`yearning_audit_orders`、`yearning_order_comments`（JSON）与 `yearning_run_query`（msgpack）。它们依赖将裸 JWT 放入 `Sec-WebSocket-Protocol` 头完成鉴权；若 Yearning 前置了不透传该请求头、或会校验 subprotocol 合法性的反向代理 / 网关，WebSocket 鉴权会失败。直连 Yearning 不受影响。
- **每次调用新建一条 WebSocket 连接，无复用**：列表类接口被高频调用时存在握手开销（TCP + WS + JWT 解析每回重来）。功能无误，但高并发场景延迟偏高。
- **查询审核开启时 `yearning_run_query` 需先审批**：若 Yearning 开启了查询审核（数据源需先有 `status=2` 的已批准查询工单），未审批前执行查询不会返回结果；本服务会识别该情况并明确提示「请先经 `yearning_submit_query_order` 提交查询申请并审批」，而非返回空结果误导。
- **权限 / token 类失败已转译为友好错误**：无对应数据源权限、token 失效、查询审核未批准、或传入参数不合法时，Yearning 会**不回帧直接关闭连接**。本服务的 WebSocket 封装已增加接收超时，并把底层连接中断转成 `YearningApiError`（说明可能原因），不再向调用方抛出底层栈信息。
- **`yearning_run_query` 请求字段与 Yearning 结构体绑定**：查询请求以 msgpack 编码，键名（`type` / `sql` / `schema`）对应 Yearning `QueryDeal.Ref` 的 Go 字段；若 Yearning 改动了该结构体或引入 msgpack tag，需同步更新 `clients/ws.py` 的打包逻辑。
- **`yearning_sql_check` 仅接受 DDL / DML**：Yearning 的检测接口会拒绝 SELECT 等非工单 SQL（返回「请提交DML语句」）。这是平台设计而非缺陷——SELECT 不走工单流程，请直接使用 `yearning_run_query`。
- **登录类型仅支持 `general` / `ldap`**：受 Yearning 平台限制，OIDC 等第三方 SSO 为浏览器跳转流程，无头客户端无法用账号密码完成，当前未实现（详见配置说明）。
- **部分只读接口使用 `GET` + JSON body**：Yearning 的 `/fetch/*` 等接口（对应 `yearning_list_*`、`yearning_order_detail` 等工具）通过 `GET` 请求携带 JSON body 传参（服务端 `c.Bind` 只读 body，不读 query string）。这不符合常见 HTTP 语义，若前置的反向代理 / 网关 / CDN 会丢弃 GET 请求的 body，这些工具会静默返回空结果（信封仍为 `code==1200`）。透传 body 的代理（如默认配置的 Nginx）不受影响，已在真实部署环境实测正常；如遇列表恒为空，请优先排查代理是否吞掉了 GET body。

## 开发

```bash
pip install -e ".[dev]"
pytest          # respx mock 测试，无需真实 Yearning 环境
ruff check src tests
```

### 实现注记

- 列表类接口（我的工单、审核列表、评论）为 WebSocket（`/common/list`、`/audit/order/list`、`/fetch/comment`），客户端封装为「连接→发一帧→收一帧→关闭」的伪 REST
- 查询执行（`/query/results`）走 WebSocket 且用 msgpack 编解码：请求为 `msgpack.packb({"type": 0, "sql": sql, "schema": schema})`，响应含 `results` / `query_time` / `error` / `status` 等
- HTTP 请求统一 `Authorization: Bearer <JWT>`；WebSocket 将 JWT 放入 `Sec-WebSocket-Protocol`（裸 token，无 Bearer 前缀）
- 响应信封恒为 HTTP 200，结构 `{payload, code, text}`；成功 `code==1200`，非 1200 视为业务错误；未知 `tp` 返回裸字符串 `"Illegal"`

## License

MIT
