feat: add README.md

This commit is contained in:
Claude
2026-07-02 12:17:27 +08:00
parent b7150fc830
commit e285dc0f66
+482
View File
@@ -0,0 +1,482 @@
# spark-executor-mcp
`spark-executor-mcp` 是一个基于 **FastAPI + fastapi-mcp** 的 Python MCP 服务集合,主要用于让 LLM Agent 以受控方式操作 Spark-on-YARN:保存集群连接、写入或选择 PySpark 脚本、准备提交、二次确认提交、查询状态、读取日志和终止作业。
项目同时内置一个受沙箱限制的 `files_mcp` 服务,用于在指定目录内执行文件读写、查询、移动、复制等操作。
## 核心能力
### Spark Executor MCP
- 管理 Spark/YARN 连接配置。
- 将 LLM 生成的 PySpark 代码写入服务端文件系统。
-**两步提交** 方式运行 Spark 作业:
1. `prepare_submit_job`:只生成待确认提交记录,不执行 `spark-submit`
2. `confirm_submit_job`:用户二次确认后才真正调用 `spark-submit`
- 查询 Spark/YARN 作业状态、终态结果和聚合日志。
- 通过本地 `job_id` 或 YARN `application_id` 查询同一个作业。
- Kill 正在运行的 YARN application。
- 持久化连接、待提交作业和已提交作业记录。
### Files MCP
-`FILES_MCP_ROOT` 指定的沙箱目录内创建、读取、更新、删除文件。
- 支持目录列举、路径状态查询、glob 搜索、移动和复制。
- 路径会经过沙箱校验,避免访问沙箱外文件。
- 文本内容限制为 UTF-8,单文件读写默认有大小上限。
### 可插拔 MCP 服务
- 根应用启动时会自动发现并挂载内置 MCP 服务。
- 可通过 `MCP_SERVICES` 环境变量选择启用哪些服务。
- 新增 MCP 服务只需要提供一个 FastAPI 子应用和 `SERVICE: McpService` 声明。
## 技术栈
| 类别 | 技术 |
| --- | --- |
| 语言 | Python 3.12+ |
| Web 框架 | FastAPI |
| MCP 暴露 | fastapi-mcp |
| 数据模型 | Pydantic v2 |
| HTTP 客户端 | httpx、httpx-kerberos |
| Spark 集成 | `spark-submit`、YARN REST API、`yarn logs` |
| 日志 | loguru |
| 进程管理 | uvicorn、gunicorn |
| 包管理 | uv |
| 测试 | pytest、pytest-mock |
| 容器 | Docker、Docker Compose |
## 快速开始
### 1. 安装依赖
```bash
uv sync
```
项目依赖在 `pyproject.toml``uv.lock` 中固定,默认 PyPI 镜像配置为清华源。
### 2. 启动服务
开发环境直接运行:
```bash
uv run main.py
```
默认监听:
```text
0.0.0.0:8000
```
健康检查:
```bash
curl http://localhost:8000/health
```
预期返回:
```json
{"status":"ok"}
```
### 3. MCP 端点
服务启动后会挂载内置 MCP 服务:
| 服务 | MCP endpoint | 说明 |
| --- | --- | --- |
| Spark Executor | `http://localhost:8000/spark-executor-mcp` | Spark-on-YARN 作业管理 |
| Files MCP | `http://localhost:8000/files-mcp` | 沙箱文件系统工具 |
MCP 客户端需要先执行 `initialize` 握手,拿到 `mcp-session-id` 后,再调用 `tools/list``tools/call`。不要跳过握手。
## MCP 工具概览
### Spark Executor 工具
| 工具 | 作用 |
| --- | --- |
| `save_connection` | 保存或更新一个 Spark/YARN 连接配置 |
| `list_connections` | 列出所有连接配置 |
| `get_connection` | 按名称读取连接配置 |
| `delete_connection` | 删除连接配置 |
| `write_job_file` | 将 LLM 已生成的 PySpark 代码写入服务端文件 |
| `read_job_file` | 读取已存在的 PySpark 脚本内容 |
| `update_job_file` | 覆盖更新已存在的 PySpark 脚本 |
| `prepare_submit_job` | 创建待确认提交记录,不执行 `spark-submit` |
| `update_pending_job` | 在提交前更新待确认作业参数 |
| `list_pending_jobs` | 列出待提交/已提交/已取消/失败的 pending 记录 |
| `get_pending_job` | 查看单个 pending 记录 |
| `cancel_pending_job` | 取消还未提交的 pending 记录 |
| `confirm_submit_job` | 真正执行 `spark-submit` |
| `get_job_status` | 查询 YARN 当前状态 |
| `get_job_result` | 查询终态结果视图 |
| `get_job_logs` | 拉取 YARN 聚合日志 |
| `kill_job` | Kill YARN application |
### Files MCP 工具
| 工具 | 作用 |
| --- | --- |
| `create_file` | 在沙箱内创建文本文件 |
| `read_file` | 读取沙箱内文本文件 |
| `update_file` | 覆盖更新文本文件 |
| `delete_file` | 删除文件或目录 |
| `list_dir` | 列出目录内容 |
| `stat` | 查看路径类型、大小、mtime、权限等信息 |
| `search` | 使用 glob pattern 搜索路径 |
| `move_file` | 移动或重命名文件/目录 |
| `copy_file` | 复制文件/目录 |
## Spark 作业提交流程
Spark 作业提交是故意设计成两步,不要绕过。
### 1. 保存连接
先保存一个连接配置,后续提交作业时通过 `connection` 名称引用它。
示例参数:
```json
{
"name": "prod-yarn",
"master": "yarn",
"deploy_mode": "cluster",
"yarn_rm_url": "http://yarn-rm.example.com:8088",
"spark_conf": {
"spark.yarn.queue": "default"
}
}
```
调用工具:
```text
save_connection
```
### 2. 写入或选择 PySpark 脚本
如果脚本由 LLM 生成,必须先调用:
```text
write_job_file
```
该工具会把代码写入 `SPARK_EXECUTOR_JOBS_DIR`,并返回真实存在于服务端文件系统中的 `script_path`
如果脚本已经存在,需要确保传入的是容器或服务进程能访问到的真实路径。只存在于 Agent 上下文里的“虚构路径”会被拒绝。
### 3. 准备提交
调用:
```text
prepare_submit_job
```
这个步骤会:
- 读取并快照指定 `Connection``master``deploy_mode``spark_conf``yarn_rm_url`
- 创建 `PendingSubmission` 持久化记录。
- 返回 `pending_id`
- **不会调用 `spark-submit`。**
注意:如果准备后再修改连接配置,已经创建的 pending job 不会被重新定向到新连接。快照就是快照,别让状态到处乱飞。
### 4. 二次确认提交
用户确认后调用:
```text
confirm_submit_job
```
参数只需要 `pending_id`。该步骤才会真正调用 `spark-submit`,成功后会创建 `Job` 记录,并返回本地 `job_id` 和 YARN `application_id`
### 5. 查询、读日志或终止
后续工具的 `job_id` 参数同时支持:
- 本地 12 位十六进制 `job_id`
- YARN `application_id`,例如 `application_17400000001_0001`
可用工具:
```text
get_job_status
get_job_result
get_job_logs
kill_job
```
## 配置项
### 应用配置
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `SPARK_EXECUTOR_DATA_DIR` | `./data` | 连接、pending job、job 记录和日志的基础目录 |
| `SPARK_EXECUTOR_JOBS_DIR` | `<SPARK_EXECUTOR_DATA_DIR>/jobs` | `write_job_file` 写入 PySpark 脚本的位置 |
| `SPARK_EXECUTOR_LOG_DIR` | `<SPARK_EXECUTOR_DATA_DIR>/logs` | loguru 文件日志目录 |
| `YARN_RESOURCE_MANAGER_URL` | 空 | 当 Connection 或 Job 快照中没有 `yarn_rm_url` 时的 fallback |
| `SPARK_EXECUTOR_LOG_LEVEL` | `DEBUG` | stderr 和 info 日志级别;debug 文件始终记录 DEBUG |
| `SPARK_EXECUTOR_SPARK_SUBMIT_BIN` | `spark-submit` | 调用 Spark 客户端的命令名或 wrapper 路径 |
| `SPARK_EXECUTOR_SSL_VERIFY_DEFAULT` | `true` | YARN REST 默认是否校验证书 |
| `SPARK_EXECUTOR_SSL_CA_BUNDLE_DEFAULT` | 空 | YARN REST 默认 CA bundle |
| `MCP_SERVICES` | 未设置 | 服务白名单;例如 `spark_executor,files`。未设置表示启用全部内置服务,空字符串表示不启用任何服务 |
| `FILES_MCP_ROOT` | Docker 中为 `/app/data/files` | Files MCP 能访问的唯一沙箱根目录 |
### Gunicorn 配置
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `GUNICORN_BIND` | `0.0.0.0:8000` | 监听地址 |
| `GUNICORN_WORKERS` | `2` | worker 进程数 |
| `GUNICORN_THREADS` | `1` | 每个 worker 的线程数 |
| `GUNICORN_TIMEOUT` | `120` | 请求超时时间,YARN 日志可能较慢 |
| `GUNICORN_GRACEFUL_TIMEOUT` | `30` | 优雅退出超时 |
| `GUNICORN_KEEPALIVE` | `5` | keepalive 秒数 |
| `GUNICORN_LOGLEVEL` | `info` | gunicorn 日志级别 |
> **重要:生产上建议 `GUNICORN_WORKERS=1`,除非你在前面放了 sticky-session 反向代理。**
>
> MCP session 当前存储在 worker 进程内存里。多 worker 下,`initialize` 可能落到 worker A,下一次 `tools/list` 落到 worker B,然后 B 不认识这个 `mcp-session-id`,就会返回 `Session not found` 或 `Invalid or expired session ID`。这不是玄学,是进程内状态被 round-robin 打散了。
## 数据持久化
默认数据目录是 `./data/`,可通过 `SPARK_EXECUTOR_DATA_DIR` 覆盖。
| 路径 | 说明 |
| --- | --- |
| `data/connections.json` | 保存的 `Connection` 记录 |
| `data/pending_jobs.json` | `PendingSubmission` 记录 |
| `data/jobs.json` | 已提交 `Job` 记录 |
| `data/jobs.json.lock` | `jobs.json` 跨进程写入锁 |
| `data/jobs/` | 默认 PySpark 脚本写入目录 |
| `data/logs/debug/YYYY-MM-DD.log` | DEBUG 日志,按天轮转,保留 30 天,gzip 压缩 |
| `data/logs/info/YYYY-MM-DD.log` | INFO 日志,按天轮转,保留 30 天,gzip 压缩 |
| `data/files/` | Docker 默认 Files MCP 沙箱目录 |
`connections.json``pending_jobs.json``jobs.json` 都使用简单 JSON 文件持久化。当前这是务实方案:足够简单,方便排查,也方便未来迁移到 SQLite。
## Docker 部署
### 1. 准备 Hadoop/YARN 配置
在项目根目录创建或挂载:
```text
./hadoop-conf/
```
至少应包含目标集群匹配的:
```text
core-site.xml
yarn-site.xml
hdfs-site.xml
```
这些文件会以只读方式挂载到容器内:
```text
/etc/hadoop/conf
```
### 2. 准备环境变量
复制模板:
```bash
cp .env.example .env
```
按部署环境修改 `.env`,至少确认:
```text
YARN_RESOURCE_MANAGER_URL=
SPARK_EXECUTOR_DATA_DIR=
SPARK_EXECUTOR_JOBS_DIR=
FILES_MCP_ROOT=
GUNICORN_WORKERS=1
```
`GUNICORN_WORKERS=1` 不是装饰品。除非你真的配置了 sticky-session,否则不要把它调大。
### 3. 启动容器
```bash
docker compose up -d --build
```
查看日志:
```bash
docker compose logs -f mcp-server
```
停止服务:
```bash
docker compose down
```
## 测试
运行完整测试:
```bash
uv run pytest
```
运行单个测试文件:
```bash
uv run pytest tests/unit/test_submit_tool.py -v
```
按名称过滤:
```bash
uv run pytest -v -k cancel_pending
```
测试目录分为:
```text
tests/unit/ # 单元测试
tests/integration/ # FastAPI / MCP 集成测试
```
## 目录结构
```text
main.py # 根 FastAPI applifespan 中发现并挂载 MCP 服务
common/
config.py # 环境变量配置入口
factory.py # FastApiMCP 初始化封装
logging.py # loguru 配置
mcp_service.py # MCP 服务注册、发现、过滤
sql_guard.py # PySpark/SQL 安全策略
spark_executor/
server.py # Spark Executor FastAPI 子应用和 MCP tool routes
service.py # Spark Executor MCP 服务声明
models.py # Pydantic 数据模型
core/ # spark-submit、YARN client、持久化 store、日志解析
tools/ # MCP 工具业务函数和请求模型
files_mcp/
server.py # Files MCP FastAPI 子应用和 MCP tool routes
service.py # Files MCP 服务声明
core/ # 文件系统操作和路径沙箱校验
tools/ # 文件工具业务函数和请求模型
data/ # 默认运行时数据目录,gitignored
tests/ # 单元测试和集成测试
Dockerfile # 生产镜像
docker-compose.yml # 本地/生产容器启动模板
gunicorn.conf.py # gunicorn 运行配置
pyproject.toml # Python 项目和依赖声明
uv.lock # uv 锁文件
```
## 开发维护说明
### 新增一个 MCP 服务
新增服务的约定很简单,不要搞复杂框架:
1. 创建一个 Python package,例如 `my_service/`
2. 在 package 内提供 FastAPI app。
3. 新增 `my_service/service.py`,导出 `SERVICE: McpService`
4.`"my_service.service"` 加到 `common/mcp_service.py``BUILTIN_SERVICES`
5. 启动时根应用会自动导入、校验并挂载。
示例:
```python
from common.mcp_service import McpService
from my_service import app
SERVICE = McpService(
name="my_service",
app=app,
mount_path="/my-service-mcp",
)
```
### 新增 MCP 工具路由
所有 MCP 工具路由都应使用 **Pydantic body model**,不要用 query 参数传复杂对象。
正确形态:
```python
@app.post("/tool_name", operation_id="tool_name")
def _tool_name(req: ToolNameRequest):
return tool_name(**req.model_dump())
```
原因很简单:`fastapi-mcp``tools/call` 会把参数作为 JSON body 传给 FastAPI。复杂 dict 如果走 query 参数,会变成字符串,最后 422。
### 错误处理约定
工具函数内部保持干净:
- 找不到资源时抛 `KeyError`
- 非法状态或非法参数抛 `ValueError`
- 路由层由 FastAPI exception handler 转成 HTTP 状态码。
当前约定:
| 异常 | HTTP 状态 |
| --- | --- |
| `KeyError` | 404 |
| `ValueError` | 400 |
| Pydantic validation error | 422 |
| `FileExistsError`Files MCP | 409 |
不要在每个 route body 里手写一堆 `try/except`。那是噪音,不是设计。
### 文件头约定
新增 Python 文件时保持当前项目风格:
```python
# coding=utf-8
"""
@Time :YYYY/M/D
@Author :tao.chen
"""
```
### 日志约定
使用项目统一 logger
```python
from common.logging import logger
```
不要在业务模块里自己创建一套 logger。
## 注意事项
1. **两步提交是核心设计,不是可选项。** `prepare_submit_job` 只创建 pending 记录,`confirm_submit_job` 才能执行 `spark-submit`
2. **不要提交虚构脚本路径。** LLM 生成的 PySpark 必须先用 `write_job_file` 写入服务端文件系统。
3. **pending job 快照连接配置。** prepare 后再修改 Connection,不会影响已经创建的 pending job。
4. **MCP 请求需要 session affinity。** gunicorn 多 worker 会打散内存 session;默认部署请设 `GUNICORN_WORKERS=1`
5. **`job_id` 参数兼容两种 ID。** 本地 `job_id` 和 YARN `application_id` 都可以传。
6. **Files MCP 只能访问沙箱目录。** 想让它访问某个目录,就把那个目录挂载并设置为 `FILES_MCP_ROOT`,不要绕路径校验。
7. **新工具必须使用 Pydantic body model。** 这是 fastapi-mcp 的调用形态决定的,不是个人偏好。
## 当前状态
- Spark Executor MCP:已实现连接管理、两步提交、作业状态/结果/日志/Kill、PySpark 文件写入/读取/更新。
- Files MCP:已实现沙箱文件 CRUD、搜索、移动、复制。
- 持久化:当前使用 JSON 文件,`jobs.json` 带文件锁;后续可迁移到 SQLite。
- 测试:包含单元测试和集成测试,修改后运行 `uv run pytest`