sample-aws-pricing-calculator-mcp 1.2.9 → 1.3.0

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.
package/README.md CHANGED
@@ -84,25 +84,30 @@ Then point your client at the built bundle:
84
84
  ```
85
85
  mcp-server.js # Entry point — registers the 9 MCP tools, stdio + HTTP transports
86
86
  lib/
87
- aws-client.js # Manifest loading, service definitions, save/read APIs
88
- estimate-builder.js # In-memory estimate model, AWS payload assembly
89
- ec2.js # EC2 agent-friendly → ec2Enhancement transform
90
- validation.js # Pre-save config validation (field-id, value-shape, region, auto-correct)
91
- can-rehydrate.js # Static rehydration linter (12 predicates, pure)
92
- can-rehydrate-fetch.js # Network wrapper around the linter
93
- lint-hints.js # Translates lint failures into agent-actionable next_step text
94
- catalog.js # Loader for catalog/services/*.json curated entries
95
- pct-config.js # PCT-based field suggestion
96
- surfaceability.js # PCT-driven surfaceability index
97
- agent-fields.js # Synthetic field surfacing for fields hidden in composite widgets
98
- dom-cost.js # Playwright DOM scrape of the calculator's rendered cost
99
- handler-helpers.js # Shared internals for the MCP tool handlers
100
- tool-descriptions.js # Long agent-facing tool descriptions (separated from wiring)
101
- estimate-store.js # Pluggable in-flight estimate store (memory default)
102
- estimate-store-dynamodb.js # DynamoDB-backed store for stateless multi-replica deployments
103
- trace-logger.js # Structured JSON trace events on stderr
104
- trace-events.js # Trace event name registry
105
- request-context.js # Per-request session id propagation
87
+ aws/ # AWS API access + payload construction
88
+ aws-client.js # Manifest loading, service definitions, save/read APIs, selector aggregations
89
+ estimate-builder.js # In-memory estimate model, AWS payload assembly, columnFormIPM remap
90
+ ec2.js # EC2 agent-friendly → ec2Enhancement transform
91
+ pct-config.js # PCT-based field suggestion
92
+ surfaceability.js # PCT-driven surfaceability index
93
+ agent-fields.js # Synthetic field surfacing for fields hidden in composite widgets
94
+ lint/ # Rehydration prediction + pre-save validation
95
+ can-rehydrate.js # Static rehydration linter (pure predicates)
96
+ can-rehydrate-fetch.js # Network wrapper around the linter
97
+ validation.js # Pre-save config validation (field-id, value-shape, region, auto-correct)
98
+ lint-hints.js # Translates lint failures into agent-actionable next_step text
99
+ catalog.js # Loader for catalog/services/*.json curated entries
100
+ mcp/ # MCP tool plumbing
101
+ handler-helpers.js # Shared internals for the MCP tool handlers
102
+ tool-descriptions.js # Long agent-facing tool descriptions (separated from wiring)
103
+ trace/ # Observability
104
+ trace-logger.js # Structured JSON trace events on stderr
105
+ trace-events.js # Trace event name registry
106
+ request-context.js # Per-request session id propagation
107
+ store/ # In-flight estimate persistence
108
+ estimate-store.js # Pluggable store (memory default)
109
+ estimate-store-dynamodb.js # DynamoDB-backed store for stateless multi-replica deployments
110
+ dom-cost.js # Playwright DOM scrape of the calculator's rendered cost (eval oracle)
106
111
  catalog/
107
112
  schema.json # JSON Schema for catalog entries
108
113
  services/ # Verified configs catalog (minimalConfig, traps, subServices)
@@ -112,7 +117,7 @@ scripts/ # Maintainer tools (catalog authoring, sweep, diagnos
112
117
  dist/ # Build output: mcp-server.js bundle, aws-calculator.zip, bundle-contract.json
113
118
  ```
114
119
 
