ClaudeandClaude Fable 5 3b698e1bdc fix: drop file tools 1 MB cap + add YarnError -> 502 handler
Two follow-ups to the previous error-handling fix (16fe011):

1) Drop the 1 MB cap on read_job_file and update_job_file.

   The cap was originally there to keep MCP responses bounded, but it
   blocks legitimate use of large PySpark scripts and large update
   payloads. With the new model (LLM composes scripts via
   write_job_file and edits them via read+update), a fixed 1 MB
   cap is more hindrance than protection. MCP response size is
   already bounded by the JSON transport and httpx; the tool itself
   doesn't need a second limit.

   Changes:
   - tools/job_file.py: delete MAX_FILE_BYTES constant, drop the two
     size checks in read_job_file and update_job_file, update
     module docstring.
   - tests/unit/test_job_file.py: delete test_update_rejects_
     oversized_content and test_update_rejects_1mb_plus_1_byte
     (the two tests that asserted the cap), replace with
     test_update_accepts_content_larger_than_former_1mb_cap.
   - server.py: drop "Caps reads at 1 MB" and "Caps writes at 1 MB"
     from the two route descriptions.

2) Add YarnError -> 502 handler.

   yarn_client wraps every httpx call: on connect / TLS / timeout /
   4xx / 5xx / parse failure it raises YarnError. Previously this
   was unhandled, so all six external job tools (get_external_job_*,
   list_applications, plus anything else that hits YARN) returned
   500 "Internal Server Error" with no detail — the LLM couldn't
   tell whether the cluster was down or the request was bad.

   Same fix as 16fe011 (which did this for fetch_url directly).
   The new handler returns HTTP 502 Bad Gateway with the YarnError
   message in the response detail. 502 because the MCP service is
   acting as a gateway to YARN — 502 is the standard status for
   "upstream didn't respond correctly".

   Changes:
   - server.py: import YarnError, add @app.exception_handler
     returning 502 + the YarnError message.
   - tests/integration/test_mcp_routes.py: new test asserts that
     when get_application_status raises YarnError, /get_job_status
     returns 502 with the YarnError message in the detail.

   Note: ValueError (request was bad) is still 400, KeyError (job
   not in JobStore) is still 404. The three handlers form a clean
   3-way classification of tool-layer errors.

Tests: 401 passed (was 401, +1 YarnError test, -2 cap tests = net -1).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:06:50 +08:00
2026-07-01 17:07:48 +08:00

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 删除连接配置
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 名称引用它。

示例参数:

{
  "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
S
Description
No description provided
Readme
462 KiB
Languages
Python 98.1%
Shell 1%
Dockerfile 0.9%