Coverage for gco_mcp/tools/costs.py: 90.65%

87 statements  

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

1"""Cost tracking MCP tools.""" 

2 

3import cli_runner 

4from audit import audit_logged 

5from server import mcp 

6 

7 

8@mcp.tool(tags={"safe", "costs"}) 

9@audit_logged 

10def cost_summary(days: int = 30) -> str: 

11 """Get total GCO spend broken down by AWS service. 

12 

13 Args: 

14 days: Number of days to look back. 

15 """ 

16 return cli_runner._run_cli("costs", "summary", "--days", str(days)) 

17 

18 

19@mcp.tool(tags={"safe", "costs"}) 

20@audit_logged 

21def cost_by_region(days: int = 30) -> str: 

22 """Get cost breakdown by AWS region. 

23 

24 Args: 

25 days: Number of days to look back. 

26 """ 

27 return cli_runner._run_cli("costs", "regions", "--days", str(days)) 

28 

29 

30@mcp.tool(tags={"safe", "costs"}) 

31@audit_logged 

32def cost_trend(days: int = 14) -> str: 

33 """Get daily cost trend. 

34 

35 Args: 

36 days: Number of days to show. 

37 """ 

38 return cli_runner._run_cli("costs", "trend", "--days", str(days)) 

39 

40 

41@mcp.tool(tags={"safe", "costs"}) 

42@audit_logged 

43def cost_workloads(region: str | None = None) -> str: 

44 """Estimate accumulated and hourly cost for running workloads. 

45 

46 Args: 

47 region: Region to inspect, or omit to inspect every deployment region. 

48 """ 

49 args = ["costs", "workloads"] 

50 if region: 50 ↛ 52line 50 didn't jump to line 52 because the condition on line 50 was always true

51 args += ["-r", region] 

52 return cli_runner._run_cli(*args) 

53 

54 

55@mcp.tool(tags={"safe", "costs"}) 

56@audit_logged 

57def cost_forecast(days_ahead: int = 30) -> str: 

58 """Forecast GCO costs for the next N days. 

59 

60 Args: 

61 days_ahead: Days to forecast ahead. 

62 """ 

63 return cli_runner._run_cli("costs", "forecast", "--days", str(days_ahead)) 

64 

65 

66@mcp.tool(tags={"safe", "costs"}) 

67@audit_logged 

68def cost_allocation_status(extra_tags: list[str] | None = None) -> str: 

69 """Show cost allocation tag activation status for GCO's billing keys. 

70 

71 Reports whether the Project tag (which every cost query filters on) and 

72 the AWS-generated aws:eks:cluster-name tag (per-cluster attribution for 

73 EKS Auto Mode compute) are Active, Inactive, or not yet discovered by 

74 Billing, plus backfill history and split-cost-allocation-data guidance. 

75 

76 Args: 

77 extra_tags: Additional tag keys to check. 

78 """ 

79 args = ["costs", "allocation", "status"] 

80 for tag in extra_tags or []: 

81 args += ["-t", tag] 

82 return cli_runner._run_cli(*args) 

83 

84 

85@mcp.tool(tags={"low-risk", "costs"}) 

86@audit_logged 

87def cost_allocation_activate( 

88 extra_tags: list[str] | None = None, 

89 backfill_from: str | None = None, 

90) -> str: 

91 """Activate GCO's cost allocation tag keys in the billing account. 

92 

93 Activates the Project and aws:eks:cluster-name tag keys (plus any 

94 extras) so Cost Explorer can attribute spend to them. Reversible in 

95 the Billing console; in an AWS Organization this requires the 

96 management (payer) account. Only affects billing data from activation 

97 onward unless a backfill date is given. 

98 

99 Args: 

100 extra_tags: Additional tag keys to activate. 

101 backfill_from: Optionally re-tag historical usage from this date 

102 (YYYY-MM-DD, up to 12 months back). 

103 """ 

104 args = ["costs", "allocation", "activate", "-y"] 

105 for tag in extra_tags or []: 105 ↛ 106line 105 didn't jump to line 106 because the loop on line 105 never started

106 args += ["-t", tag] 

107 if backfill_from: 

108 args += ["--backfill-from", backfill_from] 

109 return cli_runner._run_cli(*args) 

110 

111 

112@mcp.tool(tags={"safe", "costs"}) 

113@audit_logged 

