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
« 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.
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.
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"""
16from __future__ import annotations
18import json
20import cli_runner
21from audit import audit_logged
22from feature_flags import FLAG_DESTRUCTIVE_OPERATIONS, is_enabled
23from server import mcp
25from tools._task_status import get_task, list_tasks, tail_log
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.
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.
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.
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.
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)
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.
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.
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.
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)
92if is_enabled(FLAG_DESTRUCTIVE_OPERATIONS):
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.
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.
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")