workspaces-euc-mcp-server 0.1.2__tar.gz → 0.1.3__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.2 → workspaces_euc_mcp_server-0.1.3}/CHANGELOG.md +21 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/DESIGN.md +51 -35
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/PKG-INFO +147 -15
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/README.md +146 -14
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/pyproject.toml +1 -1
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_cost.py +101 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/__init__.py +1 -1
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/consts.py +10 -8
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/cost.py +81 -37
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.dockerignore +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.github/workflows/ci.yml +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.github/workflows/docker-publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.github/workflows/publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.gitignore +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.pre-commit-config.yaml +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/Dockerfile +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/LICENSE +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/README.md +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier0-diagnostics.json +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier1-cost.json +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier2-lifecycle.json +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier3-destructive.json +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/scripts/smoke_readonly.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_diagnostics.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_naming.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_no_embedded_secrets.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_reporting.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_secure_browser.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/models.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/server.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/_common.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/diagnostics.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
- {workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/workspaces_euc_mcp_server/tools/secure_browser.py +0 -0
|
@@ -5,6 +5,27 @@ All notable changes to this project are documented here. The format is based on
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.3] - 2026-06-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `get_euc_cost_summary` no longer silently drops spend for services whose Cost Explorer `SERVICE`
|
|
12
|
+
name isn't an exact match to a hardcoded list. In real accounts this **hid all WorkSpaces
|
|
13
|
+
Applications spend**, which bills under the name `Amazon WorkSpaces Applications` (the AppStream
|
|
14
|
+
rebrand) rather than `Amazon AppStream`. EUC services are now selected by keyword
|
|
15
|
+
(`workspaces` / `appstream`) against every service in the period, with `Amazon WorkSpaces Thin
|
|
16
|
+
Client` (out of scope) explicitly excluded, and results are paginated.
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- `get_euc_cost_summary` accepts optional `start_date` / `end_date` (YYYY-MM-DD, end exclusive) to
|
|
20
|
+
total an exact calendar month instead of only a rolling `lookback_days` window.
|
|
21
|
+
|
|
22
|
+
### Docs
|
|
23
|
+
- README: a "not an official AWS product" disclaimer at the top; an **Amazon Quick (Desktop)**
|
|
24
|
+
client example; an **AWS authentication** section (SSO login + auto-refresh; console sign-in does
|
|
25
|
+
not produce the on-disk token); and an explicit four-gate **write/destructive safety** section
|
|
26
|
+
noting that the launch flag grants no AWS access (IAM is still required).
|
|
27
|
+
- DESIGN.md reconciled with the shipped code.
|
|
28
|
+
|
|
8
29
|
## [0.1.2] - 2026-06-01
|
|
9
30
|
|
|
10
31
|
### Added
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
> inventory, troubleshooting, cost/utilization optimization, and guarded lifecycle management
|
|
5
5
|
> across the Amazon WorkSpaces family of End User Computing services.
|
|
6
6
|
>
|
|
7
|
-
> **Shipped:**
|
|
7
|
+
> **Shipped:** 18 read-only tools (Tier 0/1) + 10 guarded write tools (Tier 2) + 3 destructive
|
|
8
8
|
> tools (Tier 3), all behind opt-in flags with dry-run/confirm/blast-radius/typed-acknowledgement
|
|
9
|
-
> guards. All four IAM policy tiers ship in `iam/`. CI (ruff/format/bandit/pytest on
|
|
10
|
-
> is green.
|
|
11
|
-
> **
|
|
12
|
-
>
|
|
9
|
+
> guards. All four IAM policy tiers ship in `iam/`. CI (ruff/format/pyright/bandit/pytest on
|
|
10
|
+
> Py 3.11–3.13) is green; published to PyPI and GHCR. **`README.md` is the source of truth for the
|
|
11
|
+
> live tool catalog and exact tool names** — §5 below records the original design intent and is
|
|
12
|
+
> kept reconciled with what shipped. See `CHANGELOG.md` for per-version detail.
|
|
13
13
|
|
|
14
14
|
## 1. Principles
|
|
15
15
|
|
|
@@ -55,26 +55,33 @@
|
|
|
55
55
|
- **Observability:** Loguru with env-controlled log level (`FASTMCP_LOG_LEVEL`). Per-signal errors
|
|
56
56
|
are returned in the structured result payload (deliberate: each tool makes many AWS calls and
|
|
57
57
|
synthesizes one result), rather than `ctx.error`.
|
|
58
|
-
- **Repo layout
|
|
58
|
+
- **Repo layout** (independent package — **not** under an `awslabs/` namespace):
|
|
59
59
|
```
|
|
60
|
-
|
|
60
|
+
workspaces_euc_mcp_server/
|
|
61
61
|
__init__.py # version
|
|
62
|
-
server.py # FastMCP app + tool registration
|
|
63
|
-
consts.py # service/API constants, region maps
|
|
62
|
+
server.py # FastMCP app + tool registration + main()/argparse
|
|
63
|
+
consts.py # service/API constants, region→pricing-location maps
|
|
64
64
|
models.py # Pydantic request/response models
|
|
65
|
-
clients.py # boto3 client factory (region/profile aware)
|
|
65
|
+
clients.py # boto3 client factory (region/profile/assume-role aware)
|
|
66
66
|
tools/
|
|
67
|
+
_common.py # read_only/writes annotation helpers, try_call, paginate
|
|
67
68
|
inventory.py
|
|
68
69
|
diagnostics.py
|
|
69
70
|
cost.py
|
|
71
|
+
performance.py # perf + usage/history tools (native CloudWatch metrics)
|
|
70
72
|
reporting.py
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
secure_browser.py
|
|
74
|
+
pricing.py # AWS Price List lookups for $ estimates
|
|
75
|
+
lifecycle.py # Tier 2 (guarded writes)
|
|
76
|
+
destructive.py # Tier 3 (terminate/rebuild/restore)
|
|
77
|
+
iam/ # shippable least-privilege policy docs per tier (0–3)
|
|
78
|
+
tests/ # pytest + pytest-asyncio (no live AWS; calls are stubbed/monkeypatched)
|
|
79
|
+
Dockerfile, .dockerignore
|
|
74
80
|
pyproject.toml, .pre-commit-config.yaml, README.md, CHANGELOG.md, LICENSE
|
|
75
81
|
```
|
|
76
|
-
- **Quality gates:** ruff (format/lint), pyright (
|
|
77
|
-
pre-commit, Apache-2.0 headers.
|
|
82
|
+
- **Quality gates:** ruff (format/lint), pyright (basic type-checking), bandit (security),
|
|
83
|
+
pre-commit (incl. `detect-private-key`), Apache-2.0 headers. Tests stub the boto3 layer rather
|
|
84
|
+
than hitting AWS.
|
|
78
85
|
|
|
79
86
|
## 4. Configuration / safety flags
|
|
80
87
|
|
|
@@ -91,50 +98,59 @@ and return a synthesized result, not raw API passthroughs.
|
|
|
91
98
|
|
|
92
99
|
### Phase 1 — Read / Diagnose / Optimize (read-only, Tiers 0–1)
|
|
93
100
|
|
|
101
|
+
> Tool names below are the **shipped** names. Where the original plan used a different working
|
|
102
|
+
> name, the shipped name is what appears here.
|
|
103
|
+
|
|
94
104
|
**Inventory & discovery**
|
|
95
105
|
| Tool | Purpose | IAM actions |
|
|
96
106
|
|---|---|---|
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `list_application_fleets` | WorkSpaces Applications fleets/stacks/associations | `appstream:DescribeFleets`, `appstream:DescribeStacks`, `appstream:DescribeFleetAssociations` |
|
|
100
|
-
| `list_secure_browser_portals` | Secure Browser portals + settings | `workspaces-web:ListPortals`, `workspaces-web:GetPortal`, `workspaces-web:List*Settings` |
|
|
101
|
-
| `get_euc_inventory_summary` | Cross-service rollup (counts, states, regions) | union of the above describes |
|
|
107
|
+
| `get_euc_inventory_summary` | Cross-service rollup (counts, states, regions) across Personal, Pools, Applications, Secure Browser, Core | union of the in-scope describes |
|
|
108
|
+
| `generate_inventory_report` | Structured per-resource inventory across all in-scope services | Phase-1 describes |
|
|
102
109
|
|
|
103
110
|
**Troubleshooting & triage** (the flagship value)
|
|
104
111
|
| Tool | Purpose | IAM actions |
|
|
105
112
|
|---|---|---|
|
|
106
113
|
| `diagnose_workspace_connectivity` | Correlate a Personal WorkSpace's state + connection status + directory health + CloudWatch into a root-cause narrative | `workspaces:Describe*`, `ds:DescribeDirectories`, `cloudwatch:GetMetricData` |
|
|
107
|
-
| `
|
|
108
|
-
| `diagnose_application_fleet` | Fleet state, capacity, scaling activity, fleet errors | `appstream:DescribeFleets`, `appstream:
|
|
109
|
-
| `check_directory_health` | Shared dependency: directory reachability/registration | `ds:DescribeDirectories`, `workspaces:DescribeWorkspaceDirectories` |
|
|
114
|
+
| `diagnose_pool` | Why a Pool is unhealthy/queued — capacity status, sessions, errors, scaling | `workspaces:DescribeWorkspacesPool*`, `cloudwatch:GetMetricData` |
|
|
115
|
+
| `diagnose_application_fleet` | Fleet state, capacity, scaling activity, fleet errors | `appstream:DescribeFleets`, `appstream:ListAssociatedStacks`, `cloudwatch:GetMetricData`, `application-autoscaling:DescribeScalingActivities` |
|
|
116
|
+
| `check_directory_health` | Shared dependency: directory reachability/registration (skips `ds` for WorkSpaces-managed `wsd-` directories) | `ds:DescribeDirectories`, `workspaces:DescribeWorkspaceDirectories` |
|
|
110
117
|
|
|
111
|
-
**Cost &
|
|
118
|
+
**Cost, utilization & performance**
|
|
112
119
|
| Tool | Purpose | IAM actions |
|
|
113
120
|
|---|---|---|
|
|
114
121
|
| `analyze_workspace_utilization` | Find idle/unused Personal WorkSpaces from connection metrics | `workspaces:DescribeWorkspaces*`, `cloudwatch:GetMetricData` |
|
|
115
|
-
| `recommend_running_mode` | AlwaysOn → AutoStop candidates with $ estimate | `workspaces:DescribeWorkspaces`, `cloudwatch:GetMetricData`, `pricing:GetProducts` |
|
|
116
|
-
| `recommend_bundle_rightsizing` | Over/under-sized bundles from CPU/mem metrics | `workspaces:DescribeWorkspaces`, `workspaces:DescribeWorkspaceBundles`, `cloudwatch:GetMetricData` |
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
122
|
+
| `recommend_running_mode` | AlwaysOn → AutoStop candidates with a $ estimate | `workspaces:DescribeWorkspaces`, `cloudwatch:GetMetricData`, `pricing:GetProducts` |
|
|
123
|
+
| `recommend_bundle_rightsizing` | Over/under-sized bundles from native CPU/mem metrics (see §10) | `workspaces:DescribeWorkspaces`, `workspaces:DescribeWorkspaceBundles`, `cloudwatch:GetMetricData` |
|
|
124
|
+
| `get_workspace_performance` | Per-WorkSpace CPU/mem/GPU/FPS/latency from native `AWS/WorkSpaces` metrics | `workspaces:DescribeWorkspaces`, `cloudwatch:GetMetricData` |
|
|
125
|
+
| `get_workspace_connection_history` | Per-WorkSpace connection timeline | `workspaces:DescribeWorkspaces*`, `cloudwatch:GetMetricData` |
|
|
126
|
+
| `get_pool_session_history` | Pool session/capacity history | `workspaces:DescribeWorkspacesPool*`, `cloudwatch:GetMetricData` |
|
|
127
|
+
| `get_application_fleet_usage` | Applications fleet utilization vs capacity | `appstream:DescribeFleets`, `cloudwatch:GetMetricData` |
|
|
119
128
|
| `get_euc_cost_summary` | Spend rollup filtered to EUC services | `ce:GetCostAndUsage`, `ce:GetDimensionValues` |
|
|
120
129
|
|
|
130
|
+
**Secure Browser**
|
|
131
|
+
| Tool | Purpose | IAM actions |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| `get_secure_browser_portal_details` | Portal config + associated settings (browser/network/user/IP-access) | `workspaces-web:GetPortal`, `workspaces-web:List*`, `workspaces-web:Get*Settings` |
|
|
134
|
+
| `get_secure_browser_portal_usage` | Portal session/usage metrics | `workspaces-web:ListPortals`, `cloudwatch:GetMetricData` |
|
|
135
|
+
|
|
121
136
|
**Reporting & audit**
|
|
122
137
|
| Tool | Purpose | IAM actions |
|
|
123
138
|
|---|---|---|
|
|
124
|
-
| `generate_inventory_report` | Structured inventory across all in-scope services | Phase-1 describes |
|
|
125
139
|
| `audit_security_posture` | Encryption at rest, IP access control groups, directory config, portal policies | `workspaces:Describe*`, `workspaces-web:Get*/List*`, `appstream:Describe*` |
|
|
126
|
-
| `list_unused_resources` | Idle desktops / empty fleets / orphaned
|
|
140
|
+
| `list_unused_resources` | Idle desktops / empty fleets / orphaned resources | describes + `cloudwatch:GetMetricData` |
|
|
127
141
|
|
|
128
142
|
### Phase 2 — Guarded lifecycle (writes; Tier 2, `--enable-writes`)
|
|
129
143
|
All support `dry_run`, return a plan + blast-radius before acting, and honor `--max-bulk-targets`.
|
|
130
144
|
|
|
131
145
|
| Tool | Purpose | IAM actions |
|
|
132
146
|
|---|---|---|
|
|
133
|
-
| `start_workspaces` / `stop_workspaces` / `reboot_workspaces` |
|
|
147
|
+
| `start_workspaces` / `stop_workspaces` / `reboot_workspaces` | Personal power ops | `workspaces:Start/Stop/RebootWorkspaces` |
|
|
134
148
|
| `modify_workspace_running_mode` | Apply AutoStop/AlwaysOn recommendation | `workspaces:ModifyWorkspaceProperties` |
|
|
135
|
-
| `
|
|
136
|
-
| `
|
|
137
|
-
|
|
149
|
+
| `start_workspaces_pool` / `stop_workspaces_pool` / `update_workspaces_pool_capacity` | Pool power + resize | `workspaces:Start/Stop/UpdateWorkspacesPool` |
|
|
150
|
+
| `start_application_fleet` / `stop_application_fleet` / `update_application_fleet_capacity` | Applications fleet ops | `appstream:Start/Stop/UpdateFleet` |
|
|
151
|
+
|
|
152
|
+
> Note: bundle right-sizing is **recommend-only** (`recommend_bundle_rightsizing`); there is no
|
|
153
|
+
> `modify_workspace_compute_type` write tool — applying a compute change is left to the operator.
|
|
138
154
|
|
|
139
155
|
### Phase 3 — Destructive (Tier 3, `--enable-destructive`, hardest gating)
|
|
140
156
|
| Tool | Purpose | IAM actions |
|
|
@@ -177,7 +193,7 @@ human operator** (the pattern the AWS MCP Server uses).
|
|
|
177
193
|
## 8. Phased roadmap
|
|
178
194
|
|
|
179
195
|
- **Phase 0 — Scaffold:** repo per awslabs layout, client factory, auth, `--readonly` flag,
|
|
180
|
-
one end-to-end tool (`get_euc_inventory_summary`), tests
|
|
196
|
+
one end-to-end tool (`get_euc_inventory_summary`), tests (stubbed boto3), CI/pre-commit. Ship to
|
|
181
197
|
internal users.
|
|
182
198
|
- **Phase 1 — Read/Diagnose/Optimize:** full inventory + diagnostics + cost tools (Tiers 0–1).
|
|
183
199
|
This is the demonstrable-value milestone.
|
|
@@ -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.3
|
|
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
|
|
@@ -32,6 +32,14 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
[](https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp/actions/workflows/ci.yml)
|
|
34
34
|
|
|
35
|
+
> **Disclaimer:** This project is not an official AWS product, service, or solution, and is not
|
|
36
|
+
> affiliated with or endorsed by Amazon Web Services. It is an independent, community example
|
|
37
|
+
> implementation intended to demonstrate what is possible when combining the Amazon WorkSpaces End
|
|
38
|
+
> User Computing APIs with the Model Context Protocol. It is provided as a starting point to learn
|
|
39
|
+
> from, adapt, and iterate on — not as a supported or production-certified offering. Use it at your
|
|
40
|
+
> own discretion and always validate it against your organisation's security, compliance, and
|
|
41
|
+
> operational requirements before deploying to production.
|
|
42
|
+
|
|
35
43
|
An [MCP](https://modelcontextprotocol.io) server that gives administrators AI-assisted
|
|
36
44
|
**inventory, troubleshooting, and cost/utilization optimization** across the Amazon WorkSpaces
|
|
37
45
|
End User Computing (EUC) portfolio:
|
|
@@ -78,7 +86,7 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
|
|
|
78
86
|
| `get_workspace_connection_history` | A desktop's connection/session **history** (UserConnected + connection attempts/failures) over a window, with a summary (Tier 0). |
|
|
79
87
|
| `get_pool_session_history` | A WorkSpaces Pool's user-**session history** (active/available/utilization capacity time-series), flags idle pool capacity (Tier 0). |
|
|
80
88
|
| `recommend_bundle_rightsizing` | Suggests smaller/larger compute types from CPU & memory headroom (general families; graphics excluded) (Tier 0). |
|
|
81
|
-
| `get_euc_cost_summary` | EUC spend by service over a window via Cost Explorer, account-wide (Tier 1). |
|
|
89
|
+
| `get_euc_cost_summary` | EUC spend by service over a window (or an explicit `start_date`/`end_date` calendar month) via Cost Explorer, account-wide. Services are matched by keyword so naming variants (e.g. AppStream 2.0) aren't dropped; note Cost Explorer bills WorkSpaces Personal/Pools/Core together as one "Amazon WorkSpaces" line (Tier 1). |
|
|
82
90
|
| `generate_inventory_report` | Detailed per-resource inventory (desktops **with assigned user / computer name / IP**, pools, fleets, **stacks + their associated fleets**, portals) with key attributes (Tier 0). |
|
|
83
91
|
| `audit_security_posture` | Cross-service: flags unencrypted WorkSpace volumes, directories without IP access control groups, and **Secure Browser portals / Applications stacks that allow data egress** (clipboard/download/print) (Tier 0). |
|
|
84
92
|
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (Tier 0). |
|
|
@@ -201,6 +209,140 @@ Running from a source checkout instead of `uvx`:
|
|
|
201
209
|
}
|
|
202
210
|
```
|
|
203
211
|
|
|
212
|
+
The two blocks above are the standard `mcpServers` shape used by most clients (Claude Desktop,
|
|
213
|
+
Cursor, etc.). Some clients use a **form** instead of raw JSON — see the example below.
|
|
214
|
+
|
|
215
|
+
### Example: Amazon Quick (Desktop)
|
|
216
|
+
|
|
217
|
+
Amazon Quick's **Capabilities → MCP → Add MCP** uses a form rather than JSON. Choose connection
|
|
218
|
+
type **Local** ("Run a command on your machine") and fill in:
|
|
219
|
+
|
|
220
|
+
| Field | Value |
|
|
221
|
+
|-------|-------|
|
|
222
|
+
| **Name** | `WorkSpaces EUC` |
|
|
223
|
+
| **Command** | `uvx` |
|
|
224
|
+
| **Arguments** | `workspaces-euc-mcp-server@latest --region us-east-1` |
|
|
225
|
+
| **Description** | `Admin tools for Amazon WorkSpaces EUC — inventory, troubleshooting, cost/utilization. Read-only.` |
|
|
226
|
+
| **Environment variables** | `AWS_PROFILE` = `your-euc-admin-profile` · `AWS_REGION` = `us-east-1` |
|
|
227
|
+
| **Timeout (seconds)** | `60` |
|
|
228
|
+
|
|
229
|
+
Notes:
|
|
230
|
+
- Set `--region` (in Arguments) and `AWS_REGION` to where your WorkSpaces actually live; keep the
|
|
231
|
+
two in sync.
|
|
232
|
+
- **Bump the timeout from the default 30 → 60–120 for the first run** — `uvx` downloads the package
|
|
233
|
+
on first launch, which can exceed 30 s and look like a failed connection. Later starts are fast.
|
|
234
|
+
- If `uvx` isn't on your `PATH`, use Command `workspaces-euc-mcp-server` (after
|
|
235
|
+
`pip install workspaces-euc-mcp-server`) with Arguments `--region us-east-1`, or Command `python`
|
|
236
|
+
with Arguments `-m workspaces_euc_mcp_server.server --region us-east-1`.
|
|
237
|
+
- To enable writes/destructive, append the flags to **Arguments** (e.g.
|
|
238
|
+
`… --enable-writes --max-bulk-targets 10`) — and attach the matching IAM tier (see
|
|
239
|
+
[the safety gates](#enabling-write--destructive-tools--the-safety-gates)).
|
|
240
|
+
- A local MCP server has **no "Sign in" button** (that's a Quick *Connections* feature). It uses
|
|
241
|
+
your AWS credential chain, so authenticate first — e.g. `aws sso login --profile your-profile` for
|
|
242
|
+
SSO — then the server connects. See [AWS authentication](#aws-authentication).
|
|
243
|
+
|
|
244
|
+
## AWS authentication
|
|
245
|
+
|
|
246
|
+
The server uses the **standard AWS credential chain** — it does not handle sign-in itself. Provide
|
|
247
|
+
credentials however boto3 expects them:
|
|
248
|
+
|
|
249
|
+
- **IAM Identity Center (SSO):** configure an `sso-session` profile, then `aws sso login --profile
|
|
250
|
+
<name>`. With the modern `sso-session` format, botocore **auto-refreshes** the token for the
|
|
251
|
+
session window, so you log in once rather than every few hours. (Signing into the AWS Console or
|
|
252
|
+
access portal in a browser does **not** create the on-disk token the server needs — only
|
|
253
|
+
`aws sso login` does.)
|
|
254
|
+
- **Static / temporary keys:** a named profile in `~/.aws/credentials`, or `AWS_ACCESS_KEY_ID` /
|
|
255
|
+
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` env vars.
|
|
256
|
+
- **Assumed role / multi-account:** see [Multi-account / MSP](#multi-account--msp).
|
|
257
|
+
|
|
258
|
+
If calls fail with an expired-token / `UnauthorizedException` error, your session has lapsed —
|
|
259
|
+
re-authenticate (for SSO, `aws sso login --profile <name>`) and retry. Nothing in the MCP config
|
|
260
|
+
changes.
|
|
261
|
+
|
|
262
|
+
## Enabling write / destructive tools — the safety gates
|
|
263
|
+
|
|
264
|
+
The config above is **read-only**: the write and destructive tools are not even registered, so
|
|
265
|
+
they cannot be called no matter what the model or the IAM policy allows. Enabling them is a
|
|
266
|
+
deliberate act, and **IAM permissions alone are not enough**. A destructive call (e.g.
|
|
267
|
+
`terminate_workspaces`) only runs after clearing **all four** of these independent gates:
|
|
268
|
+
|
|
269
|
+
| # | Gate | Where it lives | What it does |
|
|
270
|
+
|---|------|----------------|--------------|
|
|
271
|
+
| 1 | **Launch flag** | The `args` in your MCP-client config | Destructive tools aren't registered unless the server was started with **both** `--enable-writes` **and** `--enable-destructive`. Default launch = read-only. |
|
|
272
|
+
| 2 | **IAM tier** | The AWS profile / assumed role | The credentials must carry the matching tier ([`iam/tier2-lifecycle.json`](iam/tier2-lifecycle.json) for writes, [`iam/tier3-destructive.json`](iam/tier3-destructive.json) for destructive). Without it, AWS denies the call. |
|
|
273
|
+
| 3 | **`confirm=true`** | The tool call | Every mutation is **dry-run by default** — it returns a plan and changes nothing. You must explicitly pass `confirm=true`. |
|
|
274
|
+
| 4 | **Typed acknowledgement + blast-radius cap** | The tool call | Destructive ops also require the **exact** phrase (`acknowledge="TERMINATE"` / `"REBUILD"` / `"RESTORE"`) and must stay within `--max-bulk-targets`. Wrong phrase or too many targets → refused, nothing changes. |
|
|
275
|
+
|
|
276
|
+
> **The launch flag grants no AWS permission.** Gate 1 (the `--enable-*` flag) and Gate 2 (IAM) are
|
|
277
|
+
> two *separate* switches and you need **both**. Turning on `--enable-destructive` only *exposes* the
|
|
278
|
+
> tool in the client — it does not give your AWS profile any access. If the flag is on but the
|
|
279
|
+
> profile/role is **missing the matching tier policy**, the tool is callable and its dry-run works,
|
|
280
|
+
> but the real `confirm=true` call **fails with an AWS `AccessDenied` error and nothing is deleted**.
|
|
281
|
+
> So enabling writes/destructive in your MCP client is necessary but **not** sufficient: you must
|
|
282
|
+
> *also* attach [`iam/tier2-lifecycle.json`](iam/tier2-lifecycle.json) /
|
|
283
|
+
> [`iam/tier3-destructive.json`](iam/tier3-destructive.json) to the AWS profile (or assumed role)
|
|
284
|
+
> the server runs as.
|
|
285
|
+
>
|
|
286
|
+
> Gates **1–2** are the real security boundary (config + AWS-enforced IAM). Gates **3–4** are
|
|
287
|
+
> in-tool guardrails against an over-eager agent or a fat-fingered single call — they are **not** a
|
|
288
|
+
> substitute for IAM. The genuine least-privilege control is: **don't grant Tier 2/3 and don't pass
|
|
289
|
+
> the flags unless you truly want those operations available.** For an extra backstop, scope the
|
|
290
|
+
> Tier 2/3 policy with resource tags / conditions so even an enabled server can only touch a bounded
|
|
291
|
+
> set of resources.
|
|
292
|
+
|
|
293
|
+
**MCP-client config — writes enabled (Tier 2):** add the flag to `args` and attach the Tier 2 policy.
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"mcpServers": {
|
|
298
|
+
"workspaces-euc": {
|
|
299
|
+
"command": "uvx",
|
|
300
|
+
"args": ["workspaces-euc-mcp-server@latest", "--enable-writes", "--max-bulk-targets", "10"],
|
|
301
|
+
"env": {
|
|
302
|
+
"AWS_PROFILE": "your-euc-admin-profile",
|
|
303
|
+
"AWS_REGION": "us-east-1"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
**MCP-client config — destructive enabled (Tier 3):** both flags, and attach the Tier 3 policy. Use
|
|
311
|
+
a tightly-scoped profile.
|
|
312
|
+
|
|
313
|
+
```json
|
|
314
|
+
{
|
|
315
|
+
"mcpServers": {
|
|
316
|
+
"workspaces-euc": {
|
|
317
|
+
"command": "uvx",
|
|
318
|
+
"args": [
|
|
319
|
+
"workspaces-euc-mcp-server@latest",
|
|
320
|
+
"--enable-writes",
|
|
321
|
+
"--enable-destructive",
|
|
322
|
+
"--max-bulk-targets", "5"
|
|
323
|
+
],
|
|
324
|
+
"env": {
|
|
325
|
+
"AWS_PROFILE": "your-euc-admin-profile",
|
|
326
|
+
"AWS_REGION": "us-east-1"
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Even with the destructive config above, a call still does nothing until gates 3–4 are satisfied:
|
|
334
|
+
|
|
335
|
+
```text
|
|
336
|
+
terminate_workspaces(workspace_ids=["ws-abc123"], confirm=true, acknowledge="TERMINATE")
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Equivalent CLI launches (e.g. for the Docker entrypoint or a shell):
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
workspaces-euc-mcp-server --enable-writes --max-bulk-targets 10 # Tier 2 only
|
|
343
|
+
workspaces-euc-mcp-server --enable-writes --enable-destructive --max-bulk-targets 5 # + Tier 3
|
|
344
|
+
```
|
|
345
|
+
|
|
204
346
|
## Command-line flags
|
|
205
347
|
|
|
206
348
|
| Flag | Default | Purpose |
|
|
@@ -256,19 +398,9 @@ pick the right tool. A few examples:
|
|
|
256
398
|
| "Reboot these three stuck desktops." | `reboot_workspaces` (dry-run, then `confirm=true`) |
|
|
257
399
|
|
|
258
400
|
**Write/destructive tools are off unless enabled.** Mutations show a dry-run plan first; to execute
|
|
259
|
-
you pass `confirm=true` (and, for destructive ops, the exact acknowledge phrase).
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
```text
|
|
263
|
-
terminate_workspaces(workspace_ids=["ws-abc123"], confirm=true, acknowledge="TERMINATE")
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
Launch with writes/destructive enabled:
|
|
267
|
-
|
|
268
|
-
```bash
|
|
269
|
-
workspaces-euc-mcp-server --enable-writes # Tier 2 power/capacity tools
|
|
270
|
-
workspaces-euc-mcp-server --enable-writes --enable-destructive # + Tier 3 terminate/rebuild/restore
|
|
271
|
-
```
|
|
401
|
+
you pass `confirm=true` (and, for destructive ops, the exact acknowledge phrase). See
|
|
402
|
+
[Enabling write / destructive tools — the safety gates](#enabling-write--destructive-tools--the-safety-gates)
|
|
403
|
+
for how to turn them on (MCP-client config + IAM) and the four gates each call must clear.
|
|
272
404
|
|
|
273
405
|
## Development
|
|
274
406
|
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp/actions/workflows/ci.yml)
|
|
4
4
|
|
|
5
|
+
> **Disclaimer:** This project is not an official AWS product, service, or solution, and is not
|
|
6
|
+
> affiliated with or endorsed by Amazon Web Services. It is an independent, community example
|
|
7
|
+
> implementation intended to demonstrate what is possible when combining the Amazon WorkSpaces End
|
|
8
|
+
> User Computing APIs with the Model Context Protocol. It is provided as a starting point to learn
|
|
9
|
+
> from, adapt, and iterate on — not as a supported or production-certified offering. Use it at your
|
|
10
|
+
> own discretion and always validate it against your organisation's security, compliance, and
|
|
11
|
+
> operational requirements before deploying to production.
|
|
12
|
+
|
|
5
13
|
An [MCP](https://modelcontextprotocol.io) server that gives administrators AI-assisted
|
|
6
14
|
**inventory, troubleshooting, and cost/utilization optimization** across the Amazon WorkSpaces
|
|
7
15
|
End User Computing (EUC) portfolio:
|
|
@@ -48,7 +56,7 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
|
|
|
48
56
|
| `get_workspace_connection_history` | A desktop's connection/session **history** (UserConnected + connection attempts/failures) over a window, with a summary (Tier 0). |
|
|
49
57
|
| `get_pool_session_history` | A WorkSpaces Pool's user-**session history** (active/available/utilization capacity time-series), flags idle pool capacity (Tier 0). |
|
|
50
58
|
| `recommend_bundle_rightsizing` | Suggests smaller/larger compute types from CPU & memory headroom (general families; graphics excluded) (Tier 0). |
|
|
51
|
-
| `get_euc_cost_summary` | EUC spend by service over a window via Cost Explorer, account-wide (Tier 1). |
|
|
59
|
+
| `get_euc_cost_summary` | EUC spend by service over a window (or an explicit `start_date`/`end_date` calendar month) via Cost Explorer, account-wide. Services are matched by keyword so naming variants (e.g. AppStream 2.0) aren't dropped; note Cost Explorer bills WorkSpaces Personal/Pools/Core together as one "Amazon WorkSpaces" line (Tier 1). |
|
|
52
60
|
| `generate_inventory_report` | Detailed per-resource inventory (desktops **with assigned user / computer name / IP**, pools, fleets, **stacks + their associated fleets**, portals) with key attributes (Tier 0). |
|
|
53
61
|
| `audit_security_posture` | Cross-service: flags unencrypted WorkSpace volumes, directories without IP access control groups, and **Secure Browser portals / Applications stacks that allow data egress** (clipboard/download/print) (Tier 0). |
|
|
54
62
|
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (Tier 0). |
|
|
@@ -171,6 +179,140 @@ Running from a source checkout instead of `uvx`:
|
|
|
171
179
|
}
|
|
172
180
|
```
|
|
173
181
|
|
|
182
|
+
The two blocks above are the standard `mcpServers` shape used by most clients (Claude Desktop,
|
|
183
|
+
Cursor, etc.). Some clients use a **form** instead of raw JSON — see the example below.
|
|
184
|
+
|
|
185
|
+
### Example: Amazon Quick (Desktop)
|
|
186
|
+
|
|
187
|
+
Amazon Quick's **Capabilities → MCP → Add MCP** uses a form rather than JSON. Choose connection
|
|
188
|
+
type **Local** ("Run a command on your machine") and fill in:
|
|
189
|
+
|
|
190
|
+
| Field | Value |
|
|
191
|
+
|-------|-------|
|
|
192
|
+
| **Name** | `WorkSpaces EUC` |
|
|
193
|
+
| **Command** | `uvx` |
|
|
194
|
+
| **Arguments** | `workspaces-euc-mcp-server@latest --region us-east-1` |
|
|
195
|
+
| **Description** | `Admin tools for Amazon WorkSpaces EUC — inventory, troubleshooting, cost/utilization. Read-only.` |
|
|
196
|
+
| **Environment variables** | `AWS_PROFILE` = `your-euc-admin-profile` · `AWS_REGION` = `us-east-1` |
|
|
197
|
+
| **Timeout (seconds)** | `60` |
|
|
198
|
+
|
|
199
|
+
Notes:
|
|
200
|
+
- Set `--region` (in Arguments) and `AWS_REGION` to where your WorkSpaces actually live; keep the
|
|
201
|
+
two in sync.
|
|
202
|
+
- **Bump the timeout from the default 30 → 60–120 for the first run** — `uvx` downloads the package
|
|
203
|
+
on first launch, which can exceed 30 s and look like a failed connection. Later starts are fast.
|
|
204
|
+
- If `uvx` isn't on your `PATH`, use Command `workspaces-euc-mcp-server` (after
|
|
205
|
+
`pip install workspaces-euc-mcp-server`) with Arguments `--region us-east-1`, or Command `python`
|
|
206
|
+
with Arguments `-m workspaces_euc_mcp_server.server --region us-east-1`.
|
|
207
|
+
- To enable writes/destructive, append the flags to **Arguments** (e.g.
|
|
208
|
+
`… --enable-writes --max-bulk-targets 10`) — and attach the matching IAM tier (see
|
|
209
|
+
[the safety gates](#enabling-write--destructive-tools--the-safety-gates)).
|
|
210
|
+
- A local MCP server has **no "Sign in" button** (that's a Quick *Connections* feature). It uses
|
|
211
|
+
your AWS credential chain, so authenticate first — e.g. `aws sso login --profile your-profile` for
|
|
212
|
+
SSO — then the server connects. See [AWS authentication](#aws-authentication).
|
|
213
|
+
|
|
214
|
+
## AWS authentication
|
|
215
|
+
|
|
216
|
+
The server uses the **standard AWS credential chain** — it does not handle sign-in itself. Provide
|
|
217
|
+
credentials however boto3 expects them:
|
|
218
|
+
|
|
219
|
+
- **IAM Identity Center (SSO):** configure an `sso-session` profile, then `aws sso login --profile
|
|
220
|
+
<name>`. With the modern `sso-session` format, botocore **auto-refreshes** the token for the
|
|
221
|
+
session window, so you log in once rather than every few hours. (Signing into the AWS Console or
|
|
222
|
+
access portal in a browser does **not** create the on-disk token the server needs — only
|
|
223
|
+
`aws sso login` does.)
|
|
224
|
+
- **Static / temporary keys:** a named profile in `~/.aws/credentials`, or `AWS_ACCESS_KEY_ID` /
|
|
225
|
+
`AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` env vars.
|
|
226
|
+
- **Assumed role / multi-account:** see [Multi-account / MSP](#multi-account--msp).
|
|
227
|
+
|
|
228
|
+
If calls fail with an expired-token / `UnauthorizedException` error, your session has lapsed —
|
|
229
|
+
re-authenticate (for SSO, `aws sso login --profile <name>`) and retry. Nothing in the MCP config
|
|
230
|
+
changes.
|
|
231
|
+
|
|
232
|
+
## Enabling write / destructive tools — the safety gates
|
|
233
|
+
|
|
234
|
+
The config above is **read-only**: the write and destructive tools are not even registered, so
|
|
235
|
+
they cannot be called no matter what the model or the IAM policy allows. Enabling them is a
|
|
236
|
+
deliberate act, and **IAM permissions alone are not enough**. A destructive call (e.g.
|
|
237
|
+
`terminate_workspaces`) only runs after clearing **all four** of these independent gates:
|
|
238
|
+
|
|
239
|
+
| # | Gate | Where it lives | What it does |
|
|
240
|
+
|---|------|----------------|--------------|
|
|
241
|
+
| 1 | **Launch flag** | The `args` in your MCP-client config | Destructive tools aren't registered unless the server was started with **both** `--enable-writes` **and** `--enable-destructive`. Default launch = read-only. |
|
|
242
|
+
| 2 | **IAM tier** | The AWS profile / assumed role | The credentials must carry the matching tier ([`iam/tier2-lifecycle.json`](iam/tier2-lifecycle.json) for writes, [`iam/tier3-destructive.json`](iam/tier3-destructive.json) for destructive). Without it, AWS denies the call. |
|
|
243
|
+
| 3 | **`confirm=true`** | The tool call | Every mutation is **dry-run by default** — it returns a plan and changes nothing. You must explicitly pass `confirm=true`. |
|
|
244
|
+
| 4 | **Typed acknowledgement + blast-radius cap** | The tool call | Destructive ops also require the **exact** phrase (`acknowledge="TERMINATE"` / `"REBUILD"` / `"RESTORE"`) and must stay within `--max-bulk-targets`. Wrong phrase or too many targets → refused, nothing changes. |
|
|
245
|
+
|
|
246
|
+
> **The launch flag grants no AWS permission.** Gate 1 (the `--enable-*` flag) and Gate 2 (IAM) are
|
|
247
|
+
> two *separate* switches and you need **both**. Turning on `--enable-destructive` only *exposes* the
|
|
248
|
+
> tool in the client — it does not give your AWS profile any access. If the flag is on but the
|
|
249
|
+
> profile/role is **missing the matching tier policy**, the tool is callable and its dry-run works,
|
|
250
|
+
> but the real `confirm=true` call **fails with an AWS `AccessDenied` error and nothing is deleted**.
|
|
251
|
+
> So enabling writes/destructive in your MCP client is necessary but **not** sufficient: you must
|
|
252
|
+
> *also* attach [`iam/tier2-lifecycle.json`](iam/tier2-lifecycle.json) /
|
|
253
|
+
> [`iam/tier3-destructive.json`](iam/tier3-destructive.json) to the AWS profile (or assumed role)
|
|
254
|
+
> the server runs as.
|
|
255
|
+
>
|
|
256
|
+
> Gates **1–2** are the real security boundary (config + AWS-enforced IAM). Gates **3–4** are
|
|
257
|
+
> in-tool guardrails against an over-eager agent or a fat-fingered single call — they are **not** a
|
|
258
|
+
> substitute for IAM. The genuine least-privilege control is: **don't grant Tier 2/3 and don't pass
|
|
259
|
+
> the flags unless you truly want those operations available.** For an extra backstop, scope the
|
|
260
|
+
> Tier 2/3 policy with resource tags / conditions so even an enabled server can only touch a bounded
|
|
261
|
+
> set of resources.
|
|
262
|
+
|
|
263
|
+
**MCP-client config — writes enabled (Tier 2):** add the flag to `args` and attach the Tier 2 policy.
|
|
264
|
+
|
|
265
|
+
```json
|
|
266
|
+
{
|
|
267
|
+
"mcpServers": {
|
|
268
|
+
"workspaces-euc": {
|
|
269
|
+
"command": "uvx",
|
|
270
|
+
"args": ["workspaces-euc-mcp-server@latest", "--enable-writes", "--max-bulk-targets", "10"],
|
|
271
|
+
"env": {
|
|
272
|
+
"AWS_PROFILE": "your-euc-admin-profile",
|
|
273
|
+
"AWS_REGION": "us-east-1"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**MCP-client config — destructive enabled (Tier 3):** both flags, and attach the Tier 3 policy. Use
|
|
281
|
+
a tightly-scoped profile.
|
|
282
|
+
|
|
283
|
+
```json
|
|
284
|
+
{
|
|
285
|
+
"mcpServers": {
|
|
286
|
+
"workspaces-euc": {
|
|
287
|
+
"command": "uvx",
|
|
288
|
+
"args": [
|
|
289
|
+
"workspaces-euc-mcp-server@latest",
|
|
290
|
+
"--enable-writes",
|
|
291
|
+
"--enable-destructive",
|
|
292
|
+
"--max-bulk-targets", "5"
|
|
293
|
+
],
|
|
294
|
+
"env": {
|
|
295
|
+
"AWS_PROFILE": "your-euc-admin-profile",
|
|
296
|
+
"AWS_REGION": "us-east-1"
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Even with the destructive config above, a call still does nothing until gates 3–4 are satisfied:
|
|
304
|
+
|
|
305
|
+
```text
|
|
306
|
+
terminate_workspaces(workspace_ids=["ws-abc123"], confirm=true, acknowledge="TERMINATE")
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
Equivalent CLI launches (e.g. for the Docker entrypoint or a shell):
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
workspaces-euc-mcp-server --enable-writes --max-bulk-targets 10 # Tier 2 only
|
|
313
|
+
workspaces-euc-mcp-server --enable-writes --enable-destructive --max-bulk-targets 5 # + Tier 3
|
|
314
|
+
```
|
|
315
|
+
|
|
174
316
|
## Command-line flags
|
|
175
317
|
|
|
176
318
|
| Flag | Default | Purpose |
|
|
@@ -226,19 +368,9 @@ pick the right tool. A few examples:
|
|
|
226
368
|
| "Reboot these three stuck desktops." | `reboot_workspaces` (dry-run, then `confirm=true`) |
|
|
227
369
|
|
|
228
370
|
**Write/destructive tools are off unless enabled.** Mutations show a dry-run plan first; to execute
|
|
229
|
-
you pass `confirm=true` (and, for destructive ops, the exact acknowledge phrase).
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
```text
|
|
233
|
-
terminate_workspaces(workspace_ids=["ws-abc123"], confirm=true, acknowledge="TERMINATE")
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
Launch with writes/destructive enabled:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
workspaces-euc-mcp-server --enable-writes # Tier 2 power/capacity tools
|
|
240
|
-
workspaces-euc-mcp-server --enable-writes --enable-destructive # + Tier 3 terminate/rebuild/restore
|
|
241
|
-
```
|
|
371
|
+
you pass `confirm=true` (and, for destructive ops, the exact acknowledge phrase). See
|
|
372
|
+
[Enabling write / destructive tools — the safety gates](#enabling-write--destructive-tools--the-safety-gates)
|
|
373
|
+
for how to turn them on (MCP-client config + IAM) and the four gates each call must clear.
|
|
242
374
|
|
|
243
375
|
## Development
|
|
244
376
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "workspaces-euc-mcp-server"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.3"
|
|
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"
|
|
@@ -129,6 +129,107 @@ def test_cost_summary_aggregates_by_service():
|
|
|
129
129
|
assert summary.by_service[1].amount == 40.0
|
|
130
130
|
|
|
131
131
|
|
|
132
|
+
def test_cost_summary_keyword_matches_variants_and_excludes_noneuc():
|
|
133
|
+
# Real-world Cost Explorer SERVICE names: AppStream now bills as "Amazon WorkSpaces
|
|
134
|
+
# Applications" (the rebrand that the old exact-match list missed) and must be captured;
|
|
135
|
+
# "Amazon WorkSpaces Thin Client" contains "workspaces" but is OUT of scope and must be
|
|
136
|
+
# excluded; non-EUC services (EC2) are excluded.
|
|
137
|
+
ce = types.SimpleNamespace(
|
|
138
|
+
get_cost_and_usage=lambda **_: {
|
|
139
|
+
"ResultsByTime": [
|
|
140
|
+
{
|
|
141
|
+
"Groups": [
|
|
142
|
+
{
|
|
143
|
+
"Keys": ["Amazon WorkSpaces Applications"],
|
|
144
|
+
"Metrics": {"UnblendedCost": {"Amount": "1006.85", "Unit": "USD"}},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"Keys": ["Amazon WorkSpaces"],
|
|
148
|
+
"Metrics": {"UnblendedCost": {"Amount": "694.97", "Unit": "USD"}},
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"Keys": ["Amazon WorkSpaces Thin Client"],
|
|
152
|
+
"Metrics": {"UnblendedCost": {"Amount": "6.00", "Unit": "USD"}},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"Keys": ["Amazon Elastic Compute Cloud - Compute"],
|
|
156
|
+
"Metrics": {"UnblendedCost": {"Amount": "485.58", "Unit": "USD"}},
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
]
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
factory = FakeFactory({consts.COST_EXPLORER_API: ce})
|
|
164
|
+
|
|
165
|
+
summary = cost.get_euc_cost_summary_core(factory, lookback_days=30)
|
|
166
|
+
|
|
167
|
+
# WorkSpaces Applications captured, Thin Client + EC2 excluded.
|
|
168
|
+
assert {li.service for li in summary.by_service} == {
|
|
169
|
+
"Amazon WorkSpaces Applications",
|
|
170
|
+
"Amazon WorkSpaces",
|
|
171
|
+
}
|
|
172
|
+
assert summary.total == 1701.82
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def test_cost_summary_explicit_date_range_overrides_lookback():
|
|
176
|
+
captured: dict = {}
|
|
177
|
+
|
|
178
|
+
def get_cost_and_usage(**kwargs):
|
|
179
|
+
captured.update(kwargs)
|
|
180
|
+
return {"ResultsByTime": []}
|
|
181
|
+
|
|
182
|
+
ce = types.SimpleNamespace(get_cost_and_usage=get_cost_and_usage)
|
|
183
|
+
factory = FakeFactory({consts.COST_EXPLORER_API: ce})
|
|
184
|
+
|
|
185
|
+
summary = cost.get_euc_cost_summary_core(
|
|
186
|
+
factory, start_date="2026-05-01", end_date="2026-06-01"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert captured["TimePeriod"] == {"Start": "2026-05-01", "End": "2026-06-01"}
|
|
190
|
+
assert summary.start == "2026-05-01"
|
|
191
|
+
assert summary.end == "2026-06-01"
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_cost_summary_follows_pagination():
|
|
195
|
+
page1 = {
|
|
196
|
+
"ResultsByTime": [
|
|
197
|
+
{
|
|
198
|
+
"Groups": [
|
|
199
|
+
{
|
|
200
|
+
"Keys": ["Amazon WorkSpaces"],
|
|
201
|
+
"Metrics": {"UnblendedCost": {"Amount": "10.00", "Unit": "USD"}},
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
"NextPageToken": "p2",
|
|
207
|
+
}
|
|
208
|
+
page2 = {
|
|
209
|
+
"ResultsByTime": [
|
|
210
|
+
{
|
|
211
|
+
"Groups": [
|
|
212
|
+
{
|
|
213
|
+
"Keys": ["Amazon AppStream"],
|
|
214
|
+
"Metrics": {"UnblendedCost": {"Amount": "20.00", "Unit": "USD"}},
|
|
215
|
+
}
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
def get_cost_and_usage(**kwargs):
|
|
222
|
+
return page2 if kwargs.get("NextPageToken") == "p2" else page1
|
|
223
|
+
|
|
224
|
+
ce = types.SimpleNamespace(get_cost_and_usage=get_cost_and_usage)
|
|
225
|
+
factory = FakeFactory({consts.COST_EXPLORER_API: ce})
|
|
226
|
+
|
|
227
|
+
summary = cost.get_euc_cost_summary_core(factory, lookback_days=30)
|
|
228
|
+
|
|
229
|
+
assert summary.total == 30.0
|
|
230
|
+
assert {li.service for li in summary.by_service} == {"Amazon WorkSpaces", "Amazon AppStream"}
|
|
231
|
+
|
|
232
|
+
|
|
132
233
|
def test_cost_summary_records_errors_gracefully():
|
|
133
234
|
from botocore.exceptions import ClientError
|
|
134
235
|
|
|
@@ -26,14 +26,16 @@ PRICING_API = "pricing" # AWS Price List (global).
|
|
|
26
26
|
# Cost Explorer is a global endpoint served from us-east-1 regardless of the working region.
|
|
27
27
|
COST_EXPLORER_REGION = "us-east-1"
|
|
28
28
|
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
# Substrings (lowercased) that identify an EUC service in the Cost Explorer SERVICE dimension.
|
|
30
|
+
# Matched in code against every service returned — NOT used as an exact-name server-side filter —
|
|
31
|
+
# so account/era naming variants (e.g. "Amazon AppStream 2.0") and casing differences can never be
|
|
32
|
+
# silently dropped. "workspaces" covers Personal/Pools/Core/Web/Secure Browser; "appstream" covers
|
|
33
|
+
# WorkSpaces Applications (AppStream 2.0).
|
|
34
|
+
EUC_COST_EXPLORER_SERVICE_TOKENS = ["workspaces", "appstream"]
|
|
35
|
+
# Substrings that EXCLUDE a service even when an include token matches. WorkSpaces Thin Client is
|
|
36
|
+
# out of scope for this server, but its Cost Explorer name ("Amazon WorkSpaces Thin Client")
|
|
37
|
+
# contains "workspaces" — so exclude it explicitly.
|
|
38
|
+
EUC_COST_EXPLORER_EXCLUDE_TOKENS = ["thin client"]
|
|
37
39
|
|
|
38
40
|
# Current official product names (used in all human-facing output).
|
|
39
41
|
PRODUCT_WORKSPACES_PERSONAL = "Amazon WorkSpaces Personal"
|
|
@@ -195,45 +195,72 @@ def recommend_running_mode_core(
|
|
|
195
195
|
)
|
|
196
196
|
|
|
197
197
|
|
|
198
|
+
def _is_euc_service(service_name: str) -> bool:
|
|
199
|
+
"""True if a Cost Explorer SERVICE value belongs to the EUC portfolio.
|
|
200
|
+
|
|
201
|
+
Matches by keyword rather than exact name, so account/era naming variants
|
|
202
|
+
(e.g. "Amazon AppStream 2.0") are never silently excluded.
|
|
203
|
+
"""
|
|
204
|
+
name = service_name.lower()
|
|
205
|
+
if any(token in name for token in consts.EUC_COST_EXPLORER_EXCLUDE_TOKENS):
|
|
206
|
+
return False
|
|
207
|
+
return any(token in name for token in consts.EUC_COST_EXPLORER_SERVICE_TOKENS)
|
|
208
|
+
|
|
209
|
+
|
|
198
210
|
def get_euc_cost_summary_core(
|
|
199
|
-
factory: ClientFactory,
|
|
211
|
+
factory: ClientFactory,
|
|
212
|
+
lookback_days: int = 30,
|
|
213
|
+
granularity: str = "MONTHLY",
|
|
214
|
+
start_date: str | None = None,
|
|
215
|
+
end_date: str | None = None,
|
|
200
216
|
) -> CostSummary:
|
|
201
217
|
errors: list[ServiceError] = []
|
|
202
218
|
# Cost Explorer is a global endpoint served from us-east-1, regardless of working region.
|
|
203
219
|
cost_explorer = factory.client(consts.COST_EXPLORER_API, region=consts.COST_EXPLORER_REGION)
|
|
204
220
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
"AWS Cost Explorer",
|
|
212
|
-
"GetCostAndUsage",
|
|
213
|
-
lambda: cost_explorer.get_cost_and_usage(
|
|
214
|
-
TimePeriod={"Start": start, "End": end},
|
|
215
|
-
Granularity=granularity,
|
|
216
|
-
Metrics=["UnblendedCost"],
|
|
217
|
-
Filter={
|
|
218
|
-
"Dimensions": {
|
|
219
|
-
"Key": "SERVICE",
|
|
220
|
-
"Values": consts.EUC_COST_EXPLORER_SERVICES,
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
GroupBy=[{"Type": "DIMENSION", "Key": "SERVICE"}],
|
|
224
|
-
),
|
|
225
|
-
default={},
|
|
226
|
-
)
|
|
221
|
+
if start_date and end_date:
|
|
222
|
+
start, end = start_date, end_date
|
|
223
|
+
else:
|
|
224
|
+
end_d = datetime.now(UTC).date()
|
|
225
|
+
start_d = end_d - timedelta(days=lookback_days)
|
|
226
|
+
start, end = start_d.isoformat(), end_d.isoformat()
|
|
227
227
|
|
|
228
|
+
# Group by SERVICE across ALL spend and select EUC services in code (see _is_euc_service).
|
|
229
|
+
# 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.
|
|
228
231
|
totals_by_service: dict[str, float] = {}
|
|
229
232
|
currency = "USD"
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
233
|
+
next_token: str | None = None
|
|
234
|
+
while True:
|
|
235
|
+
kwargs: dict[str, Any] = {
|
|
236
|
+
"TimePeriod": {"Start": start, "End": end},
|
|
237
|
+
"Granularity": granularity,
|
|
238
|
+
"Metrics": ["UnblendedCost"],
|
|
239
|
+
"GroupBy": [{"Type": "DIMENSION", "Key": "SERVICE"}],
|
|
240
|
+
}
|
|
241
|
+
if next_token:
|
|
242
|
+
kwargs["NextPageToken"] = next_token
|
|
243
|
+
response = try_call(
|
|
244
|
+
errors,
|
|
245
|
+
"AWS Cost Explorer",
|
|
246
|
+
"GetCostAndUsage",
|
|
247
|
+
lambda kwargs=kwargs: cost_explorer.get_cost_and_usage(**kwargs),
|
|
248
|
+
default={},
|
|
249
|
+
)
|
|
250
|
+
if not response:
|
|
251
|
+
break
|
|
252
|
+
for period in response.get("ResultsByTime", []):
|
|
253
|
+
for group in period.get("Groups", []):
|
|
254
|
+
service = (group.get("Keys") or ["Unknown"])[0]
|
|
255
|
+
if not _is_euc_service(service):
|
|
256
|
+
continue
|
|
257
|
+
metric = group.get("Metrics", {}).get("UnblendedCost", {})
|
|
258
|
+
amount = float(metric.get("Amount", 0.0))
|
|
259
|
+
currency = metric.get("Unit", currency)
|
|
260
|
+
totals_by_service[service] = totals_by_service.get(service, 0.0) + amount
|
|
261
|
+
next_token = response.get("NextPageToken")
|
|
262
|
+
if not next_token:
|
|
263
|
+
break
|
|
237
264
|
|
|
238
265
|
by_service = [
|
|
239
266
|
CostLineItem(service=s, amount=round(a, 2))
|
|
@@ -250,8 +277,10 @@ def get_euc_cost_summary_core(
|
|
|
250
277
|
by_service=by_service,
|
|
251
278
|
errors=errors,
|
|
252
279
|
notes=[
|
|
253
|
-
"
|
|
254
|
-
"
|
|
280
|
+
"EUC services are selected by matching the Cost Explorer SERVICE name against the EUC "
|
|
281
|
+
"keyword set (workspaces / appstream), so naming variants are not dropped.",
|
|
282
|
+
"Cost Explorer bills WorkSpaces Personal, Pools, and Core together under the single "
|
|
283
|
+
"'Amazon WorkSpaces' service; they cannot be separated via the SERVICE dimension.",
|
|
255
284
|
],
|
|
256
285
|
)
|
|
257
286
|
|
|
@@ -293,18 +322,33 @@ def register(mcp: Any, factory: ClientFactory) -> None:
|
|
|
293
322
|
return report.model_dump()
|
|
294
323
|
|
|
295
324
|
async def get_euc_cost_summary(
|
|
296
|
-
lookback_days: int = 30,
|
|
325
|
+
lookback_days: int = 30,
|
|
326
|
+
granularity: Literal["MONTHLY", "DAILY"] = "MONTHLY",
|
|
327
|
+
start_date: str | None = None,
|
|
328
|
+
end_date: str | None = None,
|
|
297
329
|
) -> dict[str, Any]:
|
|
298
330
|
"""Summarize EUC spend by service over a window (account-wide via Cost Explorer).
|
|
299
331
|
|
|
300
|
-
Returns unblended cost grouped by service for the EUC portfolio
|
|
301
|
-
|
|
332
|
+
Returns unblended cost grouped by service for the EUC portfolio (WorkSpaces, including
|
|
333
|
+
Personal/Pools/Core which Cost Explorer bills together as "Amazon WorkSpaces"; WorkSpaces
|
|
334
|
+
Applications/AppStream; and Secure Browser). Services are matched by keyword, so naming
|
|
335
|
+
variants are never dropped. Cost Explorer is not region-scoped, so figures are account-wide.
|
|
336
|
+
Read-only.
|
|
337
|
+
|
|
338
|
+
For a specific calendar month, pass start_date/end_date instead of lookback_days — Cost
|
|
339
|
+
Explorer's end is EXCLUSIVE, so for May 2026 use start_date="2026-05-01",
|
|
340
|
+
end_date="2026-06-01".
|
|
302
341
|
|
|
303
342
|
Args:
|
|
304
|
-
lookback_days: How far back to total (default 30).
|
|
343
|
+
lookback_days: How far back to total when start_date/end_date are omitted (default 30).
|
|
305
344
|
granularity: Cost Explorer granularity: MONTHLY or DAILY (default MONTHLY).
|
|
345
|
+
start_date: Optional inclusive start, "YYYY-MM-DD". Use with end_date; overrides
|
|
346
|
+
lookback_days.
|
|
347
|
+
end_date: Optional EXCLUSIVE end, "YYYY-MM-DD". Use with start_date.
|
|
306
348
|
"""
|
|
307
|
-
summary = get_euc_cost_summary_core(
|
|
349
|
+
summary = get_euc_cost_summary_core(
|
|
350
|
+
factory, lookback_days, granularity, start_date, end_date
|
|
351
|
+
)
|
|
308
352
|
return summary.model_dump()
|
|
309
353
|
|
|
310
354
|
mcp.add_tool(
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier0-diagnostics.json
RENAMED
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier2-lifecycle.json
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/iam/tier3-destructive.json
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/scripts/smoke_readonly.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_destructive.py
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/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.2 → workspaces_euc_mcp_server-0.1.3}/tests/test_performance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.2 → workspaces_euc_mcp_server-0.1.3}/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
|