ADR-023: A Measured Context Budget for Wiring MCP Servers
| Field | Value |
|---|---|
| Status | Proposed |
| Date | 2026-09-13 |
| Decision Drafters | product-owner + cloud-architect + developer-experience-engineer under scope_id mcp-aws-azure-capability-pages-2026-09-13 |
| Decision Makers | HITL approval required before this ADR may be marked Accepted |
| Scope | How AWS and Azure MCP servers are admitted to the catalog, wired to products, and documented |
| Review trigger | TR-1 through TR-4 below |
Context
The /mcps page was scored 6/10. Two sections earned their place; two did not. The weak
half shared one root cause: the catalog could say a server existed without anyone having
proven it ran, fenced it, or given a reader a reason to care.
That gap produced three concrete defects, each measured rather than asserted:
- Boilerplate presented as a guide. Every generated tool prompt was one template —
Use the <tool> tool from the <server> MCP server for my current task: <description>— with the description pasted twice.docs/docs/mcp/aws.mdxwas 817 lines of it. Forawslabs-apithe tool descriptions are AWS's own deprecation notice, so the copy-paste prompt instructed the agent to announce a deprecation. - Silent content loss.
_meta.playbooksauthored 5 entries; the page rendered 3.build_playbooks()excludes any playbook whose servers are not allmeasured/verified, and nothing surfaced the exclusion. 40% of authored content was invisible, including to its own authors. - The
blockedclaim was accurate for its environment._meta.roadmaprecordedawslabs-iam,awslabs-cloudtrailandawslabs-cloudwatchasblockedbyModuleNotFoundErrorin the canonical image. Re-tested on 2026-09-13 on the host at identical pins: all three start correctly. Both records were accurate for their respective environments. The block persists in the canonical image due to a PYTHONPATH leak in the runbooks Dockerfile (/app:/venv/lib/python3.13/site-packages), not due to the servers themselves. Cleared or on the host, they function normally.
Underneath all three sits the decision this ADR exists to settle. A wired MCP server is not
a command an agent chooses to run — it is a standing tool surface loaded into every
session of every product in its _targets. The 15 AWS servers under consideration would
have added an unmeasured 150-250 tools, and six of them can mutate AWS resources. The cost
is paid by every session, forever. The benefit is paid once, on a documentation page.
Before this ADR, no tool-budget policy existed anywhere under .claude/mcps/ — a grep
returned zero hits. Wiring decisions were therefore taken without a number to evaluate them
against.
Decision
Admission to the catalog, wiring to products, and documentation quality are three separate gates. A server may pass one and fail the next.
| ID | Decision | Where recorded |
|---|---|---|
| D1 | Measure every candidate server; wire only a curated subset; unwired servers render as measured/available | §1 budget, §2 tiers |
| D2 | Azure mirrors AWS: the same capability groups, populated from the measured @azure/mcp namespaces, so both pages read as one system | §6 |
| D3 | Replace generated per-tool boilerplate with authored setup (single account and landing zone) + one scenario per server | §4 |
| D4 | Resolve the awslabs-api → aws-mcp migration by probing the successor and recording what is measured | Consequences |
1. A measured context budget, with a derived ceiling
Session weight is defined as core-plugin tools (60, fixed) + measured tools of every product-tier server whose _targets contains this product.
The ceiling is 400 tools per product session. It is derived, not invented: the measured
incumbent maximum today is 384 (the largest current project: 324 product + 60 core), plus 16 tools of
headroom. The policy it encodes is "no product session may become materially heavier than
the heaviest one we already ship and operate." Exceeding it requires an explicit HITL
waiver recorded at _meta.budget_waivers[] with measured before/after figures.
The gate runs after measurement and before any _targets value is finalised. A
wiring decision taken before the gate has a number to evaluate is a forecast, not a design.
2. Three wiring tiers, not two
| Tier | Shape | Session cost | Meaning |
|---|---|---|---|
| Wired | _targets non-empty | Full tool count, every session | Measured, fenced, and under budget |
| Available, not wired | _targets: [], _status: measured | Zero | Probed and printable on the page; loadable on demand |
| Roadmap | _meta.roadmap entry | Zero | Named, with the specific reason it is not admitted |
The middle tier is the load-bearing one. It lets the catalog tell the truth about a server's capability without charging every session for it.
3. A verified read-only fence is a precondition of wiring, fail-closed
Not a warning, not a follow-up. Unverifiable fence ⇒ not wired ⇒ not added. The fence
must be verified against the server's own --help at the pinned version, never inferred
from a flag name.
Applying this rule rejected 5 of 12 candidates on evidence:
| Server | Why rejected |
|---|---|
awslabs-postgres | --allow_write_query does not gate its separate create_cluster tool, which creates a real Aurora cluster |
awslabs-lambda-tool | Pure invoke surface; no read-only mode exists |
awslabs-stepfunctions | Pure invoke surface; no read-only mode exists |
awslabs-security-agent | Auto-provisions an IAM role and S3 bucket; generic call_api with no fence |
awslabs-sns-sqs | Its only flag (--allow-resource-creation) does not gate purge_queue, delete_message or send_message |
The case of awslabs-sns-sqs carries genuine use-case value, yet it is still excluded
and recorded with a named re-evaluation trigger. A fence that does not cover the destructive
tool is not a fence.
4. Business scenarios are authored in the SSOT, never generated
Three new SSOT fields carry what a generator cannot invent: _group (the AWS Labs
capability taxonomy), _scenario (one real outcome, naming only tools that exist in
measured output), and _setup (both a single-account and a multi-account Landing Zone
path, expressed purely through environment-variable indirection). _scenario.measured_count
has minimum: 1, which forces the no-data case down an explicit no_data branch rather
than letting a zero be quietly styled as a finding.
5. Scaffolds stay behind the curtain
INVEST, 5W1H, and why / what_if_missing / business_value / purpose / critical_thinking are
generative scaffolds — they are how an agent guarantees it covered every angle. They belong
in coordination logs, this ADR, and the rules layer. They are never customer-facing, and
neither is a CEO/CTO/CFO/CDO/CSO persona taxonomy. Pages are organised by AWS Labs' own
capability groups and speak in outcomes. Enforced by a grep gate over rendered strings.
6. Azure mirrors AWS through namespaces
The azure MCP server runs in namespace mode, exposing 60 measured namespaces across five capability groups: identity, network, compute, storage, and analytics. These namespace groups are assigned in the SSOT at .claude/mcps/.mcp-enterprise.json mcpServers.azure._namespace_groups, forming a set identical to the measured tool names. The Azure page renders a capability group when it has either Azure servers (wired to that group) OR Azure namespaces (assigned to that group), enabling Azure content to populate alongside AWS. Tool counts per group are summed per brand, ensuring that Azure namespaces appear only on Azure pages and AWS tools only on AWS pages.
7. Public outputs carry no tenant facts
Per-tenant data in screenshots, cost figures, resource counts, or alarm states is a customer's live security and cost posture. Publishing it discloses reconnaissance detail, breaches confidentiality expectations (APRA CPS 234-class), goes stale and then contradicts reality, and identifies the customer. Rule: public outputs carry only vendor-level facts (server, tools/list tool counts, read-only status). Tenant terms are read from _meta.tenants at run time, never hard-coded. What if kept: the customer finds their own posture on a vendor site → trust and contract loss; an attacker learns which alarms fire and which subscriptions are off; a board reader sees per-month cost figures beside agents and concludes low value.
Consequences
Gains
- Every printed tool count traces to a probe, and every wired server to a verified fence.
- Nine servers become printable at zero session cost via the available-not-wired tier.
- Four write-capable servers are kept out of production sessions on evidence, before anyone had to discover the gap through an incident.
- The budget ceiling converts an unbounded "add another server" pressure into an explicit, reviewable waiver.
- Public outputs display "N MCP tools" per item and group (no per-month cost figures), and orbit tiles use official AWS Architecture Icons (release 07312026) unmodified, ensuring readers recognize the actual AWS services agents operate on.
Group ordering and narrative
The AWS MCP orbit displays five groups in C-suite story order, each defined in the catalog:
- Cost & Operations — "See your spend, alarms, and who changed what."
- Developer Tools — "Find who can reach admin. Get current Terraform guidance."
- Infra & Deployment — "See what runs in containers and serverless."
- Data & Analytics — "Query S3 Tables and Aurora data. Check DynamoDB designs first."
- Integration & Messaging — "Answer questions from your REST APIs; more next."
Narrative arc: The sequence answers the executive questions, in this order: What is our spend and who changed what? (Cost ↓) → Who can access admin? (Risk ↓) → What services are running? (Time-to-value ↑) → What data can we query? (Insight) → What's next? (Connect).
Video sequence: A presenter navigates the orbit in rank order, using each group's catalog line as narration.
| Scene | On screen | Narration source |
|---|---|---|
| Opening | Hub: "Your AI agents · 144 tools connected" | capability_showcase.AWS.hub.tools_measured |
| Step 1 | Cost & Operations highlighted; "Step 1 of 5" slide | Cost & Operations line |
| Transition | Animated dot travels ring to next group | (visual) |
| Step 2 | Developer Tools highlighted; "Step 2 of 5" slide | Developer Tools line |
| Transition | Dot travels to Step 3 | (visual) |
| Step 3 | Infra & Deployment highlighted; "Step 3 of 5" slide | Infra & Deployment line |
| Transition | Dot travels to Step 4 | (visual) |
| Step 4 | Data & Analytics highlighted; "Step 4 of 5" slide | Data & Analytics line |
| Transition | Dot travels to Step 5 | (visual) |
| Step 5 | Integration & Messaging highlighted; "Step 5 of 5" slide | Integration & Messaging line |
| Transition | Dot travels from Step 5 back to Step 1; the ring loop closes | (visual) |
| Closing | Orbit with roadmap section visible | "more next" narrative from Step 5 line |
Every number and line in this table is read from docs/static/data/mcp-catalog.json at build time; the video producer MUST re-read it rather than copy today's values. Current tool count (2026-09-13): 144. Out of scope: tenant facts, resource counts, per-month costs, per-tenant resource states.
Losses / open risks
- The catalog is now smaller than the ambition. Five of the fifteen named AWS servers do not ship as wired capability. That is the correct outcome, but it is a real reduction against the original request.
- The 400 ceiling is anchored to today's incumbent, not to a measured degradation point. We do not know the tool count at which agent instruction-adherence actually degrades. The ceiling is a defensible convention, not a discovered threshold.
- Two acceptance criteria cannot be closed by a command. Playbook contrast quality and
page voice require a non-producer reader verdict. A grep proves "CFO" is absent; it cannot
prove the prose is good. Treating a mechanical check as proof here would be
GREP_PROXY_FOR_JUDGMENT. awslabs-ecsbreaks on any fresh install of the container image. The server pinsfastmcp>=3.2.0with no upper bound. On 2026-09-05, fastmcp 4.0.3 removedadd_tool_transformation, causing startup crashes. The server remains wired to 4 products and was corrected in the SSOT with--with fastmcp<4in the launch args. This found a second class of problem: hidden by the PYTHONPATH leak, a write-capable server was wired while in a permanently broken state in the canonical test image.- The measuring instrument had structural defects. The stock
list-mcp-tools.pyprobe has no per-read timeout (one stalled server blocks all others) and inherits the runbooks image's PYTHONPATH leak and credential-mount issues. Two durable fixes remain out of scope: (1) scopePYTHONPATHin the.devcontainer/runbooks/Dockerfileinstead of image-wide, and (2) pass-e PYTHONPATH=plus read-write mount of the container user's AWS config directory in themcp:toolsTaskfile step. Both are infrastructure-engineer decisions documented as named next steps, not silent gaps. awslabs-apiremains wired to 6 products while deprecated upstream. Its successormcp-proxy-for-awsuses different profile semantics, so a like-for-like swap would silently change which credentials six products use. The swap is deliberately deferred.
Alternatives Considered
| Alternative | Weighted score | Verdict | When to reconsider |
|---|---|---|---|
| Wire all 15 servers to all relevant products | 34/100 | Deferred — adds 150-250 unmeasured tools to every session and admits 6 write-capable servers without verified fences | If per-server lazy tool loading lands, making session weight a function of use rather than configuration |
| Catalog-only; probe nothing, wire nothing | 41/100 | Deferred — zero cost and zero risk, but reproduces the exact unverified quality that scored 2/10 | If the page's purpose narrows to link-directory only |
| Wire everything, fence at the agent-permission layer instead of the server | 48/100 | Deferred — permission prompts are per-session and bypassable; a server-level fence is structural | If a deny-list gains per-tool granularity that is enforced server-side |
| Three tiers + measured budget + fail-closed fence | 86/100 | Adopted | TR-1..TR-4 below |
| Fork the AWS Labs servers to add missing read-only modes | 29/100 | Deferred — solves the fence gap properly but creates a permanent maintenance obligation across 5 forks | If a rejected server becomes business-critical and upstream declines the fence |
Review triggers
- TR-1 — A product session measures over 400 tools without a recorded waiver.
- TR-2 — Upstream adds a genuine read-only mode to any of the 5 rejected servers.
- TR-3 —
mcp-proxy-for-awsreaches profile parity withawslabs-api, unblocking the swap. - TR-4 — Evidence emerges of a real degradation threshold, replacing the 400 convention with a measured number.
Evidence
| Claim | Artifact |
|---|---|
| PO stories and acceptance criteria (10 stories, 42 ACs, 7 risks) | tmp/adlc-framework/coordination-logs/mcp-aws-azure-capability-pages-2026-09-13/product-owner.json |
| Budget derivation, fence design, Azure mapping, D4 probe | tmp/adlc-framework/coordination-logs/mcp-aws-azure-capability-pages-2026-09-13/cloud-architect.json |
Live re-test refuting 3 stale blocked claims | tmp/adlc-framework/evidence/mcp-aws-server-retest-2026-09-13.md |
| Server catalog and pins | .claude/mcps/.mcp-enterprise.json (40 servers) |
| Upstream deprecation | awslabs/mcp src/aws-api-mcp-server/MIGRATION.md |
| Terraform supersession | PyPI yank reason on awslabs.terraform-mcp-server |
| PYTHONPATH leak diagnosis (image vs. host) | tmp/adlc-framework/evidence/mcp-container-pythonpath-leak-2026-09-13.md |
| Bounded MCP tool probe (AWS servers, cleared environment) | tmp/adlc-framework/evidence/mcp-tools-probe-aws-bounded-2026-09-13.json |
A note on one rejected package. awslabs.terraform-mcp-server is yanked on PyPI with the
reason "Superceeded by https://github.com/hashicorp/terraform-mcp-server". This settles a
question that had been open in planning: Terraform's MCP server is HashiCorp's, and it
was already correctly wired here as a digest-pinned container with ENABLE_TF_OPERATIONS=false.
No change was required — the evidence simply confirmed an existing decision, which is worth
recording so the question is not reopened.
Status note
Status is Proposed. Accepted is reserved for HITL sign-off. The three environment-specific
blocked claims are documented accurately in the SSOT (reflecting both image and host
measurements); azure-devops remains blocked because it was not re-tested this session
— environment-specific claims must be labeled as such, never promoted on the basis of a
different environment's test alone.