feat: 多语言任务、WebUI 增强与 Agent MCP 集成

重构 lib 为扁平模块并支持 Windows schtasks;新增 JS/Bash/PowerShell 模板、WebUI 调度编辑,以及 Cursor Skill 与 MCP 工具供 Agent 管理定时任务。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 21:45:25 +08:00
parent 6c2db2dfa3
commit c10cacd5c6
59 changed files with 3989 additions and 800 deletions

41
mcp-server/README.md Normal file
View File

@@ -0,0 +1,41 @@
# SproutClaw Cron MCP Server
通过 [Model Context Protocol](https://modelcontextprotocol.io/) 让 AI Agent 管理本项目的定时任务。
## 安装
```bash
pip install -r requirements.txt
# 或
pip install fastmcp
```
## 在 Cursor 中使用
项目已包含 `.cursor/mcp.json`。打开本仓库后 Cursor 会自动加载 `sproutclaw-cron` MCP 服务器。
手动测试stdio
```bash
python mcp-server/server.py
```
## 环境变量
| 变量 | 说明 |
|---|---|
| `SPROUTCLAW_CRON_ROOT` | 定时任务项目根目录;默认自动推断为 `mcp-server/` 的上级目录 |
## 工具一览
| 工具 | 说明 |
|---|---|
| `cron_list_tasks` | 列出全部任务 |
| `cron_get_task` | 任务详情 |
| `cron_enable_task` / `cron_disable_task` / `cron_toggle_task` | 开关 |
| `cron_run_task` | 立即执行 |
| `cron_get_log` | 读日志 |
| `cron_update_schedule` | 改 cron 表达式 / 描述 / 标签 |
| `cron_sync_cron` | 同步 cron.d / schtasks |
| `cron_create_task` | 从模板创建新任务 |
| `cron_list_templates` | 可用模板 |

12
mcp-server/pyproject.toml Normal file
View File

@@ -0,0 +1,12 @@
[project]
name = "sproutclaw-cron-mcp"
version = "0.1.0"
description = "MCP server for SproutClaw Cron task management"
requires-python = ">=3.10"
dependencies = [
"fastmcp>=3.0",
]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"

View File

@@ -0,0 +1 @@
fastmcp>=3.0

228
mcp-server/server.py Normal file
View File

@@ -0,0 +1,228 @@
#!/usr/bin/env python3
"""SproutClaw Cron MCP 服务器 — 通过 stdio 暴露定时任务管理工具。"""
from __future__ import annotations
import json
import os
import re
import shutil
import sys
from pathlib import Path
from typing import Literal
from fastmcp import FastMCP
MCP_DIR = Path(__file__).resolve().parent
CRON_ROOT = Path(os.environ.get("SPROUTCLAW_CRON_ROOT", MCP_DIR.parent)).resolve()
LIB_DIR = CRON_ROOT / "lib"
if str(LIB_DIR) not in sys.path:
sys.path.insert(0, str(LIB_DIR))
from manager import ( # noqa: E402
build_task_info,
get_context,
iter_task_ids,
list_tasks,
read_log_tail,
run_task,
set_task_state,
sync_cron_d,
toggle_task,
update_task_schedule,
)
from runner import task_is_enabled # noqa: E402
TEMPLATE_DIRS: dict[str, str] = {
"python": "_template",
"javascript": "_template-javascript",
"bash": "_template-bash",
"powershell": "_template-powershell",
}
TASK_ID_RE = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$")
DEFAULT_CRON_ROOTS = (
"/shumengya/project/agent/sproutclaw-cron",
str(CRON_ROOT),
str(CRON_ROOT).replace("\\", "/"),
)
mcp = FastMCP(
name="sproutclaw-cron",
instructions=(
"SproutClaw 定时任务系统。先 cron_list_tasks 了解任务;"
"新建任务用 cron_create_task默认禁用"
"改调度用 cron_update_schedule"
"试跑用 cron_run_task 后 cron_get_log 查日志。"
f" 项目根目录: {CRON_ROOT}"
),
)
def _root() -> Path:
return CRON_ROOT
def _task_dict(task_id: str) -> dict[str, object]:
return build_task_info(task_id, _root()).to_dict()
def _json(data: object) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)
def _rewrite_schedule_cron(schedule_file: Path, template_name: str, task_id: str) -> None:
text = schedule_file.read_text(encoding="utf-8")
text = text.replace(template_name, task_id)
cron_root_posix = _root().as_posix()
for old in DEFAULT_CRON_ROOTS:
if old and old != cron_root_posix:
text = text.replace(old, cron_root_posix)
text = re.sub(
r"(/usr/bin/python3\s+)\S+/cronctl\.py",
rf"\g<1>{cron_root_posix}/cronctl.py",
text,
)
schedule_file.write_text(text, encoding="utf-8")
@mcp.tool(annotations={"readOnlyHint": True})
def cron_list_tasks() -> str:
"""列出所有定时任务(含 .disabled/ 下已关闭的任务及状态、调度、runtime。"""
tasks = [info.to_dict() for info in list_tasks(_root())]
return _json({"count": len(tasks), "tasks": tasks})
@mcp.tool(annotations={"readOnlyHint": True})
def cron_get_task(task_id: str) -> str:
"""获取单个任务的详细信息状态、cron 表达式、描述、日志大小、标签等)。"""
return _json(_task_dict(task_id))
@mcp.tool
def cron_enable_task(task_id: str) -> str:
"""启用任务:移出 .disabled/ 并同步系统 cron / Windows 计划任务。"""
info, message = set_task_state(task_id, True, _root())
return _json({"task": info.to_dict(), "message": message})
@mcp.tool
def cron_disable_task(task_id: str) -> str:
"""禁用任务:移入 .disabled/cron 条目可保留,到点自动跳过)。"""
info, message = set_task_state(task_id, False, _root())
return _json({"task": info.to_dict(), "message": message})
@mcp.tool
def cron_toggle_task(task_id: str) -> str:
"""切换任务启用/禁用状态。"""
info, message = toggle_task(task_id, _root())
return _json({"task": info.to_dict(), "message": message})
@mcp.tool
def cron_run_task(task_id: str) -> str:
"""立即执行一次任务(同步等待完成),返回 exit code。禁用的任务会直接跳过。"""
exit_code = run_task(task_id, _root())
return _json({"task_id": task_id, "exit_code": exit_code})
@mcp.tool(annotations={"readOnlyHint": True})
def cron_get_log(task_id: str, lines: int = 200) -> str:
"""读取任务日志末尾内容(默认 200 行,最多 2000"""
lines = max(1, min(lines, 2000))
content = read_log_tail(task_id, lines, _root())
return _json({"task_id": task_id, "lines": lines, "content": content})
@mcp.tool
def cron_update_schedule(
task_id: str,
schedule: str,
description: str = "",
tags: list[str] | None = None,
) -> str:
"""更新 schedule.cron 中的 cron 五段表达式、描述注释与 task.json 标签,并同步 cron.d。"""
info, message = update_task_schedule(
task_id,
description=description,
schedule=schedule,
tags=tags,
root=_root(),
sync=True,
)
return _json({"task": info.to_dict(), "message": message})
@mcp.tool
def cron_sync_cron(task_id: str) -> str:
"""将 schedule.cron 同步到 /etc/cron.d/Linux或 schtasksWindows不改开关状态。"""
ctx = get_context(task_id, _root())
message = sync_cron_d(ctx, task_is_enabled(ctx))
return _json({"task_id": task_id, "message": message or "已同步"})
@mcp.tool
def cron_create_task(
task_id: str,
runtime: Literal["python", "javascript", "bash", "powershell"] = "python",
enable: bool = False,
) -> str:
"""从语言模板复制创建新任务。命名约定 <主机名>-<功能描述>。默认创建后保持禁用。"""
if not TASK_ID_RE.match(task_id):
raise ValueError("task_id 仅允许字母数字、点、连字符与下划线,且不能以特殊符号开头")
template_name = TEMPLATE_DIRS[runtime]
template_dir = _root() / template_name
if not template_dir.is_dir():
raise FileNotFoundError(f"模板不存在: {template_name}")
if task_id in iter_task_ids(_root()):
raise ValueError(f"任务已存在: {task_id}")
target = _root() / task_id
shutil.copytree(template_dir, target)
schedule_file = target / "schedule.cron"
if schedule_file.is_file():
_rewrite_schedule_cron(schedule_file, template_name, task_id)
if runtime == "bash":
run_sh = target / "run.sh"
if run_sh.is_file():
run_sh.chmod(run_sh.stat().st_mode | 0o111)
message = "已创建"
if not enable:
info, msg = set_task_state(task_id, False, _root())
message = msg or "已创建并禁用"
return _json({"task": info.to_dict(), "message": message, "template": template_name})
info, msg = set_task_state(task_id, True, _root())
return _json({"task": info.to_dict(), "message": msg or message, "template": template_name})
@mcp.tool(annotations={"readOnlyHint": True})
def cron_list_templates() -> str:
"""列出可用于 cron_create_task 的语言模板。"""
templates = []
for runtime, dirname in TEMPLATE_DIRS.items():
path = _root() / dirname
templates.append(
{
"runtime": runtime,
"template_dir": dirname,
"exists": path.is_dir(),
"entry": {
"python": "run.py",
"javascript": "run.js",
"bash": "run.sh",
"powershell": "run.ps1",
}[runtime],
}
)
return _json({"templates": templates})
if __name__ == "__main__":
mcp.run()