Files
mcp-server/README.md
T
ClaudeandClaude 7d4e512cb0 fix: address review findings on fetch-url-tool
- fetch_url: revalidate allowlist on every redirect hop (fixes SSRF where
  302 to disallowed host / 169.254.169.254 / file:// bypassed the
  url_allowlist). Stream response body with iter_bytes and cap at 1MB
  so a multi-GB response from an allowlisted host cannot OOM the service.
  Reuses the manual-redirect-loop pattern from yarn_client.

- list_applications: stop swallowing 404 (YARN returns 200+empty for
  "no match"; 404 means the RM doesn't support the endpoint — surface
  the YarnError instead of hiding it as an empty result). Add
  Field(ge=1, le=10000) to ListApplicationsRequest.limit so a runaway
  limit is rejected at the Pydantic layer with 422.

- save_connection: PATCH semantics for existing records. Re-route to
  update_connection when the name already exists so partial updates
  (e.g. only master) no longer wipe url_allowlist back to []. Uses an
  _UNSET sentinel in the tool function to distinguish "omitted" from
  "None" without breaking the existing parameter list.

- README: drop leading space on 5 new connection-tool table rows that
  was breaking GitHub Flavored Markdown table continuity.

- Indentation: normalize connections.py and requests.py to 4-space
  indent (auth_password/auth_principal/auth_keytab were 3-space).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 14:23:16 +08:00

489 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` | 删除连接配置 |
| `update_connection` | 部分更新一个已存在的连接 (PATCH 语义, 只改提供的字段) |
| `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 |
| `get_external_job_status` | 查询**非本服务提交**的外部 YARN application 状态(按 `application_id` + `connection_name` |
| `get_external_job_result` | 查询外部 YARN application 终态结果视图 |
| `get_external_job_logs` | 拉取外部 YARN application 的聚合日志 |
| `list_applications` | 列出 YARN 上所有应用(按 `state` / `queue` / `limit` 过滤),绕过 JobStore |
| `fetch_url` | 代理 HTTP GET 到集群内网 URL (host 受 `Connection.url_allowlist` glob allowlist 约束, 空则全拒) |
### 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`