115
- The `lib/` listing is exhaustive at 1.2.0. The architecture diagram below highlights the load-bearing modules; minor helpers are not shown.
120
+ The `lib/` tree groups modules by layer (aws, lint, mcp, trace, store). The architecture diagram below highlights the load-bearing modules; minor helpers are not shown.
116
121
 
117
122
  ## Build
118
123
 
@@ -127,13 +132,29 @@ Produces:
127
132
 
128
133
  ## Tests
129
134
 
130
- Three layers, no overlap:
135
+ Four layers, no overlap:
131
136
 
132
- - **`npm test`** — node:test suite (424/429 pass with mocked I/O). Per-commit gate. Covers pure functions (validation, lint, surfaceability), payload construction, EC2 transforms, catalog schema.
137
+ - **`npm test`** — node:test suite, **hermetic**: 491 pass / 10 skipped, no network, ~1.5s. Per-commit gate, and the only layer that runs in CI. Covers pure functions (validation, lint, surfaceability), payload construction, EC2 transforms, catalog schema.
138
+ - **`npm run test:network`** — the same suite with `SKIP_NETWORK` unset, so the 10 skipped tests run. They fetch the live manifest and POST real estimates to the save API, which is why they are not in CI. Run before a release.
133
139
  - **`npm run validate-catalog:cost`** — sweeps verified catalog entries against the live cost oracle. Catches stale URLs and pricing-engine drift.
134
- - **`python eval/run.py`** — 87 YAML scenarios (scripted MCP calls + LLM-driven). Each does a real save, then asserts on the saved blob (DOM-rendered cost, structural field equality, lint-must-pass). Run on demand (~1-2 min for stdio scenarios; LLM scenarios cost a few cents on Bedrock Haiku).
140
+ - **`python eval/run.py`** — 87 YAML scenarios (scripted MCP calls + LLM-driven). Each does a real save, then asserts on the saved blob (DOM-rendered cost, structural field equality, lint-must-pass). Read `eval/README.md` on interpreting failures — a raw count is not meaningful; use `--compare-baseline`.
135
141
 
136
- 424/429 tests pass with mocked I/O (5 skipped, network-dependent). Set `SKIP_NETWORK=1` for offline runs.
142
+ `npm test` sets `SKIP_NETWORK=1` itself, so a clean checkout is green offline. The
143
+ network-dependent tests declare the gate at their `describe`, following the
144
+ convention in `test/author-catalog.test.js`.
145
+
146
+ ### CI
147
+
148
+ `.github/workflows/ci.yml` runs two jobs on every push to `main` and every PR:
149
+
150
+ - **test** — `npm ci && npm test` on Node 22 and 24.
151
+ - **dist-freshness** — rebuilds the bundle and requires `dist/mcp-server.js` and
152
+ `dist/bundle-contract.json` to be byte-identical to what is committed. `dist/`
153
+ is a tracked artifact that consumers install (see `files` in `package.json`),
154
+ so a source change with a stale bundle ships nothing. Expect this job to fail
155
+ on a dependency bump until you rerun `npm run build` — deps are bundled, so
156
+ that is a true positive. `dist/aws-calculator.zip` is not compared, since zip
157
+ embeds mtimes.
137
158
 
138
159
  ## Architecture
139
160
 
@@ -142,12 +163,12 @@ Three layers, no overlap:
142
163
  │ MCP Client │◄───────────────────►│ MCP Server │
143
164
  │ (Kiro, Claude, │ JSON-RPC over │ │
144
165
  │ Cursor, etc.) │ stdin/stdout │ mcp-server.js (entry point) │
145
- │ │ — or, with │ ├── lib/aws-client.js │
146
- │ │ MCP_TRANSPORT │ ├── lib/estimate-builder.js │
147
- │ │ =http, JSON-RPC │ ├── lib/ec2.js (EC2 transform) │
148
- │ │ over HTTPS │ ├── lib/validation.js │
149
- └─────────────────┘ │ ├── lib/can-rehydrate.js (lint) │
150
- │ └── lib/catalog.js │
166
+ │ │ — or, with │ ├── lib/aws/ (client, builder) │
167
+ │ │ MCP_TRANSPORT │ ├── lib/lint/ (can-rehydrate, │
168
+ │ │ =http, JSON-RPC │ │ validation,catalog)│
169
+ │ │ over HTTPS │ ├── lib/mcp/ (handler-helpers) │
170
+ └─────────────────┘ │ ├── lib/trace/ (trace-logger) │
171
+ │ └── lib/store/ (estimate-store) │
151
172
  └──────┬─────────┬─────────┬──────────┘
