workspaces-euc-mcp-server 0.1.3__tar.gz → 0.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/CHANGELOG.md +8 -0
  2. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/PKG-INFO +1 -1
  3. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/pyproject.toml +1 -1
  4. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_cost.py +43 -0
  5. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/__init__.py +1 -1
  6. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/models.py +16 -0
  7. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/cost.py +27 -1
  8. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.dockerignore +0 -0
  9. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/ci.yml +0 -0
  10. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/docker-publish.yml +0 -0
  11. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/publish.yml +0 -0
  12. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.gitignore +0 -0
  13. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.pre-commit-config.yaml +0 -0
  14. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/DESIGN.md +0 -0
  15. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/Dockerfile +0 -0
  16. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/LICENSE +0 -0
  17. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/README.md +0 -0
  18. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/README.md +0 -0
  19. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier0-diagnostics.json +0 -0
  20. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier1-cost.json +0 -0
  21. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier2-lifecycle.json +0 -0
  22. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier3-destructive.json +0 -0
  23. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/scripts/smoke_readonly.py +0 -0
  24. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/__init__.py +0 -0
  25. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_clients.py +0 -0
  26. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_destructive.py +0 -0
  27. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_diagnostics.py +0 -0
  28. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_inventory.py +0 -0
  29. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_lifecycle.py +0 -0
  30. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_naming.py +0 -0
  31. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_no_embedded_secrets.py +0 -0
  32. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_performance.py +0 -0
  33. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_pricing.py +0 -0
  34. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_reporting.py +0 -0
  35. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_secure_browser.py +0 -0
  36. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/clients.py +0 -0
  37. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/consts.py +0 -0
  38. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/server.py +0 -0
  39. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
  40. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/_common.py +0 -0
  41. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
  42. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/diagnostics.py +0 -0
  43. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
  44. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
  45. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/performance.py +0 -0
  46. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
  47. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
  48. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/secure_browser.py +0 -0
@@ -5,6 +5,14 @@ All notable changes to this project are documented here. The format is based on
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.4] - 2026-06-02
9
+
10
+ ### Added
11
+ - `get_euc_cost_summary` now returns `by_period` — a per-bucket time series (one entry per day for
12
+ `DAILY`, per month for `MONTHLY`), each with its own per-service split. Previously the tool
13
+ collapsed all periods into per-service totals, so a `DAILY` request lost the daily breakdown and
14
+ clients had to query each day individually to chart trends.
15
+
8
16
  ## [0.1.3] - 2026-06-02
9
17
 
10
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspaces-euc-mcp-server
3
- Version: 0.1.3
3
+ Version: 0.1.4
4
4
  Summary: MCP server for administering the Amazon WorkSpaces family of End User Computing services (Personal, Pools, Applications, Secure Browser, Core).
5
5
  Project-URL: Homepage, https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp
6
6
  Project-URL: Repository, https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workspaces-euc-mcp-server"
3
- version = "0.1.3"
3
+ version = "0.1.4"
4
4
  description = "MCP server for administering the Amazon WorkSpaces family of End User Computing services (Personal, Pools, Applications, Secure Browser, Core)."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -172,6 +172,49 @@ def test_cost_summary_keyword_matches_variants_and_excludes_noneuc():
172
172
  assert summary.total == 1701.82
173
173
 
174
174
 
175
+ def test_cost_summary_daily_returns_per_period_time_series():
176
+ # DAILY granularity must preserve the per-day breakdown in by_period (for charts), not just
177
+ # collapse everything into by_service totals.
178
+ ce = types.SimpleNamespace(
179
+ get_cost_and_usage=lambda **_: {
180
+ "ResultsByTime": [
181
+ {
182
+ "TimePeriod": {"Start": "2026-05-01", "End": "2026-05-02"},
183
+ "Groups": [
184
+ {
185
+ "Keys": ["Amazon WorkSpaces Applications"],
186
+ "Metrics": {"UnblendedCost": {"Amount": "30.00", "Unit": "USD"}},
187
+ }
188
+ ],
189
+ },
190
+ {
191
+ "TimePeriod": {"Start": "2026-05-02", "End": "2026-05-03"},
192
+ "Groups": [
193
+ {
194
+ "Keys": ["Amazon WorkSpaces Applications"],
195
+ "Metrics": {"UnblendedCost": {"Amount": "45.00", "Unit": "USD"}},
196
+ }
197
+ ],
198
+ },
199
+ ]
200
+ }
201
+ )
202
+ factory = FakeFactory({consts.COST_EXPLORER_API: ce})
203
+
204
+ summary = cost.get_euc_cost_summary_core(
205
+ factory, granularity="DAILY", start_date="2026-05-01", end_date="2026-05-03"
206
+ )
207
+
208
+ # Aggregate total preserved...
209
+ assert summary.total == 75.0
210
+ # ...and the daily series is available, ordered by date.
211
+ assert [(p.start, p.total) for p in summary.by_period] == [
212
+ ("2026-05-01", 30.0),
213
+ ("2026-05-02", 45.0),
214
+ ]
215
+ assert summary.by_period[0].by_service[0].service == "Amazon WorkSpaces Applications"
216
+
217
+
175
218
  def test_cost_summary_explicit_date_range_overrides_lookback():
176
219
  captured: dict = {}
177
220
 
@@ -3,4 +3,4 @@
3
3
  # A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
4
4
  """MCP server for administering the Amazon WorkSpaces End User Computing portfolio."""
5
5
 
6
- __version__ = "0.1.3"
6
+ __version__ = "0.1.4"
@@ -231,6 +231,15 @@ class CostLineItem(BaseModel):
231
231
  amount: float
