Files
mcp-server/README.md
T
ClaudeandClaude Fable 5 6cf68439a2 feat: add external job tools + improve LLM-facing tool descriptions
Add 3 new MCP tools for inspecting YARN applications NOT submitted
through this service: get_external_job_logs, get_external_job_status,
get_external_job_result. Each takes application_id + connection_name
and queries YARN directly, bypassing the local JobStore.

- spark_executor/tools/external_jobs.py: 3 tool functions
- spark_executor/tools/requests.py: 3 new Pydantic body models
  (ExternalJobLogsRequest, ExternalJobStatusRequest,
  ExternalJobResultRequest)
- spark_executor/server.py: 3 new POST routes with explicit operation_id
- tests/unit/test_external_jobs.py: 7 unit tests
- tests/integration/test_mcp_routes.py: assert 20 tool routes
- README.md: list the 3 new tools

To make the LLM pick the right tool and not guess at field values,
also:

- Add Pydantic field descriptions for 22 fields across 8 request models
  (SaveConnectionRequest, UpdatePendingJobRequest, GetJobLogsRequest,
  JobIdRequest, PendingIdRequest, ConnectionNameRequest, plus the new
  ExternalJob*Request models).
- Update 12 route descriptions with cross-references, prerequisite
  context, and 400 behavior notes.
- Refactor _unknown_job_error: an input that looks like a YARN
  application_id (starts with 'application_') now returns HTTP 400
  (ValueError) with a hint message naming the right external tool;
  other not-found cases still return 404 (KeyError). This catches the
  common LLM mistake of passing application_id to the internal
  get_job_* / kill_job tools.
- 4 new unit tests for the 400 behavior.

Tests: 356 passed (up from 242).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 20:03:40 +08:00

15 KiB
Raw Blame History

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. 安装依赖

uv sync

项目依赖在 pyproject.tomluv.lock 中固定,默认 PyPI 镜像配置为清华源。

2. 启动服务

开发环境直接运行:

uv run main.py

默认监听:

0.0.0.0:8000

健康检查:

curl http://localhost:8000/health

预期返回:

{"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/listtools/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
get_external_job_status 查询非本服务提交的外部 YARN application 状态(按 application_id + connection_name
get_external_job_result 查询外部 YARN application 终态结果视图
get_external_job_logs 拉取外部 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 名称引用它。

示例参数:

{
  "name": "prod-yarn",
  "master": "yarn",
  "deploy_mode": "cluster",
  "yarn_rm_url": "http://yarn-rm.example.com:8088",
  "spark_conf": {
    "spark.yarn.queue": "default"
  }
}

调用工具:

save_connection

2. 写入或选择 PySpark 脚本

如果脚本由 LLM 生成,必须先调用:

write_job_file

该工具会把代码写入 SPARK_EXECUTOR_JOBS_DIR,并返回真实存在于服务端文件系统中的 script_path

如果脚本已经存在,需要确保传入的是容器或服务进程能访问到的真实路径。只存在于 Agent 上下文里的“虚构路径”会被拒绝。

3. 准备提交

调用:

prepare_submit_job

这个步骤会:

  • 读取并快照指定 Connectionmasterdeploy_modespark_confyarn_rm_url
  • 创建 PendingSubmission 持久化记录。
  • 返回 pending_id
  • 不会调用 spark-submit

注意:如果准备后再修改连接配置,已经创建的 pending job 不会被重新定向到新连接。快照就是快照,别让状态到处乱飞。

4. 二次确认提交

用户确认后调用:

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

可用工具:

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 foundInvalid 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.jsonpending_jobs.jsonjobs.json 都使用简单 JSON 文件持久化。当前这是务实方案:足够简单,方便排查,也方便未来迁移到 SQLite。

Docker 部署

1. 准备 Hadoop/YARN 配置

在项目根目录创建或挂载:

./hadoop-conf/

至少应包含目标集群匹配的:

core-site.xml
yarn-site.xml
hdfs-site.xml

这些文件会以只读方式挂载到容器内:

/etc/hadoop/conf

2. 准备环境变量

复制模板:

cp .env.example .env

按部署环境修改 .env,至少确认:

YARN_RESOURCE_MANAGER_URL=
SPARK_EXECUTOR_DATA_DIR=
SPARK_EXECUTOR_JOBS_DIR=
FILES_MCP_ROOT=
GUNICORN_WORKERS=1

GUNICORN_WORKERS=1 不是装饰品。除非你真的配置了 sticky-session,否则不要把它调大。

3. 启动容器

docker compose up -d --build

查看日志:

docker compose logs -f mcp-server

停止服务:

docker compose down

测试

运行完整测试:

uv run pytest

运行单个测试文件:

uv run pytest tests/unit/test_submit_tool.py -v

按名称过滤:

uv run pytest -v -k cancel_pending

测试目录分为:

tests/unit/          # 单元测试
tests/integration/   # FastAPI / MCP 集成测试

目录结构

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.pyBUILTIN_SERVICES
  5. 启动时根应用会自动导入、校验并挂载。

示例:

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 参数传复杂对象。

正确形态:

@app.post("/tool_name", operation_id="tool_name")
def _tool_name(req: ToolNameRequest):
    return tool_name(**req.model_dump())

原因很简单:fastapi-mcptools/call 会把参数作为 JSON body 传给 FastAPI。复杂 dict 如果走 query 参数,会变成字符串,最后 422。

错误处理约定

工具函数内部保持干净:

  • 找不到资源时抛 KeyError
  • 非法状态或非法参数抛 ValueError
  • 路由层由 FastAPI exception handler 转成 HTTP 状态码。

当前约定:

异常 HTTP 状态
KeyError 404
ValueError 400
Pydantic validation error 422
FileExistsErrorFiles MCP 409

不要在每个 route body 里手写一堆 try/except。那是噪音,不是设计。

文件头约定

新增 Python 文件时保持当前项目风格:

# coding=utf-8
"""
@Time   :YYYY/M/D
@Author :tao.chen
"""

日志约定

使用项目统一 logger

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