Self-Host Microcks: Mock the APIs Your AI Agent Calls
There is a failure mode that shows up over and over once you put an agent in a retry loop, and it goes like this.
The loop says "check the order status, and if it fails, retry with backoff." The upstream API returns a 502 once. The agent retries. The retry trips a rate limit. The agent reads that rate limit error as another transient failure, so it retries again. Nobody wrote a bug. Every individual decision was defensible. The result is still a burned quota and a log file you have to explain to somebody.
The fix is not a better prompt. The fix is that the agent should not be talking to a real API during a test run in the first place.
Everyone sandboxes the code, nobody sandboxes the calls
There is a lot of good writing right now about running agent-generated code in a sandbox. Almost none about the other half: the agent's outbound HTTP calls. Your agent calls Stripe, or Salesforce, or your own billing service, and every one of those calls is real. Real quota, real rate limits, real rows in someone's database.
Vendor sandboxes help, until you notice they are shared, flaky, rate limited, and impossible to make fail on demand. And "make it fail on demand" is the entire point. You need to know what your agent does when the payment API returns a 429, or a 500, or a valid response with a null field in it. Good luck asking Stripe's sandbox for that at a specific moment.
Microcks solves this. It is a CNCF incubating project, currently at v1.15.0, and the short version is: you hand it an API spec, it hands you back a running mock of that API. Not a stub you have to write. A mock derived from the examples already sitting in the spec.
It speaks OpenAPI, AsyncAPI, GraphQL, gRPC, SOAP, plus Postman collections and HAR files. If your agent talks to it over HTTP, Microcks can probably impersonate it.
Get one running
Microcks is genuinely light. The Elestio template asks for just 1 vCPU and 1 GB of RAM, so it fits on a small instance. Deploying it on Elestio starts at $11/mo with SSL, backups and updates handled. Open source means no license fees and no per-seat pricing, but the VM underneath is real infrastructure and it does cost something. Check current rates before you budget.
Once it is up, you have a UI and a REST API. The API is the part that matters, because the whole value here is that this runs in CI without a human clicking anything.
Import a spec, get a mock
Microcks builds mocks from the example objects in your OpenAPI file. That is the single most important thing to understand about it, and it is where everyone gets stuck. Upload a spec:
curl -X POST 'https://microcks.example.com/api/artifact/upload?mainArtifact=true' \
-H "Authorization: Bearer $TOKEN" \
-F 'file=@./specs/orders-api.yaml'
If your instance has authentication enabled, $TOKEN needs to come from a service account with the manager role. A default user account will get a 403 on this endpoint.
For anything ongoing, skip manual uploads and create an Importer job pointed at your Git repo instead. Microcks rescans on a schedule set by SERVICES_UPDATE_INTERVAL (a cron expression, every 2 hours by default), so when a backend team ships a new API version, your agent's mock updates itself. Import jobs also resolve external $ref dependencies, which direct uploads do not.
Your mock is now live at a predictable URL:
https://microcks.example.com/rest/Orders+API/1.0.0/orders/42
The pattern is /rest/{Service Name}/{Version}/{path}, with spaces in the service name encoded as +. Point your agent's base URL at that prefix and you are done. No code changes in the agent itself.
Now make it lie convincingly
A mock that returns one frozen JSON blob teaches your agent nothing. The useful part is controlling which response comes back.
Responses support mustache templating, so mocks can echo the request and generate fresh values:
{
"orderId": "{{ uuid() > put(oid) }}",
"customer": "{{ request.body/name || randomFullName() }}",
"reference": "{{ oid }}",
"createdAt": "{{ now(yyyy-MM-dd) }}"
}
The || operator falls back when the request has no name field. The > operator stores a value so later fields can reuse it, which is how you keep IDs consistent inside one response.
Routing is handled by dispatchers. These are the ones worth knowing:
| Dispatcher | Picks the response based on | Use it for |
|---|---|---|
| URI_PARTS | Path variables | Different responses per resource ID |
| QUERY_HEADER | A request header | Forcing a scenario from the agent side |
| JSON_BODY | Payload content | Amount over a threshold, bad country code |
| SCRIPT | Groovy or JavaScript you write | Anything the others cannot express |
| PROXY_FALLBACK | Real backend, when no mock matches | Gradual migration off a live dependency |
Header-driven routing is the one I reach for constantly with agents, because it lets a test harness demand a specific failure on command. For simple cases QUERY_HEADER is enough. When you want branching logic, use a SCRIPT dispatcher, which is what the snippet below is:
def headers = mockRequest.getRequestHeaders()
if (headers.hasValues("X-Test-Case")) {
switch(headers.get("X-Test-Case", "null")) {
case "rate-limited": return "429 response"
case "gateway-error": return "502 response"
case "null-amount": return "null amount"
}
}
return "standard order"
One catch worth knowing before you copy that: the strings you return are response names, so each one has to match a named example that exists in your spec. Return "429 response" without a response example called 429 response and Microcks has nothing to serve.
Now your eval suite can assert that a 429 makes the agent back off instead of hammering, and it runs in seconds against a mock rather than minutes against someone's sandbox. You can also set a per-operation response delay, which is how you find out whether your agent's timeout handling is real or aspirational.
The part that pays for itself later
Microcks does contract testing in the same instance. Point a test at your actual API endpoint and it replays the spec's examples against the real thing, checking that responses still conform.
That closes the loop. The mock your agent was developed against and the API it eventually hits are validated from one source of truth, so the day the backend team quietly renames a field, your tests say so instead of your agent hallucinating around the missing data.
Troubleshooting
The mock returns 404 or an empty body. Your spec has no complete examples. Microcks discards incomplete pairs, so an example on the request without a matching example on the response gives you nothing. Add named examples on both sides, using the same name to pair them.
404 on a URL you are sure is right. Check the service name and version encoding. Spaces become +, and the version must match the spec's info.version exactly. 1.0 and 1.0.0 are different services.
403 when uploading via the API. Regular accounts cannot upload. You need a service account with the manager role.
Changes to the spec are not showing up. Direct uploads are one-shot. If you expect continuous updates, you want an Importer job, and it runs on the SERVICES_UPDATE_INTERVAL schedule rather than instantly.
204 responses never match. There is no body to attach an example to, so use the x-microcks-refs extension to name the request that should trigger them.
Worth an afternoon
The setup cost is small. Import a spec, change one base URL in your agent config, and every destructive test your agent runs stops touching anything real.
The mental shift is the valuable part. Agent reliability gets treated as a prompting problem far more often than it deserves. A good chunk of it is plain infrastructure: give the thing a place to fail safely, then make it fail on purpose, repeatedly, until you know what it does.
You can deploy Microcks on Elestio and have a mock running before your coffee goes cold.
Thanks for reading ❤️ See you in the next one 👋