MCP Strategies on AWS, Part 4: Governance in Code
written by Stefan Christoph
- 8 minutes readThe hosting post bounded who can reach an MCP server. This one goes inside it. Governance, in the AWS guidance, covers authentication and authorization, controlling load, and operational metrics [1]. I am going to make the first two runnable (both demos are on GitHub) because the principle behind them is the single most important idea in the document, and it is easy to nod along to and hard to actually feel until you watch it work.
Token isolation: the story worth running
Here is the canonical example from the guidance, almost verbatim. An admin user asks an agent to clone a production database to pre-prod. That task needs two permissions: read, to copy the data, and create, to make the target. It does not need delete.
But LLM-driven agents can behave non-deterministically. The model might invent a step that was never asked for: a “let me clean up the old copy” flourish that tries to delete production. If the agentic system reused the admin’s own credentials, which include delete, that hallucination would be authorized, and in this deliberately simplified story, with no deletion protection or confirmation gate standing in the way, production would be gone. The guidance’s principle, which it calls out as the headline: MCP tools must use their own purpose-generated, scoped-down tokens, and user credentials must not propagate through the system [1].
The same hallucinated delete, two outcomes. A scoped read-create token denies it and production survives. Reusing the admin’s credentials executes it and production is gone.
I built this against a mock database service that checks the token’s scope on every operation, then ran the identical hallucinated workflow under two tokens. This is the actual output:
Measured output of token_isolation.py:
[scoped-read-create] scopes=['create', 'read']
- read prod (ok)
- create preprod (ok)
- delete prod (DENIED — token 'scoped-read-create' lacks scope 'delete')
=> SAFE — prod still exists
[admin-all] scopes=['create', 'delete', 'read']
- read prod (ok)
- create preprod (ok)
- delete prod (EXECUTED!)
=> DISASTER — prod was deleted
That is the whole argument in eight lines. The simulated workflow attempted the same wrong delete in both runs, the demo tests the authorization boundary, not model behavior. The only difference was the token it held, and that difference was the gap between a normal Tuesday and an incident.
The enforcement is unremarkable code, which is the point: it belongs in the tool, on every call:
From token_isolation.py, the tool authorizes every operation:
def _authorize(self, token, scope):
if token.audience != self.server_id:
raise PermissionDenied(f"audience mismatch: token for '{token.audience}'")
if not token.allows(scope):
raise PermissionDenied(f"token '{token.name}' lacks scope '{scope.value}'")
Notice the first check. The guidance, following the MCP specification, says to validate the token’s aud (audience) claim so a token minted for one server cannot be replayed against another [2]. The demo enforces that too: a token addressed to a different server is rejected before any scope check runs.
On AWS, the service that manages these scoped tokens for agents (both machine-to-machine workload tokens and user-delegated tokens) is Amazon Bedrock AgentCore Identity, which keeps them in a token vault encrypted with AWS KMS [3]. The demo illustrates one authorization outcome scoped tokens produce, the deny, not that service’s implementation or full behavior; you would not hand-roll token storage in production.
The honest limit
I want to be precise about what this buys you, because it is easy to oversell. Token isolation limits the blast radius of a bad model step. It does not prevent the step. The model still tried to delete production. If the workflow had legitimately needed delete (say, a tool whose job is to tear down environments), a scoped token would have happily allowed it, and you would be relying on other controls. Scoping is necessary and it is not sufficient. The fuller answer layers defense in depth, read and write separation, human confirmation for destructive actions, and the injection defenses I flagged as gaps in the series overview are examples; which controls you actually need depends on your threat model. Token isolation is the floor, not the ceiling.
Controlling load, so an agent can back off
The second governance lever is rate limiting. The guidance recommends limiting primarily at the MCP-server level, using per-tool limits to protect downstream resources, and (the part agents need) returning the limit state in HTTP headers so the agent can self-manage instead of retrying blindly [1]:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640995200
I implemented a per-tool limiter that emits exactly those headers. Calling a tool limited to three per window, five times, produces:
Measured output of rate_limit.py:
call 1: 200 OK [ok] X-RateLimit-Limit=3 X-RateLimit-Remaining=2 ...
call 2: 200 OK [ok] X-RateLimit-Limit=3 X-RateLimit-Remaining=1 ...
call 3: 200 OK [ok] X-RateLimit-Limit=3 X-RateLimit-Remaining=0 ...
call 4: 429 Too Many Requests [per-tool limit] X-RateLimit-Remaining=0 ...
call 5: 429 Too Many Requests [per-tool limit] X-RateLimit-Remaining=0 ...
The fourth call is rejected with a 429 and a remaining count of zero. A well-behaved agent reads X-RateLimit-Reset and waits, rather than retrying into the wall. The guidance adds a useful rule for tools that fan out: if a tool calls several downstream APIs, start by setting its limit to the lowest those APIs allow, a first bound, not a guarantee, since a single invocation can make several downstream calls and those quotas are shared with every other consumer.
There is also load shedding, which is for overload that no single caller is responsible for. The demo approximates it with the bluntest possible instrument, a fixed global ceiling across all tools, real load shedding reacts to live capacity signals such as concurrency, queue depth, or latency, but the caller sees the same 429:
Measured output: global load shedding:
call 6 -> b_y_get 200 OK [ok]
call 7 -> a_x_get 429 [load-shed (global limit)]
call 8 -> b_y_get 429 [load-shed (global limit)]
Neither tool hit its own limit, but their combined load tripped the global ceiling. That is the difference between per-tool limiting (protect a downstream resource) and shedding at a global admission cap (protect the fleet).
The third lever: metrics
I did not write demo code for the third governance area, operational metrics, instrumenting, collecting, and alerting on them is very much a mechanism you implement, but the interesting choices are in what you measure, so I kept this one to a paragraph. The guidance recommends watching token usage per tool, tool-selection accuracy, the number of tools registered (an early warning for the context-window problem from the tool-design post), and tool latency and success rates. AWS describes building golden datasets for regression testing, generated synthetically from historical invocation logs, to measure whether the agent picks the right tool and fills its parameters correctly across turns [1]. That is the operational-excellence half of governance, and it gives you a shot at catching tool drift before a user does, for the failure modes your golden dataset actually covers.
Running it yourself
The companion code for this post is on GitHub at stechr/schristoph-blog-samples › 2026-06-12-mcp-strategies/governance. Both demos are pure standard library: no AWS, no network, no credentials:
python run_demo.py
You will see the token-isolation split and the rate-limit headers in a couple of seconds.
Where the series lands
Four posts, one guide. The overview mapped the three pillars and the build-versus-buy ladder. Tool design measured the token tax and the granularity trade-off. Hosting deployed a real account-bounded server and tore it down. This post made the security model run. If you take one thing from the whole series, take the eight lines above: the model can take a wrong step at any time, and when that step falls outside the granted scope, the token is what decides whether it is a near-miss or an incident.
Sources
- Model Context Protocol strategies on AWS (PDF), the governance pillar: token isolation, rate limiting and load shedding, operational metrics, and golden datasets.
- MCP specification, Authorization Security Considerations, audience validation and the ban on token passthrough.
- Securing AI agents with Amazon Bedrock AgentCore Identity and the AgentCore Identity docs, scoped workload and user tokens, KMS-encrypted token vault.
About the Author
Stefan Christoph is a Principal Solutions Architect at AWS, focused on agentic AI, media & entertainment, and helping builders move from demo to production. He writes about AI architecture, developer productivity, and the future of software.
This is a personal blog. Opinions expressed here are my own and do not represent the views or positions of my employer.