114def cost_k8s_namespaces(days: int = 7, region: str | None = None) -> str: 

115 """Show Kubernetes cost by namespace (Athena over OpenCost reports). 

116 

117 Args: 

118 days: Days to look back. 

119 region: Restrict to one deployment region, or omit for all. 

120 """ 

121 args = ["costs", "k8s", "namespaces", "--days", str(days)] 

122 if region: 122 ↛ 124line 122 didn't jump to line 124 because the condition on line 122 was always true

123 args += ["-r", region] 

124 return cli_runner._run_cli(*args) 

125 

126 

127@mcp.tool(tags={"safe", "costs"}) 

128@audit_logged 

129def cost_k8s_regions(days: int = 7) -> str: 

130 """Show Kubernetes allocation cost by deployment region. 

131 

132 Args: 

133 days: Days to look back. 

134 """ 

135 return cli_runner._run_cli("costs", "k8s", "regions", "--days", str(days)) 

136 

137 

138@mcp.tool(tags={"safe", "costs"}) 

139@audit_logged 

140def cost_k8s_trend( 

141 days: int = 14, 

142 granularity: str = "daily", 

143 namespace: str | None = None, 

144) -> str: 

145 """Show Kubernetes cost over time. 

146 

147 Args: 

148 days: Days to look back. 

149 granularity: Trend bucket size ("daily" or "hourly"). 

150 namespace: Restrict to one namespace, or omit for all. 

151 """ 

152 args = ["costs", "k8s", "trend", "--days", str(days), "--granularity", granularity] 

153 if namespace: 153 ↛ 155line 153 didn't jump to line 155 because the condition on line 153 was always true

154 args += ["-n", namespace] 

155 return cli_runner._run_cli(*args) 

156 

157 

158@mcp.tool(tags={"safe", "costs"}) 

159@audit_logged 

160def cost_k8s_top(limit: int = 10, by: str = "namespace", days: int = 7) -> str: 

161 """Show the top-N Kubernetes spenders by namespace, region, or cluster. 

162 

163 Args: 

164 limit: Number of results. 

165 by: Grouping dimension ("namespace", "region", or "cluster"). 

166 days: Days to look back. 

167 """ 

168 return cli_runner._run_cli( 

169 "costs", "k8s", "top", "-n", str(limit), "--by", by, "--days", str(days) 

170 ) 

171 

172 

173@mcp.tool(tags={"safe", "costs"}) 

174@audit_logged 

175def cost_report_status(region: str | None = None) -> str: 

176 """Show cost monitoring health, including OpenCost status. 

177 

178 Args: 

179 region: Region whose cost monitor to check, or omit for the 

180 nearest healthy region via the global API. 

181 """ 

182 args = ["costs", "report", "status"] 

183 if region: 183 ↛ 185line 183 didn't jump to line 185 because the condition on line 183 was always true

184 args += ["-r", region] 

185 return cli_runner._run_cli(*args) 

186 

187 

188@mcp.tool(tags={"safe", "costs"}) 

189@audit_logged 

190def cost_report_list(region: str | None = None, adhoc: bool = False, limit: int = 20) -> str: 

191 """List recent OpenCost allocation report objects in S3. 

192 

193 Args: 

194 region: Region whose reports to list. 

195 adhoc: List ad-hoc instead of scheduled reports. 

196 limit: Maximum results. 

197 """ 

198 args = ["costs", "report", "list", "-l", str(limit)] 

199 if region: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 args += ["-r", region] 

201 if adhoc: 201 ↛ 203line 201 didn't jump to line 203 because the condition on line 201 was always true

202 args += ["--adhoc"] 

203 return cli_runner._run_cli(*args) 

204 

205 

206@mcp.tool(tags={"low-risk", "costs"}) 

207@audit_logged 

208def cost_report_generate(region: str | None = None, window_hours: int = 24) -> str: 

209 """Generate an ad-hoc OpenCost allocation report now (written to S3). 

210 

211 Args: 

212 region: Region whose cost monitor generates the report. 

213 window_hours: Trailing window the report covers (1-168). 

214 """ 

215 args = ["costs", "report", "generate", "--window-hours", str(window_hours)] 

216 if region: 216 ↛ 218line 216 didn't jump to line 218 because the condition on line 216 was always true

217 args += ["-r", region] 

218 return cli_runner._run_cli(*args)