Coverage for mcp/tools/templates.py: 97%
57 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-15 15:07 +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 name: str,
72 region: str | None = None,
73 override_namespace: str | None = None,
74 override_priority: int | None = None,
75) -> str:
76 """`gco templates run` — instantiate a job from a stored template.
78 Args:
79 name: Template name to run.
80 region: Region in which to run the resulting job.
81 override_namespace: Override the namespace embedded in the template.
82 override_priority: Override the priority embedded in the template.
83 """
84 args = ["templates", "run", name]
85 if region:
86 args += ["-r", region]
87 if override_namespace:
88 args += ["-n", override_namespace]
89 if override_priority is not None:
90 args += ["--priority", str(override_priority)]
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)