152
173
  │ │ │
153
174
  GET │ GET │ POST │
@@ -178,7 +199,7 @@ On first use, the server fetches the AWS Calculator manifest from CloudFront (~4
178
199
 
179
200
  ### EC2 handling
180
201
 
181
- EC2 uses a custom config transform (`lib/ec2.js`) that converts agent-friendly fields (instance type, OS, pricing strategy) into the `ec2Enhancement` format the calculator expects. Supports On-Demand, Savings Plans, Reserved Instances, and Spot pricing.
202
+ EC2 uses a custom config transform (`lib/aws/ec2.js`) that converts agent-friendly fields (instance type, OS, pricing strategy) into the `ec2Enhancement` format the calculator expects. Supports On-Demand, Savings Plans, Reserved Instances, and Spot pricing.
182
203
 
183
204
  ### Partition support
184
205
 
@@ -383,7 +404,7 @@ Trace events are typically captured to log groups that may be queryable by anyon
383
404
 
384
405
  Both fields are replaced with `[redacted: <N> chars]` in `tool.call` args and `tool.result` resultText. Structure is preserved (you still see "the agent set a description") but the content isn't. `resultLength` reflects the original pre-redaction length. Non-sensitive fields — service codes, region IDs, field IDs, numeric values, UUIDs — pass through untouched.
385
406
 
386
- To extend redaction for a new sensitive field, add the key to `SENSITIVE_KEYS` in `lib/trace-logger.js`. The redactor walks nested structures recursively, including the JSON-stringified `services` arg of `add_service` / `build_estimate`.
407
+ To extend redaction for a new sensitive field, add the key to `SENSITIVE_KEYS` in `lib/trace/trace-logger.js`. The redactor walks nested structures recursively, including the JSON-stringified `services` arg of `add_service` / `build_estimate`.
387
408
 
388
409
  The `mcpSessionId` field has three states, useful for telling transports apart:
389
410
 
@@ -54,7 +54,7 @@
54
54
  "defaultFields": {
55
55
  "type": "object",
56
56
  "additionalProperties": true,
57
- "description": "Per-field literal saved-blob shapes injected at add_service time when the agent omits the key. Use for fields the calculator silently demands but the manifest does NOT mark validations.required (the case where get_service_fields doesn't expose them and the lint can't refuse without them). Authors declare the canonical envelope; lib/handler-helpers.js#applyDefaultFields merges any missing keys into the user's config before estimate.addService."
57
+ "description": "Per-field literal saved-blob shapes injected at add_service time when the agent omits the key. Use for fields the calculator silently demands but the manifest does NOT mark validations.required (the case where get_service_fields doesn't expose them and the lint can't refuse without them). Authors declare the canonical envelope; lib/mcp/handler-helpers.js#applyDefaultFields merges any missing keys into the user's config before estimate.addService."
58
58
  }
59
59
  },
60
60
  "allOf": [
@@ -14,7 +14,7 @@
14
14
  },
