MCP Strategies on AWS, Part 3: Hosting, from Local to AWS
written by Stefan Christoph
- 8 minutes readThe tool-design post decided what your tools are. This one decides where they live. The AWS MCP guidance is candid that most MCP usage today is local, and it lays out a ladder you climb only as far as you need to [1]. All the code in this post (the local server, the CDK stack, the signing client, and the deploy/teardown scripts) is on GitHub.
The hosting ladder. Start local. Move to a remote server bounded to your account when you need auth and lifecycle control. Reach for a managed gateway when you need a single endpoint and context optimization. Internet-facing is a separate, deliberate step.
Rung one: local, where you should start
A local MCP server runs as a subprocess on your machine and talks JSON-RPC over stdio. There is no client-to-server authentication to configure, because there is no network in between: the server runs with your own local credentials and files. That makes it the right place to start and iterate.
With the official SDK it is a few lines:
From local_server.py, a local stdio MCP server:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("mcp-strategies-demo-local")
@mcp.tool()
def word_count(text: str) -> dict:
"""Count words and characters. A read-only tool: it computes, it does not mutate."""
return {"words": len(text.split()), "characters": len(text)}
if __name__ == "__main__":
mcp.run(transport="stdio") # no network, no auth
The guidance is honest about the cost of staying here: you get no lifecycle control. Users self-discover, download, and configure the server, which means some of them run outdated or vulnerable versions, and compliance gets hard. That is the pressure that pushes you up the ladder.
Rung two: remote, bounded to one account
The remote rung is where I wanted to be concrete, because “remote” is where the security model from the governance pillar starts to matter, and where a vague blog post would hand-wave. So I deployed a real one.
The design choice that does the work is the authorizer. I used a Lambda Function URL with AuthType = AWS_IAM. The URL sits on a public hostname, but every request must be SigV4-signed by an IAM principal in the deploying account that holds lambda:InvokeFunctionUrl on the function. There is no wildcard resource permission. An unsigned or out-of-account caller is rejected before the handler runs. That is the account boundary, enforced at the edge by IAM rather than by anything in my code.
The CDK stack is small:
From cdk/app.py, the account boundary is the auth type:
fn = _lambda.Function(self, "McpServer",
runtime=_lambda.Runtime.PYTHON_3_12,
handler="handler.handler",
code=_lambda.Code.from_asset("lambda"),
environment={"MCP_ACCOUNT_ID": self.account})
# AWS_IAM auth = account boundary. No CORS (not browser-facing).
fn.add_function_url(auth_type=_lambda.FunctionUrlAuthType.AWS_IAM)
The handler is stdlib-only: MCP is JSON-RPC 2.0, so a remote server needs only to answer initialize, tools/list, and tools/call. Keeping it dependency-free keeps the deploy small and auditable. The client side is where the signing happens:
From sigv4_client.py, sign every request as the lambda service:
aws_req = AWSRequest(method="POST", url=url, data=payload,
headers={"Content-Type": "application/json"})
SigV4Auth(creds.get_frozen_credentials(), "lambda", region).add_auth(aws_req)
The live run
I deployed to a real account in eu-central-1, ran the signing client, and fired an unsigned request as a negative control. This is the actual output, with the account id redacted:
Real output from deploy.sh (account id redacted):
✅ McpStrategiesRemoteStack
✨ Deployment time: 36.56s
>>> DEMO: SigV4-signed in-account client
initialize -> {"name": "mcp-strategies-demo-remote", "version": "1.0.0"}
tools/list -> ['demo_echo', 'demo_account_context']
tools/call demo_account_context -> {"account": "123456789012", "region": "eu-central-1", "function": "mcp-strategies-demo-remote"}
OK: a SigV4-signed, in-account client reached the remote MCP server, listed its tools, and invoked one.
>>> DEMO: negative control — unsigned curl should be 403
unsigned GET status: 403
That 403 is the whole point. A request without a valid in-account signature does not reach the tool. The signed client, using credentials from a principal in the same account, gets through and invokes the tool, which reports back the account and region it ran in: proof the call stayed inside the boundary.
Tearing it down, for real
A blog post that deploys real infrastructure owes you a teardown, and a careful one. The deploy driver registers a teardown trap before it deploys anything, so the stack is destroyed even if the process is killed mid-run:
From deploy.sh, teardown that survives a killed process:
cleanup() { echo ">>> TRAP: running teardown"; bash "$HERE/teardown.sh"; }
trap cleanup EXIT SIGTERM SIGINT
The teardown does not just call destroy and trust it. It verifies:
Real teardown output:
✅ McpStrategiesRemoteStack: destroyed
[teardown] verified: mcp-strategies-demo-remote is gone
[teardown] stack status: DELETE_COMPLETE
[teardown] clean.
The resources existed for about a minute. This matters beyond tidiness: the first time I ran the driver it failed on a shell-quoting bug and deployed nothing, and the trap still ran the teardown and confirmed a clean account. A teardown you trust is one that checks, not one that assumes.
Rung three: the managed gateway
The top rung is a managed MCP gateway. On AWS that is Amazon Bedrock AgentCore Gateway, a managed service that acts as a unified entry point for agents to reach tools over MCP, handling authentication and invoking callable targets like Lambda functions and REST APIs [2]. It adds something my hand-rolled remote server does not: semantic tool search, which ties straight back to the token-tax problem from the tool-design post. The gateway can present a scoped-down toolset instead of shipping every definition on every call. One detail worth knowing before you build: semantic search must be enabled when you create the gateway and cannot be added later [2].
I did not deploy a gateway here, because the account-bounded Lambda is enough to show the access model concretely. The gateway is the graduation path when you have many servers and want one endpoint, central auth, and context optimization.
Extending to internet-facing, deliberately
The deploy above is account-bounded on purpose. Making an MCP server reachable from the internet is a separate, deliberate decision, and the governance pillar tells you what it requires. The path, which I describe in the code comments but do not deploy:
- Swap the authorizer. Replace IAM auth with a JWT or OIDC authorizer (Amazon Cognito, or Amazon Bedrock AgentCore Identity issuing scoped tokens) or front the Lambda with an API Gateway HTTP API [3].
- Validate the audience claim. Per the MCP specification, the server must check that a token’s
audclaim names this server, so tokens minted for a different server are rejected [4]. - Add a public-edge defense. AWS WAF with rate-based and IP or geo rules.
I kept those undeployed on purpose. The cheapest way to avoid an exposed agent endpoint is to not open one until you have the authorizer, the audience check, and the edge protection in place: in the same deploy, not as a follow-up.
Running it yourself
The companion code for this post is on GitHub at stechr/schristoph-blog-samples › 2026-06-12-mcp-strategies/hosting:
export MCP_ACCOUNT_ID=<your-account-id>
export MCP_REGION=eu-central-1
export MCP_PROFILE=<your-aws-profile>
bash deploy.sh
The driver bootstraps, deploys, runs the signed client and the 403 negative control, then tears down and verifies. No account ids are baked into the code: everything comes from the environment.
Up next: Part 4, Governance in Code
This post bounded who can reach the server. Part 4 goes inside it: the token-isolation model where a scoped token makes a hallucinated delete fail safely, and per-tool rate limiting with the standard headers. It is the security story the deploy here only set the stage for.
Sources
- Model Context Protocol strategies on AWS (PDF) — the hosting pillar: local, remote, and gateway.
- Create an Amazon Bedrock AgentCore Gateway — managed MCP entry point; semantic search is set at creation.
- Provide identity and credential management with Amazon Bedrock AgentCore Identity.
- MCP specification — Authorization Security Considerations — audience validation and the ban on token passthrough.
- AWS Lambda — Function URL access control with IAM.
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.
❤️ Created with the support of AI (Kiro)