You are in Claude Code. You have just finished a service that needs a queue, a bucket and a database. The agent can write the Terraform for all three. It cannot create any of them.
So you alt-tab. You open the console, or you copy the generated code into a repo, open a PR, wait for CI, and come back twenty minutes later to find you named the queue wrong.
The gap is not the model. The model knows what an SQS queue is. The gap is that nothing connects it to your actual cloud account in a way anyone would let near production.
That is the gap an MCP server fills.
What is an MCP server, exactly?
MCP stands for Model Context Protocol. It is an open standard, originally published by Anthropic, for connecting AI assistants to external tools and data.
The shape is simple. There is a client (Claude Code, Claude Desktop, Cursor, or any other MCP-capable app) and a server (a program that exposes capabilities). They talk over JSON-RPC. The server advertises what it can do, the client shows those capabilities to the model, and the model calls them by name with structured arguments.
A server can expose three kinds of thing:
- Tools: actions the model can invoke.
create_bucket,get_status,list_regions. - Resources: data the model can read. A config file, a service catalogue, a schema.
- Prompts: reusable templates a user can trigger deliberately.
For infrastructure, tools are the part that matters.
The important word in all of this is typed. An MCP tool is not a text description of an API. It declares its name, what it does, and a schema for its inputs. The model does not guess a curl command. It fills in a form.
That distinction is the reason this works at all. A model asked to "run the right AWS CLI command" will occasionally invent a flag. A model asked to fill in { name: string, region: string, versioning: boolean } either fills it in or fails validation.
What does an MCP server for AWS actually do?
Here is the honest answer: much less than people expect, and that is the point.
A naive design exposes the AWS SDK. Every service, every operation, a thousand tools. It demos well and it is unusable, because the model now has to pick correctly from a thousand options with no idea which combination your organisation considers acceptable.
A useful design is narrow. Something closer to:
list_supported_resources what can I create here?
describe_resource what does this resource need, and what does it look like now?
provision_resource create one, with these parameters
provision_service create an application service and its dependencies
trigger_deployment ship it
get_deployment_status did that work?
Six or seven verbs. Not a thousand.
The narrowness is doing real work. list_supported_resources is how the model learns that your platform supports S3, SQS, DynamoDB, ElastiCache and RDS, and does not support whatever it was about to hallucinate. describe_resource is how it learns which parameters your organisation actually requires, including the ones that are not in the AWS docs because they are yours: cost centre, environment tag, owning team.
The server is not a passthrough. It is an opinion about what a developer is allowed to ask for.
What it should refuse to do
Equally important, and less discussed:
- No raw SDK escape hatch. The moment you add
run_aws_command, every guardrail above it becomes decorative. - No delete without a separate, explicit path. Creating is recoverable. Destroying often is not.
- No credentials in the conversation. The server holds them. The model never sees them, so they cannot end up in a log, a context window, or a screenshot.
Why not just give the agent AWS credentials?
Because you would not do that for a person either.
Handing an agent an access key means it can do anything the key can do, in any order, with no record of intent. You get an audit trail of API calls with no explanation attached. Six weeks later, when something is wrong, you can see that a security group changed and you cannot see who asked for it or why.
An MCP server sits in between and gives you three things a raw key does not:
A vocabulary. Actions have names and arguments that a human can read in a log. provision_resource(type=sqs, name=payments-dlq, env=staging) is a sentence. A sequence of SDK calls is not.
A choke point. Policy lives in one place. If production requires an approval, that rule is in the server, not repeated in prompts that a model may or may not follow. A system prompt saying "do not touch production" is a suggestion. A server that has no production tool is a rule.
A boundary you can test. You can write tests against an MCP server. You cannot write tests against "the model will probably behave."
Who is the agent acting as?
This is the question that separates a demo from something you would run.
For a local MCP server launched over stdio, the answer is easy: it runs as you, on your machine, with your credentials.
For a remote MCP server, the one your whole team connects to, it is the entire design problem. The convenient answer is a shared service account with broad permissions. It works on day one and destroys your audit trail permanently, because every action from every engineer arrives under the same identity.
The alternative is that the server authenticates the human and acts on their behalf. MCP's authorization specification builds on OAuth 2.1 for exactly this: the client performs an OAuth flow, the user consents, and the server receives a token tied to that person.
The consequence is worth stating plainly: your existing permissions become the safety boundary. If a developer cannot create resources in production, the agent cannot create resources in production while working for that developer. Not because it was told not to, but because the token does not allow it.
That is a much stronger property than any prompt engineering, and it is the reason OAuth is in the spec rather than left as an exercise.
What can go wrong
Three failure modes worth knowing before you build or adopt one.
Tool sprawl. Every new tool makes selection harder. Past roughly twenty tools, accuracy starts to degrade because the model is choosing from a menu rather than following an obvious path. Fewer, better-named tools beat more.
Vague descriptions. The tool description is the prompt. "Creates a resource" tells the model nothing about when to use it. "Creates a new SQS queue in the given environment. Use for asynchronous work between services. Requires an existing service to own it." tells it exactly.
Silent partial success. An infrastructure action can succeed, then fail during apply, minutes later. If provision_resource returns success at the point of submission, the model will report success to the user and move on. Return a job identifier and make get_deployment_status the way anyone finds out what really happened.
What to look for
If you are evaluating an MCP server for infrastructure, four questions get you most of the way:
- Does it apply, or does it propose? A server that opens a pull request inherits your existing review process. A server that applies directly does not have one.
- Whose identity does it use? If the answer is a shared service account, your audit trail is already gone.
- How many tools does it expose? Six is a considered design. Two hundred is an SDK with extra steps.
- What does it do when a plan would destroy something? If the answer is "the same as anything else", that is your answer.
The interesting thing about that list is that none of it is about the model. It is all about the boundary. Which is usually true of AI infrastructure questions, and usually the last thing anyone checks.
DevLift exposes its platform to AI coding agents through an MCP server with OAuth 2.1, so the agent provisions as the developer driving it rather than as a shared account. It generates infrastructure as code and opens a pull request instead of applying, which means your existing review still runs. Book a walkthrough to see it against your own account, or read what has to be true before an AI agent touches production.