workspaces-euc-mcp-server 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,620 @@
1
+ # Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
2
+ # You may not use this file except in compliance with the License.
3
+ # A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
4
+ """WorkSpaces Personal performance metrics & bundle right-sizing (read-only, IAM Tier 0).
5
+
6
+ The AWS/WorkSpaces namespace publishes per-desktop resource metrics (CPUUsage, MemoryUsage,
7
+ GPUUsage, disk usage, latency, uptime, …) natively, keyed by WorkspaceId — no CloudWatch agent
8
+ required. ``get_workspace_performance`` surfaces them; ``recommend_bundle_rightsizing`` uses CPU and
9
+ memory headroom to suggest a smaller/larger compute type.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from datetime import UTC, datetime, timedelta
15
+ from typing import Any
16
+
17
+ from .. import consts
18
+ from ..clients import ClientFactory
19
+ from ..models import (
20
+ FleetMetricSeries,
21
+ FleetUsage,
22
+ MetricStat,
23
+ PerformanceReport,
24
+ Recommendation,
25
+ RecommendationReport,
26
+ ServiceError,
27
+ UsageHistory,
28
+ UsagePoint,
29
+ WorkspacePerformance,
30
+ )
31
+ from . import pricing
32
+ from ._common import paginate, read_only, try_call
33
+
34
+ # Right-sizing thresholds on window peak (%). Conservative: only suggest a downsize when there is
35
+ # clear headroom, and an upsize when the desktop is genuinely pressured.
36
+ _DOWNSIZE_PEAK_CPU = 30.0
37
+ _DOWNSIZE_PEAK_MEM = 40.0
38
+ _UPSIZE_PEAK_CPU = 85.0
39
+ _UPSIZE_PEAK_MEM = 85.0
40
+ _MIN_DATAPOINTS = 6 # need at least this many hourly points to judge
41
+
42
+
43
+ def _fetch_metrics(
44
+ cloudwatch: Any,
45
+ workspace_id: str,
46
+ metric_names: list[tuple[str, str]],
47
+ lookback_hours: int,
48
+ period: int = 300,
49
+ ) -> dict[str, MetricStat]:
50
+ """Fetch Average + Maximum series for each metric and reduce to latest/average/peak."""
51
+ end = datetime.now(UTC)
52
+ start = end - timedelta(hours=lookback_hours)
53
+ stat_suffix = {"Average": "avg", "Maximum": "max"}
54
+ queries: list[dict[str, Any]] = []
55
+ for i, (name, _unit) in enumerate(metric_names):
56
+ for stat in ("Average", "Maximum"):
57
+ queries.append(
58
+ {
59
+ "Id": f"m{i}_{stat_suffix[stat]}",
60
+ "MetricStat": {
61
+ "Metric": {
62
+ "Namespace": "AWS/WorkSpaces",
63
+ "MetricName": name,
64
+ "Dimensions": [{"Name": "WorkspaceId", "Value": workspace_id}],
65
+ },
66
+ "Period": period,
67
+ "Stat": stat,
68
+ },
69
+ "ReturnData": True,
70
+ }
71
+ )
72
+ response = cloudwatch.get_metric_data(
73
+ MetricDataQueries=queries,
74
+ StartTime=start,
75
+ EndTime=end,
76
+ ScanBy="TimestampAscending",
77
+ )
78
+ by_id = {r.get("Id"): r.get("Values", []) for r in response.get("MetricDataResults", [])}
79
+
80
+ out: dict[str, MetricStat] = {}
81
+ for i, (name, unit) in enumerate(metric_names):
82
+ avg_vals = by_id.get(f"m{i}_avg", [])
83
+ max_vals = by_id.get(f"m{i}_max", [])
84
+ if not avg_vals and not max_vals:
85
+ continue
86
+ out[name] = MetricStat(
87
+ latest=avg_vals[-1] if avg_vals else None,
88
+ average=(sum(avg_vals) / len(avg_vals)) if avg_vals else None,
89
+ peak=max(max_vals) if max_vals else None,
90
+ unit=unit,
91
+ )
92
+ return out
93
+
94
+
95
+ def get_workspace_performance_core(
96
+ factory: ClientFactory,
97
+ workspace_ids: list[str],
98
+ region: str | None,
99
+ lookback_hours: int = 3,
100
+ ) -> PerformanceReport:
101
+ errors: list[ServiceError] = []
102
+ cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
103
+ results: list[WorkspacePerformance] = []
104
+ for wid in workspace_ids:
105
+ metrics = try_call(
106
+ errors,
107
+ "Amazon CloudWatch",
108
+ "GetMetricData",
109
+ lambda wid=wid: _fetch_metrics(
110
+ cloudwatch, wid, consts.WORKSPACES_PERFORMANCE_METRICS, lookback_hours
111
+ ),
112
+ default={},
113
+ )
114
+ results.append(
115
+ WorkspacePerformance(
116
+ workspace_id=wid,
117
+ lookback_hours=lookback_hours,
118
+ metrics=metrics or {},
119
+ note=None if metrics else "No performance datapoints (desktop may be stopped).",
120
+ )
121
+ )
122
+ return PerformanceReport(
123
+ region=region,
124
+ lookback_hours=lookback_hours,
125
+ workspaces=results,
126
+ errors=errors,
127
+ notes=[
128
+ "Metrics come from the native AWS/WorkSpaces namespace; CPUUsage/MemoryUsage are "
129
+ "percentages. Stopped desktops report no datapoints."
130
+ ],
131
+ )
132
+
133
+
134
+ def _rightsizing_recommendation(
135
+ workspace_id: str,
136
+ compute_type: str | None,
137
+ cpu: MetricStat | None,
138
+ mem: MetricStat | None,
139
+ lookback_days: int,
140
+ ) -> Recommendation | None:
141
+ if not compute_type:
142
+ return None
143
+ order = consts.WORKSPACES_COMPUTE_ORDER
144
+ if compute_type not in order:
145
+ return None # graphics / unknown family — not safe to auto-suggest
146
+ idx = order.index(compute_type)
147
+ if cpu is None or mem is None or cpu.peak is None or mem.peak is None:
148
+ return None # insufficient data
149
+ cpu_peak = cpu.peak
150
+ mem_peak = mem.peak
151
+
152
+ def stats() -> str:
153
+ return (
154
+ f"over {lookback_days}d peak CPU {cpu_peak:.0f}% / peak memory {mem_peak:.0f}% "
155
+ f"(avg CPU {cpu.average:.0f}% / mem {mem.average:.0f}%)"
156
+ )
157
+
158
+ if cpu_peak < _DOWNSIZE_PEAK_CPU and mem_peak < _DOWNSIZE_PEAK_MEM and idx > 0:
159
+ return Recommendation(
160
+ target_id=workspace_id,
161
+ kind="bundle_rightsizing",
162
+ current=compute_type,
163
+ recommended=order[idx - 1],
164
+ rationale=f"Consistently low utilization ({stats()}); a smaller compute type should "
165
+ "cope and cost less.",
166
+ confidence="high" if cpu_peak < _DOWNSIZE_PEAK_CPU / 2 else "medium",
167
+ )
168
+ if (cpu_peak > _UPSIZE_PEAK_CPU or mem_peak > _UPSIZE_PEAK_MEM) and idx < len(order) - 1:
169
+ return Recommendation(
170
+ target_id=workspace_id,
171
+ kind="bundle_rightsizing",
172
+ current=compute_type,
173
+ recommended=order[idx + 1],
174
+ rationale=f"Resource pressure ({stats()}); a larger compute type would improve the "
175
+ "user experience.",
176
+ confidence="high" if (cpu_peak > 95 or mem_peak > 95) else "medium",
177
+ )
178
+ return None
179
+
180
+
181
+ def recommend_bundle_rightsizing_core(
182
+ factory: ClientFactory, region: str | None, lookback_days: int = 7
183
+ ) -> RecommendationReport:
184
+ errors: list[ServiceError] = []
185
+ workspaces_client = factory.client(consts.WORKSPACES_API, region=region)
186
+ cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
187
+
188
+ workspaces = try_call(
189
+ errors,
190
+ consts.PRODUCT_WORKSPACES_PERSONAL,
191
+ "DescribeWorkspaces",
192
+ lambda: paginate(workspaces_client.describe_workspaces, "Workspaces"),
193
+ default=[],
194
+ )
195
+
196
+ lookback_hours = lookback_days * 24
197
+ recommendations: list[Recommendation] = []
198
+ skipped_no_data = 0
199
+ for ws in workspaces or []:
200
+ wid = ws.get("WorkspaceId", "")
201
+ props = ws.get("WorkspaceProperties", {})
202
+ compute_type = props.get("ComputeTypeName")
203
+ metrics = try_call(
204
+ errors,
205
+ "Amazon CloudWatch",
206
+ "GetMetricData",
207
+ lambda wid=wid: _fetch_metrics(
208
+ cloudwatch,
209
+ wid,
210
+ [("CPUUsage", "Percent"), ("MemoryUsage", "Percent")],
211
+ lookback_hours,
212
+ period=3600,
213
+ ),
214
+ default={},
215
+ )
216
+ cpu = (metrics or {}).get("CPUUsage")
217
+ mem = (metrics or {}).get("MemoryUsage")
218
+ datapoints_ok = cpu is not None and mem is not None
219
+ if not datapoints_ok:
220
+ skipped_no_data += 1
221
+ continue
222
+ rec = _rightsizing_recommendation(wid, compute_type, cpu, mem, lookback_days)
223
+ if rec:
224
+ # Best-effort $ estimate for AlwaysOn desktops: monthly compute-tier difference.
225
+ if props.get("RunningMode") == "ALWAYS_ON":
226
+ cur = pricing.get_workspace_prices(
227
+ factory,
228
+ region,
229
+ props.get("OperatingSystemName"),
230
+ rec.current,
231
+ props.get("RootVolumeSizeGib"),
232
+ props.get("UserVolumeSizeGib"),
233
+ )
234
+ new = pricing.get_workspace_prices(
235
+ factory,
236
+ region,
237
+ props.get("OperatingSystemName"),
238
+ rec.recommended,
239
+ props.get("RootVolumeSizeGib"),
240
+ props.get("UserVolumeSizeGib"),
241
+ )
242
+ if cur and new and cur.alwayson_monthly and new.alwayson_monthly:
243
+ diff = round(cur.alwayson_monthly - new.alwayson_monthly, 2)
244
+ rec.estimated_monthly_savings_usd = diff if diff > 0 else None
245
+ recommendations.append(rec)
246
+
247
+ notes = [
248
+ "Based on native AWS/WorkSpaces CPUUsage/MemoryUsage (window peak). "
249
+ "estimated_monthly_savings_usd is a best-effort AlwaysOn monthly compute-tier difference "
250
+ "(Price List, Included license); null for AutoStop desktops or unmatched prices.",
251
+ "Graphics compute families are excluded from automatic suggestions.",
252
+ ]
253
+ if skipped_no_data:
254
+ notes.append(
255
+ f"{skipped_no_data} desktop(s) skipped for insufficient metrics (likely stopped "
256
+ "for the whole window)."
257
+ )
258
+ return RecommendationReport(
259
+ region=region,
260
+ lookback_days=lookback_days,
261
+ recommendations=recommendations,
262
+ errors=errors,
263
+ notes=notes,
264
+ )
265
+
266
+
267
+ def _fetch_metric_series(
268
+ cloudwatch: Any,
269
+ namespace: str,
270
+ dimension_name: str,
271
+ dimension_value: str,
272
+ metric_specs: list[tuple[str, str]],
273
+ lookback_days: int,
274
+ period_hours: int,
275
+ ) -> dict[str, FleetMetricSeries]:
276
+ """Fetch Average + Maximum time-series for each metric and reduce to aggregates + series."""
277
+ end = datetime.now(UTC)
278
+ start = end - timedelta(days=lookback_days)
279
+ period = period_hours * 3600
280
+ stat_suffix = {"Average": "avg", "Maximum": "max"}
281
+ queries: list[dict[str, Any]] = []
282
+ for i, (name, _unit) in enumerate(metric_specs):
283
+ for stat in ("Average", "Maximum"):
284
+ queries.append(
285
+ {
286
+ "Id": f"s{i}_{stat_suffix[stat]}",
287
+ "MetricStat": {
288
+ "Metric": {
289
+ "Namespace": namespace,
290
+ "MetricName": name,
291
+ "Dimensions": [{"Name": dimension_name, "Value": dimension_value}],
292
+ },
293
+ "Period": period,
294
+ "Stat": stat,
295
+ },
296
+ "ReturnData": True,
297
+ }
298
+ )
299
+ response = cloudwatch.get_metric_data(
300
+ MetricDataQueries=queries,
301
+ StartTime=start,
302
+ EndTime=end,
303
+ ScanBy="TimestampAscending",
304
+ )
305
+ by_id = {r.get("Id"): r for r in response.get("MetricDataResults", [])}
306
+
307
+ out: dict[str, FleetMetricSeries] = {}
308
+ for i, (name, unit) in enumerate(metric_specs):
309
+ avg = by_id.get(f"s{i}_avg", {})
310
+ mx = by_id.get(f"s{i}_max", {})
311
+ avg_vals = avg.get("Values", [])
312
+ avg_ts = avg.get("Timestamps", [])
313
+ max_vals = mx.get("Values", [])
314
+ if not avg_vals and not max_vals:
315
+ continue
316
+ series = [
317
+ UsagePoint(
318
+ timestamp=str(avg_ts[j]) if j < len(avg_ts) else str(j),
319
+ average=avg_vals[j] if j < len(avg_vals) else None,
320
+ peak=max_vals[j] if j < len(max_vals) else None,
321
+ )
322
+ for j in range(max(len(avg_vals), len(max_vals)))
323
+ ]
324
+ out[name] = FleetMetricSeries(
325
+ unit=unit,
326
+ latest=avg_vals[-1] if avg_vals else None,
327
+ average=(sum(avg_vals) / len(avg_vals)) if avg_vals else None,
328
+ peak=max(max_vals) if max_vals else None,
329
+ series=series,
330
+ )
331
+ return out
332
+
333
+
334
+ def _fetch_fleet_usage(
335
+ cloudwatch: Any, fleet_name: str, lookback_days: int, period_hours: int
336
+ ) -> dict[str, FleetMetricSeries]:
337
+ return _fetch_metric_series(
338
+ cloudwatch,
339
+ "AWS/AppStream",
340
+ "Fleet",
341
+ fleet_name,
342
+ consts.APPSTREAM_FLEET_METRICS,
343
+ lookback_days,
344
+ period_hours,
345
+ )
346
+
347
+
348
+ def _summarize_fleet_usage(metrics: dict[str, FleetMetricSeries], lookback_days: int) -> str | None:
349
+ if not metrics:
350
+ return "No usage datapoints — the fleet was likely stopped for the whole window."
351
+ in_use = metrics.get("InUseCapacity")
352
+ running = metrics.get("RunningCapacity") or metrics.get("ActualCapacity")
353
+ util = metrics.get("CapacityUtilization")
354
+ if in_use and (in_use.peak or 0) == 0 and running and (running.peak or 0) > 0:
355
+ return (
356
+ f"Zero sessions in use across {lookback_days}d, yet up to {running.peak:.0f} "
357
+ "instance(s) were kept running — idle running capacity (cost with no usage). Consider "
358
+ "stopping the fleet or lowering desired capacity."
359
+ )
360
+ if in_use and in_use.peak is not None:
361
+ util_txt = f"; peak utilization {util.peak:.0f}%" if util and util.peak is not None else ""
362
+ return f"Peak {in_use.peak:.0f} instance(s) in use over {lookback_days}d{util_txt}."
363
+ return None
364
+
365
+
366
+ def get_application_fleet_usage_core(
367
+ factory: ClientFactory,
368
+ fleet_name: str,
369
+ region: str | None,
370
+ lookback_days: int = 7,
371
+ period_hours: int = 24,
372
+ ) -> FleetUsage:
373
+ errors: list[ServiceError] = []
374
+ cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
375
+ metrics = try_call(
376
+ errors,
377
+ "Amazon CloudWatch",
378
+ "GetMetricData",
379
+ lambda: _fetch_fleet_usage(cloudwatch, fleet_name, lookback_days, period_hours),
380
+ default={},
381
+ )
382
+ return FleetUsage(
383
+ fleet_name=fleet_name,
384
+ lookback_days=lookback_days,
385
+ period_hours=period_hours,
386
+ metrics=metrics or {},
387
+ summary=_summarize_fleet_usage(metrics or {}, lookback_days),
388
+ errors=errors,
389
+ )
390
+
391
+
392
+ def _count_active_buckets(series: list[UsagePoint]) -> int:
393
+ return sum(1 for p in series if (p.peak or 0) >= 1)
394
+
395
+
396
+ def _summarize_connection_history(metrics: dict[str, FleetMetricSeries], lookback_days: int) -> str:
397
+ connected = metrics.get("UserConnected")
398
+ if not connected or not connected.series:
399
+ return f"No connection datapoints in {lookback_days}d (desktop may be stopped/unused)."
400
+ active = _count_active_buckets(connected.series)
401
+ total = len(connected.series)
402
+ if active == 0:
403
+ return f"No user connections in any of the {total} buckets over {lookback_days}d (unused)."
404
+ failures = metrics.get("ConnectionFailure")
405
+ fail_txt = ""
406
+ if failures and (failures.peak or 0) > 0:
407
+ fail_txt = " Connection failures were recorded — check connectivity."
408
+ return f"Connected in {active} of {total} buckets over {lookback_days}d.{fail_txt}"
409
+
410
+
411
+ def get_workspace_connection_history_core(
412
+ factory: ClientFactory,
413
+ workspace_id: str,
414
+ region: str | None,
415
+ lookback_days: int = 7,
416
+ period_hours: int = 24,
417
+ ) -> UsageHistory:
418
+ errors: list[ServiceError] = []
419
+ cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
420
+ metrics = try_call(
421
+ errors,
422
+ "Amazon CloudWatch",
423
+ "GetMetricData",
424
+ lambda: _fetch_metric_series(
425
+ cloudwatch,
426
+ "AWS/WorkSpaces",
427
+ "WorkspaceId",
428
+ workspace_id,
429
+ consts.WORKSPACES_CONNECTION_METRICS,
430
+ lookback_days,
431
+ period_hours,
432
+ ),
433
+ default={},
434
+ )
435
+ return UsageHistory(
436
+ target_type=consts.PRODUCT_WORKSPACES_PERSONAL,
437
+ target_id=workspace_id,
438
+ lookback_days=lookback_days,
439
+ period_hours=period_hours,
440
+ metrics=metrics or {},
441
+ summary=_summarize_connection_history(metrics or {}, lookback_days),
442
+ errors=errors,
443
+ )
444
+
445
+
446
+ def _summarize_pool_session_history(
447
+ metrics: dict[str, FleetMetricSeries], lookback_days: int
448
+ ) -> str | None:
449
+ if not metrics:
450
+ return f"No session datapoints in {lookback_days}d (pool may be stopped)."
451
+ active = metrics.get("ActiveUserSessionCapacity")
452
+ util = metrics.get("UserSessionsCapacityUtilization")
453
+ actual = metrics.get("ActualUserSessionCapacity")
454
+ if active and (active.peak or 0) == 0 and actual and (actual.peak or 0) > 0:
455
+ return (
456
+ f"Zero active sessions across {lookback_days}d, yet up to {actual.peak:.0f} session "
457
+ "slot(s) were kept available — idle pool capacity (cost with no usage)."
458
+ )
459
+ if active and active.peak is not None:
460
+ util_txt = f"; peak utilization {util.peak:.0f}%" if util and util.peak is not None else ""
461
+ return f"Peak {active.peak:.0f} active session(s) over {lookback_days}d{util_txt}."
462
+ return None
463
+
464
+
465
+ def get_pool_session_history_core(
466
+ factory: ClientFactory,
467
+ pool_id: str,
468
+ region: str | None,
469
+ lookback_days: int = 7,
470
+ period_hours: int = 24,
471
+ ) -> UsageHistory:
472
+ errors: list[ServiceError] = []
473
+ cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
474
+ metrics = try_call(
475
+ errors,
476
+ "Amazon CloudWatch",
477
+ "GetMetricData",
478
+ lambda: _fetch_metric_series(
479
+ cloudwatch,
480
+ "AWS/WorkSpaces",
481
+ consts.WORKSPACES_POOL_DIMENSION,
482
+ pool_id,
483
+ consts.WORKSPACES_POOL_SESSION_METRICS,
484
+ lookback_days,
485
+ period_hours,
486
+ ),
487
+ default={},
488
+ )
489
+ return UsageHistory(
490
+ target_type=consts.PRODUCT_WORKSPACES_POOLS,
491
+ target_id=pool_id,
492
+ lookback_days=lookback_days,
493
+ period_hours=period_hours,
494
+ metrics=metrics or {},
495
+ summary=_summarize_pool_session_history(metrics or {}, lookback_days),
496
+ errors=errors,
497
+ )
498
+
499
+
500
+ def register(mcp: Any, factory: ClientFactory) -> None:
501
+ """Register performance & right-sizing tools on the FastMCP app."""
502
+
503
+ async def get_workspace_performance(
504
+ workspace_ids: list[str], region: str | None = None, lookback_hours: int = 3
505
+ ) -> dict[str, Any]:
506
+ """Get native CPU/memory/disk/GPU/latency performance metrics for WorkSpaces Personal.
507
+
508
+ Reads the AWS/WorkSpaces namespace (no CloudWatch agent needed) and returns latest, window
509
+ average, and window peak for each metric per desktop. Stopped desktops report no data.
510
+
511
+ Args:
512
+ workspace_ids: One or more WorkSpace IDs.
513
+ region: AWS region. Defaults to the server's configured region.
514
+ lookback_hours: Window for the metrics (default 3).
515
+ """
516
+ report = get_workspace_performance_core(
517
+ factory, workspace_ids, region or factory.region, lookback_hours
518
+ )
519
+ return report.model_dump()
520
+
521
+ async def recommend_bundle_rightsizing(
522
+ region: str | None = None, lookback_days: int = 7
523
+ ) -> dict[str, Any]:
524
+ """Recommend smaller/larger WorkSpace compute types from CPU & memory headroom.
525
+
526
+ Uses native AWS/WorkSpaces CPUUsage/MemoryUsage over the window: flags desktops with ample
527
+ headroom to downsize and pressured ones to upsize (general compute families only;
528
+ graphics excluded). Read-only.
529
+
530
+ Args:
531
+ region: AWS region. Defaults to the server's configured region.
532
+ lookback_days: Window for the analysis (default 7).
533
+ """
534
+ report = recommend_bundle_rightsizing_core(factory, region or factory.region, lookback_days)
535
+ return report.model_dump()
536
+
537
+ async def get_application_fleet_usage(
538
+ fleet_name: str,
539
+ region: str | None = None,
540
+ lookback_days: int = 7,
541
+ period_hours: int = 24,
542
+ ) -> dict[str, Any]:
543
+ """Get a WorkSpaces Applications (formerly AppStream 2.0) fleet's usage history.
544
+
545
+ Returns the AWS/AppStream capacity time-series for the fleet (InUseCapacity,
546
+ CapacityUtilization, Running/Available/Actual/Desired/PendingCapacity) over the window, as
547
+ per-bucket points plus latest/average/peak, with a plain-language summary (e.g. flags idle
548
+ running capacity). For a stack, resolve its fleet first (see generate_inventory_report).
549
+ Read-only.
550
+
551
+ Args:
552
+ fleet_name: The fleet name (the fleet behind the stack, not the stack name).
553
+ region: AWS region. Defaults to the server's configured region.
554
+ lookback_days: Window length (default 7).
555
+ period_hours: Time-bucket size in hours (default 24 = daily; use 1 for hourly).
556
+ """
557
+ usage = get_application_fleet_usage_core(
558
+ factory, fleet_name, region or factory.region, lookback_days, period_hours
559
+ )
560
+ return usage.model_dump()
561
+
562
+ async def get_workspace_connection_history(
563
+ workspace_id: str,
564
+ region: str | None = None,
565
+ lookback_days: int = 7,
566
+ period_hours: int = 24,
567
+ ) -> dict[str, Any]:
568
+ """Get a WorkSpaces Personal desktop's connection/session history.
569
+
570
+ Returns the AWS/WorkSpaces connection time-series for the desktop (UserConnected, and —
571
+ when present — ConnectionAttempt/Success/Failure, SessionLaunchTime, InSessionLatency) as
572
+ per-bucket points plus latest/average/peak, with a plain-language summary. Read-only.
573
+
574
+ Args:
575
+ workspace_id: The WorkSpace ID (ws-...).
576
+ region: AWS region. Defaults to the server's configured region.
577
+ lookback_days: Window length (default 7).
578
+ period_hours: Bucket size in hours (default 24 = daily; use 1 for hourly).
579
+ """
580
+ usage = get_workspace_connection_history_core(
581
+ factory, workspace_id, region or factory.region, lookback_days, period_hours
582
+ )
583
+ return usage.model_dump()
584
+
585
+ async def get_pool_session_history(
586
+ pool_id: str,
587
+ region: str | None = None,
588
+ lookback_days: int = 7,
589
+ period_hours: int = 24,
590
+ ) -> dict[str, Any]:
591
+ """Get a WorkSpaces Pool's user-session history.
592
+
593
+ Returns the AWS/WorkSpaces session-capacity time-series for the pool
594
+ (Active/Actual/Available/Desired/PendingUserSessionCapacity and
595
+ UserSessionsCapacityUtilization) as per-bucket points plus latest/average/peak, with a
596
+ plain-language summary that flags idle pool capacity. Read-only.
597
+
598
+ Args:
599
+ pool_id: The WorkSpaces Pool ID (wspool-...).
600
+ region: AWS region. Defaults to the server's configured region.
601
+ lookback_days: Window length (default 7).
602
+ period_hours: Bucket size in hours (default 24 = daily; use 1 for hourly).
603
+ """
604
+ usage = get_pool_session_history_core(
605
+ factory, pool_id, region or factory.region, lookback_days, period_hours
606
+ )
607
+ return usage.model_dump()
608
+
609
+ mcp.add_tool(get_workspace_performance, annotations=read_only("WorkSpace performance"))
610
+ mcp.add_tool(
611
+ recommend_bundle_rightsizing, annotations=read_only("Recommend bundle right-sizing")
612
+ )
613
+ mcp.add_tool(
614
+ get_application_fleet_usage, annotations=read_only("Applications fleet usage history")
615
+ )
616
+ mcp.add_tool(
617
+ get_workspace_connection_history,
618
+ annotations=read_only("WorkSpace connection history"),
619
+ )
620
+ mcp.add_tool(get_pool_session_history, annotations=read_only("Pool session history"))