Coverage for gco_mcp/tools/tasks.py: 100.00%

31 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-30 21:22 +0000

1"""MCP tools for inspecting and pruning long-running task status. 

2 

3Every long-running tool (``deploy_all``, ``destroy_all``, etc.) writes 

4a JSON status file and a raw log file under ``~/.gco/tasks/`` via 

5``_task_status.TaskStatusWriter``. The two tools in this module read 

6those artifacts so any MCP client — including agents that don't render 

7``ctx.info`` notifications — can see real-time progress without 

8blocking on the original tool call's response. 

9 

10The status and tail tools are read-only and always enabled: they never 

11mutate AWS state or spawn subprocesses. The optional ``task_prune`` tool 

12removes old local status/log pairs and is gated by 

13``GCO_ENABLE_DESTRUCTIVE_OPERATIONS``. 

14""" 

15 

16from __future__ import annotations 

17 

18import json 

19 

20import cli_runner 

21from audit import audit_logged 

22from feature_flags import FLAG_DESTRUCTIVE_OPERATIONS, is_enabled 

23from server import mcp 

24 

25from tools._task_status import get_task, list_tasks, tail_log 

26 

27 

28@mcp.tool(tags={"safe", "observability"}) 

29@audit_logged 

30def task_status(task_id: str | None = None, limit: int = 20) -> str: 

31 """Return live status of long-running tools. 

32 

33 Each long-running tool (deploy_all, destroy_all, bootstrap_cdk, 

34 deploy_stack, destroy_stack, images_build, images_push) records 

35 progress to ``~/.gco/tasks/{task_id}.json`` on every output line. 

36 This tool reads that disk-backed channel — independent of whatever 

37 the calling MCP client decides to do with progress notifications — 

38 so operators (and other agents) can observe what's happening. 

39 

40 PIDs are re-checked on every read; a status file claiming 

41 ``state=running`` whose recorded PID is no longer alive is rewritten 

42 to ``state=orphaned`` in the response so callers see honest data 

43 even when the original MCP wrapper exited unexpectedly while the 

44 underlying CLI was still running. 

45 

46 Args: 

47 task_id: Specific task to inspect (e.g. ``deploy_all-1747683123``). 

48 Omit to list every known task, newest first. 

49 limit: When listing, maximum number of records to return. 

50 Ignored when ``task_id`` is set. 

51 

52 Returns: 

53 JSON string. When ``task_id`` is set, an object with the task 

54 record. When omitted, ``{"tasks": [...]}`` newest-first. 

55 """ 

56 if task_id: 

57 record = get_task(task_id) 

58 if record is None: 

59 return json.dumps({"error": "task_not_found", "task_id": task_id}) 

60 return json.dumps(record, indent=2, sort_keys=True) 

61 records = list_tasks() 

62 if limit > 0: 

63 records = records[:limit] 

64 return json.dumps({"tasks": records}, indent=2, sort_keys=True) 

65 

66 

67@mcp.tool(tags={"safe", "observability"}) 

68@audit_logged 

69def task_tail(task_id: str, lines: int = 100) -> str: 

70 """Return the last N lines of a long-running task's raw output log. 

71 

72 The log captures the full interleaved stdout+stderr of the 

73 underlying subprocess (CDK deploy, finch image push, etc.) as the 

74 tool wrote it to disk. Each line is prefixed with ``[stdout]`` or 

75 ``[stderr]`` so observers can tell which stream produced it. 

76 

77 Args: 

78 task_id: Task to read (from ``task_status``). 

79 lines: Maximum lines to return. ``100`` is enough to see the 

80 most recent stack milestone plus surrounding context; 

81 ``500`` typically covers a full single-stack deploy. 

82 

83 Returns: 

84 JSON string ``{"task_id": ..., "lines": [...]}`` containing the 

85 tail. Empty list when the task hasn't emitted any output yet 

86 or its log file has been pruned. 

87 """ 

88 tail = tail_log(task_id, lines=lines) 

89 return json.dumps({"task_id": task_id, "lines": tail}, indent=2) 

90 

91 

92if is_enabled(FLAG_DESTRUCTIVE_OPERATIONS): 

93 

94 @mcp.tool(tags={"destructive", "observability"}) 

95 @audit_logged 

96 def task_prune(keep: int = 50) -> str: 

97 """[gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS] destructive local cleanup. 

98 

99 Delete old ``~/.gco/tasks`` status/log pairs while retaining the newest 

100 ``keep`` records. This never changes AWS or Kubernetes state, but removed 

101 local task history cannot be recovered. 

102 

103 Args: 

104 keep: Number of newest task records to retain; must be non-negative. 

105 """ 

106 if keep < 0: 

107 raise ValueError("keep must be non-negative") 

108 return cli_runner._run_cli("tasks", "prune", "--keep", str(keep), "--yes")