232
232
 
233
233
 
234
+ class CostPeriod(BaseModel):
235
+ """One time bucket (a day or a month, per the requested granularity)."""
236
+
237
+ start: str
238
+ end: str
239
+ total: float
240
+ by_service: list[CostLineItem] = Field(default_factory=list)
241
+
242
+
234
243
  class CostSummary(BaseModel):
235
244
  """Cost rollup for the EUC portfolio over a time window (account-wide)."""
236
245
 
@@ -244,6 +253,13 @@ class CostSummary(BaseModel):
244
253
  currency: str = "USD"
245
254
  total: float
246
255
  by_service: list[CostLineItem] = Field(default_factory=list)
256
+ by_period: list[CostPeriod] = Field(
257
+ default_factory=list,
258
+ description=(
259
+ "Per-bucket time series (one entry per day for DAILY, per month for MONTHLY). "
260
+ "Use this for charts / trend analysis."
261
+ ),
262
+ )
247
263
  errors: list[ServiceError] = Field(default_factory=list)
248
264
  notes: list[str] = Field(default_factory=list)
249
265
 
@@ -20,6 +20,7 @@ from .. import consts
20
20
  from ..clients import ClientFactory
21
21
  from ..models import (
22
22
  CostLineItem,
23
+ CostPeriod,
23
24
  CostSummary,
24
25
  Recommendation,
25
26
  RecommendationReport,
@@ -227,8 +228,10 @@ def get_euc_cost_summary_core(
227
228
 
228
229
  # Group by SERVICE across ALL spend and select EUC services in code (see _is_euc_service).
229
230
  # A server-side exact-name SERVICE filter would silently drop any naming variant — the very
230
- # bug that hid AppStream / WorkSpaces Applications spend. Page through all results.
231
+ # bug that hid AppStream / WorkSpaces Applications spend. Page through all results, keeping both
232
+ # the overall per-service totals and the per-period (daily/monthly) breakdown for charts.
231
233
  totals_by_service: dict[str, float] = {}
234
+ per_period: dict[tuple[str, str], dict[str, float]] = {}
232
235
  currency = "USD"
233
236
  next_token: str | None = None
234
237
  while True:
@@ -250,6 +253,8 @@ def get_euc_cost_summary_core(
250
253
  if not response:
251
254
  break
252
255
  for period in response.get("ResultsByTime", []):
256
+ tp = period.get("TimePeriod", {})
257
+ bucket = per_period.setdefault((tp.get("Start", ""), tp.get("End", "")), {})
253
258
  for group in period.get("Groups", []):
254
259
  service = (group.get("Keys") or ["Unknown"])[0]
255
260
  if not _is_euc_service(service):
@@ -258,6 +263,7 @@ def get_euc_cost_summary_core(
258
263
  amount = float(metric.get("Amount", 0.0))
259
264
  currency = metric.get("Unit", currency)
260
265
  totals_by_service[service] = totals_by_service.get(service, 0.0) + amount
266
+ bucket[service] = bucket.get(service, 0.0) + amount
261
267
  next_token = response.get("NextPageToken")
262
268
  if not next_token:
263
269
  break
@@ -267,6 +273,18 @@ def get_euc_cost_summary_core(
267
273
  for s, a in sorted(totals_by_service.items(), key=lambda kv: kv[1], reverse=True)
268
274
  ]
269
275
  total = round(sum(item.amount for item in by_service), 2)
276
+ by_period = [
277
+ CostPeriod(
278
+ start=p_start,
279
+ end=p_end,
280
+ total=round(sum(svc.values()), 2),
281
+ by_service=[
282
+ CostLineItem(service=s, amount=round(a, 2))
283
+ for s, a in sorted(svc.items(), key=lambda kv: kv[1], reverse=True)
284
+ ],
285
+ )
286
+ for (p_start, p_end), svc in sorted(per_period.items())
287
+ ]
270
288
 
271
289
  return CostSummary(
272
290
  start=start,
@@ -275,12 +293,15 @@ def get_euc_cost_summary_core(
275
293
  currency=currency,
276
294
  total=total,
277
295
  by_service=by_service,
296
+ by_period=by_period,
278
297
  errors=errors,
279
298
  notes=[
280
299
  "EUC services are selected by matching the Cost Explorer SERVICE name against the EUC "
281
300
  "keyword set (workspaces / appstream), so naming variants are not dropped.",
282
301
  "Cost Explorer bills WorkSpaces Personal, Pools, and Core together under the single "
283
302
  "'Amazon WorkSpaces' service; they cannot be separated via the SERVICE dimension.",
303
+ "by_period gives the per-bucket time series (per day for DAILY, per month for MONTHLY) "
304
+ "for charts/trends; by_service is the total across the whole window.",
284
305
  ],
285
306
  )
286
307
 
@@ -335,6 +356,11 @@ def register(mcp: Any, factory: ClientFactory) -> None:
335
356
  variants are never dropped. Cost Explorer is not region-scoped, so figures are account-wide.
336
357
  Read-only.
337
358
 
359
+ Returns both `by_service` (totals across the whole window) and `by_period` (a per-bucket
360
+ time series — one entry per day for DAILY, per month for MONTHLY — each with its own
361
+ per-service split). Use `by_period` with granularity="DAILY" to chart daily trends in a
362
+ single call.
363
+
338
364
  For a specific calendar month, pass start_date/end_date instead of lookback_days — Cost
339
365
  Explorer's end is EXCLUSIVE, so for May 2026 use start_date="2026-05-01",
340
366
  end_date="2026-06-01".