15
15
  {
16
16
  "field": "pricingStrategy",
17
- "hint": "Pricing model. Accepts shorthand strings for simple cases OR a full object for committed pricing.\n\nShorthand (On-Demand/Spot only): \"ondemand\", \"spot\"\n\nFull object (required for any committed/discounted pricing):\n- EC2 Instance Savings Plans: {\"model\":\"instanceSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n- Compute Savings Plans: {\"model\":\"computeSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n- Standard Reserved Instances (dedicated tenancy only): {\"model\":\"standard\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n- Convertible Reserved Instances (dedicated tenancy only): {\"model\":\"convertible\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n\nValid term values: \"1 Year\", \"3 Year\" (case-insensitive; shorthand \"1yr\"/\"3yr\" also accepted). Term defaults to \"3 Year\" (the cheaper, deeper-commitment Savings Plan) when omitted or unrecognized — pass \"1 Year\" explicitly if you do not want the 3-year commitment.\nValid upfrontPayment values: \"None\", \"Partial\", \"All\" (exact case)\n\nCRITICAL: Do NOT use shorthand \"reserved\" or \"instanceSavings\" for committed pricing — these may silently fall back to On-Demand or produce $0 costs. Always use the full object format for any non-OnDemand, non-Spot pricing. EstimateBuilder normalizes via lib/ec2.js.",
17
+ "hint": "Pricing model. Accepts shorthand strings OR a full object.\n\nShorthand: \"ondemand\", \"spot\", or committed shorthands like \"instanceSavings3yrAllUpfront\", \"computeSavings1yrNoUpfront\".\n\nFull object (recommended for committed pricing):\n- EC2 Instance Savings Plans: {\"model\":\"instanceSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n- Compute Savings Plans: {\"model\":\"computeSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n- Standard Reserved Instances (dedicated/host tenancy only): {\"model\":\"reserved\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"} (alias \"standard\" also accepted)\n- Convertible Reserved Instances (dedicated/host tenancy only): {\"model\":\"convertible\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}\n\nValid term values: \"1 Year\", \"3 Year\" (shorthands \"1yr\"/\"3yr\" also accepted). An OMITTED term defaults to \"3 Year\" (the deeper-commitment, cheaper Savings Plan) — pass \"1 Year\" explicitly if you do not want the 3-year commitment. A term that is present but unrecognized is rejected.\nValid upfrontPayment values: \"None\", \"Partial\", \"All\" (case-insensitive; \"No Upfront\" style accepted)\n\nDisplay labels like \"EC2 Instance Savings Plans\" are also accepted and normalized. Any value that does not resolve is REJECTED at add_service with the valid values listed — nothing silently falls back to On-Demand. EstimateBuilder normalizes via lib/aws/ec2.js.",
18
18
  "enum": [
19
19
  "ondemand",
20
20
  "reserved",
@@ -28,18 +28,15 @@
28
28
  ],
29
29
  "traps": [
30
30
  "Use serviceCode 'ec2Enhancement' with templateId 'template'. The legacy (eC2Next, quickEstimate) format saves cleanly but renders Read-only — the calculator cannot rehydrate it. EstimateBuilder forces this transformation automatically; do not bypass it.",
31
- "Reserved Instances (standard, convertible) are HIDDEN under shared tenancy. For shared tenancy with committed pricing, ALWAYS use the full object format: {\"model\":\"instanceSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"} or {\"model\":\"computeSavings\",\"term\":\"1 Year\",\"upfrontPayment\":\"None\"}. The shorthand \"reserved\" and \"instanceSavings\" strings are unreliable and may produce On-Demand pricing or $0 costs. Real Standard/Convertible RIs require tenancy:\"dedicated\" or \"host\".",
32
- "pricingStrategy term accepts '1 Year' or '3 Year' (case-insensitive; shorthand '1yr'/'3yr' also works). Anything else — including an omitted term — defaults to '3 Year'. Pass '1 Year' explicitly when you do not want the 3-year commitment.",
33
- "pricingStrategy upfrontPayment values: 'None', 'Partial', 'All'. EstimateBuilder accepts shorthand 'No' and converts to 'None'.",
34
- "The calculator silently demands tenancy, workload, workloadSelection, dataTransferForEC2, detailedMonitoringCheckbox, and ec2AdvancedPricingMetrics — even though the PCT does not mark them validations.required. EstimateBuilder injects all of these via lib/ec2.js#transformConfig.",
31
+ "Reserved Instances (standard, convertible) are HIDDEN under shared tenancy. When you request model \"reserved\"/\"standard\" or \"convertible\" with shared tenancy, EstimateBuilder remaps to instanceSavings/computeSavings (the shared-tenancy equivalents) and emits a trace event. Real Standard/Convertible RIs require tenancy:\"dedicated\" or \"host\".",
32
+ "pricingStrategy values are validated at add_service: unresolvable models/terms/upfrontPayment are rejected with the valid values listed — nothing silently falls back to On-Demand. Accepted terms: '1 Year', '3 Year' (also '1yr'/'3yr', case-insensitive); omitted term defaults to '3 Year' — pass '1 Year' explicitly for the shorter commitment. Accepted upfrontPayment: 'None', 'Partial', 'All' (case-insensitive; 'No Upfront' style accepted).",
33
+ "The calculator silently demands tenancy, workload, workloadSelection, dataTransferForEC2, detailedMonitoringCheckbox, and ec2AdvancedPricingMetrics — even though the PCT does not mark them validations.required. EstimateBuilder injects all of these via lib/aws/ec2.js#transformConfig.",
35
34
  "utilization is HIDDEN inside the pricingStrategy component and does NOT appear in get_service_fields' field list. To set utilization, pass top-level utilization: 'N' (string, 1–100); EstimateBuilder routes it into pricingStrategy.value.utilizationValue.",
36
35
  "workload is the INSTANCE COUNT — use it for the user's quantity. For 'two instances' set workload: 2 (one entry) or save two entries with workload: 1 each. Either is fine. Do NOT save workload: 1 for 'two instances' and stash the '2' only in description (the saved estimate prices for one instance and the user reads two). Do NOT compensate for utilization with workload (e.g. workload: 0.8 to fake 80% utilization) — the total cost may match but the saved estimate structurally lies about how many instances are running. Use the explicit utilization field instead.",
37
36
  "The dataTransferForEC2 field is a composite widget. Its value must be an array of transfer entries, each with entryType, value, unit, and a region field. Valid entryTypes: INBOUND, OUTBOUND, INTRA_REGION.",
38
37
  "Unit must be \"tb_month\" or \"gb_month\".",
39
38
  "OUTBOUND entries require a \"toRegion\" field — use \"External\" for internet or \"other\" for other AWS regions. INBOUND entries require a \"fromRegion\" field with the same options. INTRA_REGION requires neither.",
40
- "All three entryTypes (INBOUND, OUTBOUND, INTRA_REGION) must be present in the array even if their value is \"0\".",
41
- "For Dedicated Hosts (tenancy: \"host\"): instanceType must be the instance FAMILY only (e.g. \"m5\", \"c6g\"), NOT a specific size (e.g. \"m5.metal\", \"m5.xlarge\"). Do NOT set selectedOS — OS is not configured at the host level."
42
-
39
+ "All three entryTypes (INBOUND, OUTBOUND, INTRA_REGION) must be present in the array even if their value is \"0\"."
43
40
  ],
44
41
  "minimalConfig": {
45
42
  "region": "us-east-1",
@@ -52,9 +49,23 @@
52
49
  "defaultFields": {
53
50
  "dataTransferForEC2": {
54
51
  "value": [
55
- { "entryType": "INBOUND", "value": "", "unit": "tb_month", "fromRegion": "External" },
56
- { "entryType": "OUTBOUND", "value": "", "unit": "tb_month", "toRegion": "External" },
57
- { "entryType": "INTRA_REGION", "value": "", "unit": "tb_month" }
52
+ {
53
+ "entryType": "INBOUND",
54
+ "value": "",
55
+ "unit": "tb_month",
56
+ "fromRegion": "External"
57
+ },
58
+ {
59
+ "entryType": "OUTBOUND",
60
+ "value": "",
61
+ "unit": "tb_month",
62
+ "toRegion": "External"
63
+ },
64
+ {
65
+ "entryType": "INTRA_REGION",
66
+ "value": "",
67
+ "unit": "tb_month"
68
+ }
58
69
  ]
59
70
  }
60
71
  }