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.
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/CHANGELOG.md +8 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/PKG-INFO +1 -1
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/pyproject.toml +1 -1
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_cost.py +43 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/__init__.py +1 -1
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/models.py +16 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/cost.py +27 -1
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.dockerignore +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/ci.yml +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/docker-publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.gitignore +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.pre-commit-config.yaml +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/DESIGN.md +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/Dockerfile +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/LICENSE +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/README.md +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/README.md +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier0-diagnostics.json +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier1-cost.json +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier2-lifecycle.json +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier3-destructive.json +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/scripts/smoke_readonly.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_diagnostics.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_naming.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_no_embedded_secrets.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_reporting.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_secure_browser.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/consts.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/server.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/_common.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/diagnostics.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
- {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
|
+
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
|
+
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
|
|
|
@@ -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".
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier0-diagnostics.json
RENAMED
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier2-lifecycle.json
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/iam/tier3-destructive.json
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/scripts/smoke_readonly.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_destructive.py
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_diagnostics.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_performance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.4}/tests/test_secure_browser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|