
The AWS docs MCP server behind MiniStack
Build your own AWS emulator (or use MiniStack, we already built it for you)
When you're building an emulator, the hardest part isn't writing code. It's knowing what the correct behavior actually is.
AWS has hundreds of services, thousands of operations, and a lot of subtle behavior that isn't documented anywhere obvious. You find it by running real SDKs against your implementation and watching them fail. A timestamp returned as a float instead of an integer. A 404 where the spec requires a 400. A pagination token that's supposed to be opaque but you made it a raw integer offset. None of that is in the docs. You discover it the hard way.
A better way was needed. So after a few painful bugs in the early implementation of MiniStack, a local MCP server was built to give Kiro direct access to both the AWS API contracts and the full AWS documentation, queryable in context while writing code. That tooling was internal for a few months but I'm open-sourcing it now.
How it started
The original version ran on Voyage AI for the embedding layer, which is what Anthropic recommends for embeddings, and the retrieval quality was good enough that the MCP became the foundation for how MiniStack got built. Every new service started the same way: query the MCP for the wire contract, query it for the behavioral docs, write the tests, implement against the tests.
The open-source version swaps Voyage for a local open-source embedding model (
all-MiniLM-L6-v2 via sentence-transformers). No API key, no data leaving your machine, runs on any laptop. The tradeoff is retrieval quality, but for AWS documentation it's enough. If you want the original quality, swapping back to Voyage (or any other engine) is a few lines in server.py and ingest/ingest_docs.py.Two sources, one server
It started as two local indexes over AWS, so answers came back in milliseconds instead of from reading the docs page by page. One is a structured index over botocore. The other is a RAG over the scraped API reference. The MCP server sits in front of both.
The first is botocore. AWS publishes service model files, the same JSON files botocore uses internally to generate its client code. Every operation, every input shape, every output shape, every error code, for every service. The ingest step parses those into structured JSON files, one per service, and exposes them through the MCP server. This gives you the wire contract: exact field names, types, required flags, HTTP methods, error shapes with their HTTP status codes. No retrieval, no similarity search, just a flat index you can look up directly.
The second is the AWS API reference documentation. This is the RAG side. The ingest step scrapes the API reference page for every operation across every service MiniStack covers, strips the boilerplate, embeds each page with
all-MiniLM-L6-v2, and upserts into a ChromaDB vector store. search_docs embeds your query with the same model and returns the closest pages. This gives you the behavioral layer: what AWS actually does with those fields, edge cases, pagination rules, error conditions, the notes that don't fit in a schema. There's no LLM running on top of this. The synthesis happens in whatever MCP client you're using (Claude, Kiro, anything else).A daily sync job keeps both sources current. Botocore updates when AWS ships new operations. The docs scraper re-fetches any page older than seven days and re-embeds whatever changed.
What it exposes
The MCP server has 8 tools:
get_operation_docs is the main one. You give it a service and an operation name, and it returns the full wire contract from botocore plus the scraped documentation page in a single response. When you're implementing SQS.ReceiveMessage, you call this once and have everything: the input fields and their types, the output shape, the error codes with HTTP status codes, and the full AWS reference text explaining long polling, visibility timeout behavior, and what happens when you don't delete a message.search_docs does semantic search across all scraped pages. You ask it a behavioral question in plain language and it returns the most relevant pages. "What does DynamoDB return when a conditional check fails with ReturnValuesOnConditionCheckFailure set?" gets you the right page. "Query protocol services HTTP status codes for not-found errors" gets you the answer that took a painful audit of around 70 call sites to learn the hard way.The rest cover specific lookups:
describe_operation for just the botocore contract, get_error_codes for all error shapes in a service, get_shape for resolving a named type, get_service_protocol for wire protocol details, list_operations and list_services for exploration.What a call looks like
Here's a real
get_error_codes("sqs") call, output trimmed for length:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"service": "sqs",
"protocol": "json",
"error_count": 28,
"errors": [
{
"name": "BatchEntryIdsNotDistinct",
"code": "BatchEntryIdsNotDistinct",
"http_status": 400,
"sender_fault": true,
"docs": "Two or more batch entries in the request have the same Id."
},
{
"name": "InvalidAttributeName",
"code": "InvalidAttributeName",
"http_status": 400,
"sender_fault": true,
"docs": "The specified attribute doesn't exist."
},
...
]
}That
http_status: 400 on every error is exactly the field that drove the audit mentioned above. Once you see it laid out for the whole service, the "always 400, never 404" rule for query-protocol services stops being a thing you remember and becomes a thing you can check.And here's the wire side.
describe_operation("sqs", "ReceiveMessage"):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"service": "sqs",
"operation": "ReceiveMessage",
"http_method": "POST",
"http_uri": "/",
"input_shape": "ReceiveMessageRequest",
"input_members": {
"QueueUrl": {"shape": "String", "required": true},
"MaxNumberOfMessages": {"shape": "NullableInteger", "required": false},
"VisibilityTimeout": {"shape": "NullableInteger", "required": false},
"WaitTimeSeconds": {"shape": "NullableInteger", "required": false},
"ReceiveRequestAttemptId": {"shape": "String", "required": false}
},
"docs": "Retrieves one or more messages (up to 10), from the specified queue. Using the WaitTimeSeconds parameter enables long-poll support. ..."
}Both are deterministic lookups against the local botocore index. No network call, no LLM in the loop.
How it fits into the MiniStack workflow
Once the indexes were in place, the implementation loop turned into a kind of TDD. The seed test was already there, sourced from public and private SDK implementations, real AWS SDK test suites, and community contributions, validating against other than internal assumptions. From there, every new service or gap follows the same four steps:
get_operation_docs("service", "OperationName")to pull the wire contract and the docs page in one call.- Kiro writes the tests from that contract, plus anything the seed already covers.
- Claude implements against the tests.
search_docsfor any behavioral question that comes up during implementation or review.
The
get_error_codes tool has been particularly useful for cross-cutting SDK compatibility work. Query-protocol services (SQS, SNS, IAM, RDS, ElastiCache) must return HTTP 400 for every client error, not 404. The Java SDK v2 and Go SDK v2 do strict error mapping based on HTTP status. That took an audit of around 70 call sites across five services to fix. With get_error_codes, you can see the correct HTTP status for every error shape in a service before you write a single line of code.The full test suite lives at github.com/ministackorg/ministack . The MCP and the tests are two sides of the same thing. One tells you what AWS should do. The other verifies that MiniStack does it.
As a separate note, there are some tiny differences between real AWS and docs, so sometimes validation against real AWS is needed
Why local
It needed to be fast. When you're working across services, you ask a lot of questions per hour. Every round-trip to AWS website adds latency. A botocore lookup hitting an in-memory index returns in milliseconds, and
search_docs runs the embedding model on whatever CPU you already have. Setup
Python project, runs locally, no external API keys needed.
Wire it into Kiro by adding to
~/.kiro/settings/mcp.json:1
2
3
4
5
6
7
8
{
"mcpServers": {
"aws-docs": {
"command": "python",
"args": ["/path/to/aws-mcp/server.py"]
}
}
}Same shape for Claude Desktop.
For the daily sync on macOS:
1
2
cp launchd/com.aws-mcp.sync.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.aws-mcp.sync.plistThe index
After the initial scrape: 4000+ documents across 50 services, covering everything from S3 and DynamoDB to Step Functions, Cognito, ECS, and API Gateway v1 and v2. The botocore side covers more services with full operation and shape data.
The botocore models tell you the contract. The docs tell you the behavior. Together they're the source of truth MiniStack was built against from almost the start, now queryable in context without leaving the editor.
The code is at github.com/Nahuel990/aws-mcp . MIT licensed, same as MiniStack.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article