diff --git a/cloud/mcp_server.py b/cloud/mcp_server.py index c138dee..b91964a 100644 --- a/cloud/mcp_server.py +++ b/cloud/mcp_server.py @@ -88,6 +88,24 @@ TOOLS = [ {"task_id": {"type": "string"}}, ["task_id"]), _tool("merge_task", "合并一个已完成任务的改动(服务端状态机校验,未完成会拒绝)。", {"task_id": {"type": "string"}}, ["task_id"]), + _tool("pause_task", "暂停一个进行中任务(服务端状态机校验)。", + {"task_id": {"type": "string"}}, ["task_id"]), + _tool("goal_resume_task", "断点续跑一个 goal 任务(服务端状态机校验)。", + {"task_id": {"type": "string"}}, ["task_id"]), + _tool("create_followup", "给进行中任务发后续消息;mode=queued 排队进队尾不打断本轮,direct_send 立即送达且本轮结束后优先开下一轮。", + {"task_id": {"type": "string"}, + "body": {"type": "string"}, + "mode": {"type": "string"}}, ["task_id", "body"]), + _tool("get_task_agents", "取单个任务的 agent 分工条目。", + {"task_id": {"type": "string"}}, ["task_id"]), + _tool("list_workers", "列出 worker 聚合状态(普通用户只见在线/离线数,管理员见完整清单)。", + {}, []), + _tool("get_metrics", "取控制面可观测性聚合:任务计数、成功率、平均耗时、成本合计(只统计调用者是成员的项目)。", + {}, []), + _tool("list_project_members", "列出某项目的成员。", + {"project_id": {"type": "string"}}, ["project_id"]), + _tool("get_models", "取模型一览(价目与默认模型,服务端口径)。", + {}, []), ] @@ -187,6 +205,40 @@ class MCPServer: return True, {"error": "task_id required"} return self._wrap(*self.transport( "POST", "/tasks/" + args["task_id"] + "/merge", {})) + if name == "pause_task": + if not args.get("task_id"): + return True, {"error": "task_id required"} + return self._wrap(*self.transport( + "POST", "/tasks/" + args["task_id"] + "/pause", {})) + if name == "goal_resume_task": + if not args.get("task_id"): + return True, {"error": "task_id required"} + return self._wrap(*self.transport( + "POST", "/tasks/" + args["task_id"] + "/goal-resume", {})) + if name == "create_followup": + if not args.get("task_id") or not args.get("body"): + return True, {"error": "task_id and body required"} + body = {"body": args["body"]} + if args.get("mode") is not None: + body["mode"] = args["mode"] + return self._wrap(*self.transport( + "POST", "/tasks/" + args["task_id"] + "/followups", body)) + if name == "get_task_agents": + if not args.get("task_id"): + return True, {"error": "task_id required"} + return self._wrap(*self.transport( + "GET", "/tasks/" + args["task_id"] + "/agents")) + if name == "list_workers": + return self._wrap(*self.transport("GET", "/workers")) + if name == "get_metrics": + return self._wrap(*self.transport("GET", "/metrics")) + if name == "list_project_members": + if not args.get("project_id"): + return True, {"error": "project_id required"} + return self._wrap(*self.transport( + "GET", "/projects/" + args["project_id"] + "/members")) + if name == "get_models": + return self._wrap(*self.transport("GET", "/models")) return True, {"error": "unknown tool: %s" % name} @staticmethod diff --git a/cloud/tests/test_mcp_server.py b/cloud/tests/test_mcp_server.py index 69418c8..afba3c7 100644 --- a/cloud/tests/test_mcp_server.py +++ b/cloud/tests/test_mcp_server.py @@ -49,6 +49,22 @@ class FakeTransport: return 200, {"id": "t1", "status": "queued"} if path == "/tasks/t1/merge": return 200, {"id": "t1", "status": "merged"} + if path == "/tasks/t1/pause": + return 200, {"id": "t1", "status": "paused"} + if path == "/tasks/t1/goal-resume": + return 200, {"id": "t1", "status": "queued"} + if path == "/tasks/t1/followups" and method == "POST": + return 200, {"id": "t1", "mode": "queued"} + if path == "/tasks/t1/agents": + return 200, {"agents": []} + if path == "/workers": + return 200, {"workers_total": 2} + if path == "/metrics": + return 200, {"total_tasks": 3} + if path == "/projects/p1/members": + return 200, {"members": []} + if path == "/models": + return 200, {"models": []} if path == "/tasks/nope": return 404, {"error": "not found"} return 400, {"error": "bad request"} @@ -143,6 +159,28 @@ class MCPServerTestCase(unittest.TestCase): self.assertFalse(is_error) self.assertEqual(payload["status"], "merged") + def test_expanded_tools_translate_to_rest(self): + cases = [ + ("pause_task", {"task_id": "t1"}, ("POST", "/tasks/t1/pause")), + ("goal_resume_task", {"task_id": "t1"}, + ("POST", "/tasks/t1/goal-resume")), + ("create_followup", {"task_id": "t1", "body": "hi"}, + ("POST", "/tasks/t1/followups")), + ("get_task_agents", {"task_id": "t1"}, + ("GET", "/tasks/t1/agents")), + ("list_workers", {}, ("GET", "/workers")), + ("get_metrics", {}, ("GET", "/metrics")), + ("list_project_members", {"project_id": "p1"}, + ("GET", "/projects/p1/members")), + ("get_models", {}, ("GET", "/models")), + ] + for name, args, expected in cases: + is_error, payload = self.server.call_tool(name, args) + self.assertFalse(is_error, name) + self.assertEqual(self.transport.calls[-1][:2], expected, name) + is_error, _ = self.server.call_tool("create_followup", {"task_id": "t1"}) + self.assertTrue(is_error) + def test_missing_arg_is_error_not_rest_call(self): before = len(self.transport.calls) is_error, payload = self.server.call_tool("list_tasks", {})