Coverage for gco_mcp/tools/templates.py: 97.18%
55 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"""Job template management MCP tools (read-only)."""
3import asyncio
5import cli_runner
6from audit import audit_logged
7from server import mcp
10@mcp.tool(tags={"safe", "templates"})
11@audit_logged
12async def templates_list(region: str | None = None) -> str:
13 """`gco templates list` — list job templates.
15 Args:
16 region: Region to query (any region works).
17 """
18 args = ["templates", "list"]
19 if region:
20 args += ["-r", region]
21 return await asyncio.to_thread(cli_runner._run_cli, *args)
24@mcp.tool(tags={"safe", "templates"})
25@audit_logged
26async def templates_get(name: str, region: str | None = None) -> str:
27 """`gco templates get` — fetch a single job template by name.
29 Args:
30 name: Template name.
31 region: Region to query (any region works).
32 """
33 args = ["templates", "get", name]
34 if region:
35 args += ["-r", region]
36 return await asyncio.to_thread(cli_runner._run_cli, *args)
39# =============================================================================
40# Mutating tools (low-risk)
41# =============================================================================
44@mcp.tool(tags={"low-risk", "templates"})
45@audit_logged
46async def templates_create(
47 name: str,
48 manifest_path: str,
49 region: str | None = None,
50 description: str | None = None,
51) -> str:
52 """`gco templates create` — register a new job template from a manifest.
54 Args:
55 name: Template name.
56 manifest_path: Path to the source manifest YAML.
57 region: Region to use (any region works).
58 description: Optional human-readable description.
59 """
60 args = ["templates", "create", name, manifest_path]
61 if region:
62 args += ["-r", region]
63 if description:
64 args += ["-d", description]
65 return await asyncio.to_thread(cli_runner._run_cli, *args)
68@mcp.tool(tags={"low-risk", "templates"})
69@audit_logged
70async def templates_run(
71 template_name: str,
72 job_name: str,
73 region: str,
74 namespace: str | None = None,
75 params: list[str] | None = None,
76) -> str:
77 """`gco templates run` — instantiate and run a job from a stored template.
79 Args:
80 template_name: Name of the stored template to instantiate.
81 job_name: Name for the resulting job.
82 region: Region in which to run the job.
83 namespace: Override the Kubernetes namespace (default: gco-jobs).
84 params: Parameter overrides as ``key=value`` strings (one ``-p`` each).
85 """
86 args = ["templates", "run", template_name, "--name", job_name, "-r", region]
87 if namespace:
88 args += ["--namespace", namespace]
89 for param in params or []:
90 args += ["-p", param]
91 return await asyncio.to_thread(cli_runner._run_cli, *args)
94# =============================================================================
95# Destructive tools — gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS
96# =============================================================================
99import contextlib # noqa: E402
101from feature_flags import FLAG_DESTRUCTIVE_OPERATIONS, is_enabled # noqa: E402
104async def _ctx_warning(message: str) -> None:
105 """Emit ``ctx.warning(...)`` from inside a tool body, no-op when no Context."""
106 try:
107 from fastmcp.server.dependencies import get_context
109 ctx = get_context()
110 except Exception:
111 return
112 with contextlib.suppress(Exception):
113 await ctx.warning(message)
116if is_enabled(FLAG_DESTRUCTIVE_OPERATIONS):
118 @mcp.tool(tags={"destructive", "templates"})
119 @audit_logged
120 async def delete_template(name: str, region: str | None = None) -> str:
121 """[gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS] destructive.
123 `gco templates delete` — delete a job template.
124 Cannot be undone — the template definition is permanently removed.
126 Args:
127 name: Template name.
128 region: Region to use (any region works).
129 """
130 await _ctx_warning(f"Deleting template {name!r} — this cannot be undone.")
131 args = ["templates", "delete", name, "-y"]
132 if region:
133 args += ["-r", region]
134 return await asyncio.to_thread(cli_runner._run_cli, *args)