Generate mocks with your own AI agent

« Back to documentation home

Overview

You can have an AI coding agent (Claude Code, Cursor, or any assistant that can read files and call an HTTP API) write Traffic Parrot mocks for you. You give the agent a description of your API and it writes a mapping: a JSON file that tells Traffic Parrot "when a request like this arrives, reply with this response". Traffic Parrot does not run the AI model itself: you give your own agent the right reference files, and it does the rest.

The quick start walks the whole flow end to end with Claude Code and a copy-paste starter prompt: the agent fetches the schema and the helper policy, writes a mapping, checks it, loads it, and proves the mock works. Before you run it, make sure Traffic Parrot runs on your own machine or a trusted network. The sections after the quick start explain each part in more depth.

Traffic Parrot also serves a Model Context Protocol endpoint, so an assistant can connect to a running instance and read what it holds instead of working only from files.

Run on a trusted network

By default, Traffic Parrot does not ask for a password: anyone who can reach the management port (8080 by default, the port you configure Traffic Parrot on) or the simulator's /__admin endpoint can load a mapping, and a loaded mapping starts answering requests immediately. Some mappings can also use helpers that run code on the machine.

This matters more when an AI agent writes the mocks. The mappings come from a language model rather than a person, and what the model writes can be influenced by the text in the files you give it. Your safety net is checking the work before it goes live: check it against the schema, read the mapping, follow the helper policy. Running on your own machine or a trusted network means that if something slips through, it only affects your machine, not a shared environment.

Quick start with Claude Code

This walkthrough uses Claude Code, a coding agent that runs in your terminal. You could use Cursor, Windsurf, GitHub Copilot, OpenAI Codex CLI, Google Gemini CLI, or Aider instead: any agent that can fetch a URL and call an HTTP API follows the same flow with the same starter prompt. You need:

  • Traffic Parrot running on your machine (management port 8080, stub port 8081 by default), on a trusted network. The stub port is where the mock answers; the management port is where you configure it.
  • Claude Code installed (or the agent of your choice).
  • An API contract: a file that describes what an API accepts and returns. You do not need your own to try this. The prompt below has the agent download the Train Travel API sample (a community OpenAPI example), so you can run everything as-is first and swap in your own contract after.

1. Start Claude Code in a new folder

mkdir train-mock
cd train-mock
claude

2. Paste the starter prompt

Copy this prompt as a starting point and adapt it as needed:

Download the Train Travel API sample contract from
https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/main/openapi.yaml
and read it.

Fetch the Traffic Parrot mapping schema from
http://localhost:8080/schemas/http-mapping.schema.json
and the helper policy from
http://localhost:8080/helper-policy.json

Generate a Traffic Parrot mapping for GET /stations in a file called
stations-mapping.json. It must conform to the mapping schema, must not use any
helper denied by the helper policy, and should return a realistic response
based on the contract.

Validate stations-mapping.json against the schema, show me the mapping and the
validation result, and wait for my approval.

Once I approve, load the mapping with a POST to
http://localhost:8080/api/http/__admin/mappings and then call
http://localhost:8081/stations to prove the mock works.

3. Review, approve, and check the result

Claude Code asks before it runs each command, so you stay in control. When it shows you stations-mapping.json and the validation result, read the mapping before approving: once loaded, it starts answering requests immediately.

After you approve, Traffic Parrot confirms the mapping was created (201 Created) and the final call to http://localhost:8081/stations returns the station list: your mock is live.

4. Make it yours

  • Using your own API? Replace the prompt's download line with the path to your contract file, and change the endpoint.
  • Mocking another protocol? Have the agent pick the schema from the schema index and look up the load endpoint in the Management API specification. JMS and file messages import a ZIP of mappings instead of a single POST.
  • Optionally, finish with trafficparrot validate and the helper-policy check as an extra backstop before committing generated mappings.

More realistic mocks

The starter prompt hands the agent the minimum it needs to write a correct mock. To make the mock realistic as well as correct, give the agent more. Once you are working with your own API, add lines like these to the prompt and point them at what you have:

Also read the production code that implements the API (in ./src) and the API
documentation (in ./docs).

Reuse what you find there in the mocked responses: the real field names, values
in the formats the systems actually exchange, and the error responses the API
really returns. The mock should be hard to tell apart from the live API.

Each source fills gaps the others leave. The OpenAPI specification defines the shape of requests and responses, the documentation explains how the API behaves, and the production code shows the values and edge cases that never made it into either. An agent that has read all three writes mocks with realistic data rather than invented placeholders. Remember that everything you add becomes input to the model, so the same rule applies as for the contract: check the generated mapping before loading it.

Point the agent at the mapping schema

The mapping schema is a file that spells out exactly what a valid mapping looks like: which fields exist, which are required, and what values they can take. With the schema in hand, the agent guesses less and can check its own work. Your instance lists its schemas in a machine-readable index, served on the management port (8080 by default):

http://localhost:8080/schemas/index.json

The index has one entry per mapping format (HTTP, which also covers gRPC; JMS; native IBM® MQ; file messages) with each schema's URL, version, and the directory the mapping files live in. Have the agent fetch the index, pick the schema for your protocol, and check every mapping against it before loading (the starter prompt pastes the HTTP schema URL directly). If your assistant cannot reach the instance, download the files yourself (for example with curl) and attach them to its context instead. The schemas ship inside Traffic Parrot, so they always match the version you are running.

Ground your agent

Grounding the agent means giving it the reference material it needs, so it works from facts instead of guessing. Put three things into the agent's context, then let it generate:

  1. The mapping schema for the protocol you are mocking.
  2. The helper policy, so the agent stays away from the two unsafe helpers.
  3. Your API contract: an OpenAPI/Swagger specification for HTTP, a proto file for gRPC, or an AsyncAPI specification for messaging.

Those three are the minimum, and the more you give the agent the better. Point it at your production code, your API documentation, and your OpenAPI specification together and it writes very realistic mocks: real field names, values that look like the ones your systems exchange, and responses that behave like the live API. The More realistic mocks chapter shows prompt lines that do this.

Then have it write a mapping that follows the schema, check the mapping before loading it, and never use a denied helper, as the starter prompt does.

The helper policy

Mappings can use Handlebars helpers: small built-in functions that make a response dynamic, for example inserting a random value or today's date. Two of them can run arbitrary code on the machine and must never appear in a generated mapping. The helper policy names them, served at:

http://localhost:8080/helper-policy.json
Denied helper Why
executeProcess Spawns an external operating-system process; a generated mapping using it could run arbitrary commands on the host.
evaluate Evaluates an inline script; a generated mapping using it could run arbitrary code.

The policy is a deny-list: anything it does not name is allowed. The helper-policy check enforces it for you when you validate.

Treat your API contract as untrusted input

Everything you hand the agent, including the contract, becomes input to a language model. Text hidden in a description or an example could nudge the agent toward a denied helper or a response that leaks data. Traffic Parrot does not check a mapping between loading it and it going live, so the checks are yours to run: check against the schema, read the mapping before loading it, and run the Traffic Parrot checks.

Validate with Traffic Parrot

The checks the agent runs are your first guard; trafficparrot validate is a second, independent one you run by hand or in CI. It checks a folder of mappings without starting the server, compares them with your contracts, and fails when a mapping mocks something the contract does not describe or part of the contract has no mapping at all:

trafficparrot validate /path/to/files-root

See the validate CLI reference and the messaging coverage check for details.

The helper-policy check

For generated mappings, also enable the helper-policy check: it fails if a mapping uses a denied helper, and it reads the mappings without running them, so it even catches a helper hidden inside another one. It is off by default (hand-written mappings legitimately use those helpers):

trafficparrot validate /path/to/files-root \
  -Dtrafficparrot.validate.helper.policy.check=true

Use the generated mocks

Point the application you are testing at the stub port instead of the real API: change its base URL to http://localhost:8081 (or wherever your simulator runs). Your application now gets the mocked responses, so you can test without the real system being available.

The generated mappings are plain JSON files, so you can also:

  • Keep them: store the files in version control with your tests, and copy them into your Traffic Parrot installation's mappings directory (the schema index shows the directory for each protocol) so they load every time Traffic Parrot starts.
  • Edit them with your agent, by hand, or in the web UI: ask the agent to change a mapping the same way it wrote it (check against the schema and reload after), or open http://localhost:8080 to browse and edit what is loaded; the User Guide shows how.
  • Make them smarter: match requests more precisely with request matching, and build dynamic responses (dates, random values, echoes of the request) with the allowed helpers.

Connect your assistant with MCP

Everything above has the agent work with mapping files. Traffic Parrot also speaks the Model Context Protocol (MCP), the open standard assistants use to discover and use what a tool offers. Point an MCP client at a running instance and your assistant can work with the instance itself: the mappings it is serving, the requests that have arrived, and the scenarios defined — read without you pasting any of it into the conversation, and changed without you copying JSON back out of it.

Traffic Parrot serves MCP itself, so there is nothing extra to install and no separate process to keep running: if the instance is up, the endpoint is up. It listens on the management port (8080 by default) and takes JSON-RPC 2.0 over HTTP POST. MCP clients are configured with a bare endpoint URL, so unlike the rest of the management API it does not sit under /api:

http://localhost:8080/mcp

The trusted-network guidance above applies here too, and matters more: some of these tools change what the instance is serving. Who can reach it, below, is the setting that decides that.

Who can reach it

By default the endpoint accepts requests from the machine Traffic Parrot is running on and refuses everything else, which is the common case: the assistant runs where you do. Nothing to configure, and no token to manage.

To reach it from anywhere else, set both of these in trafficparrot.properties (see the Configuration Reference) and restart:

trafficparrot.mcp.authenticator=BEARER_TOKEN
trafficparrot.mcp.token=a-secret-of-your-choosing

Every request must then carry Authorization: Bearer <token>. There is no default token, deliberately, and Traffic Parrot will not start with BEARER_TOKEN and no token set — a token that ships with the product is a published one.

The most common reason to need this is Traffic Parrot in a container with a published port: requests then arrive from the container network rather than from loopback, so the default refuses them even though you are sitting at the same machine.

A refused request comes back as HTTP 403 with a JSON-RPC error saying which property to change, so your assistant can tell you what to do rather than reporting an opaque failure. The reason never contains the configured token.

If you have also switched on the web UI login (trafficparrot.gui.security.mode=LOGIN_PROPERTIES), that covers /mcp as well and answers first. Your MCP client must then send HTTP BASIC credentials from trafficparrot.gui.login.properties on every request to /mcp, as well as anything the MCP authenticator requires; a request that omits them is refused whether or not its bearer token is right. Both refusals are JSON-RPC errors, so tell them apart by the status code rather than by the body: 401 is the web UI login, and 403 is the trafficparrot.mcp.authenticator setting above. The 401 also names the property that switched the login on and the file the credentials come from, so your assistant can tell you which of the two you are looking at.

The handshake

An MCP client opens the conversation with an initialize call. Your client does this for you when it connects, but making the call by hand is the quickest way to confirm the endpoint is reachable:

curl -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize"}'

Traffic Parrot identifies itself and answers with the protocol revision it has negotiated with your client. The call above named no revision, so it gets the newest one Traffic Parrot speaks:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": { "tools": { "listChanged": false } },
    "serverInfo": { "name": "traffic-parrot", "version": "5.62.0" }
  }
}

A client that does name a revision, in params.protocolVersion, gets that same revision back when Traffic Parrot speaks it:

curl -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize",
       "params":{"protocolVersion":"2025-06-18"}}'

That answers "protocolVersion": "2025-06-18". The revisions Traffic Parrot speaks are 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05 and 2024-10-07, newest first. A client that asks for anything else, or asks for nothing at all, is answered 2025-11-25 rather than having its handshake refused. The rest of the answer is the same whichever revision is negotiated: the revision describes the shape of the messages, while capabilities describes what this instance can actually do.

serverInfo.version is the version of the instance you reached, which is a quick way to confirm your client is talking to the instance you meant. listChanged: false says the tool list is fixed for the lifetime of the connection, so a client does not need to watch for it changing.

After the handshake

Once a revision has been negotiated, an MCP client states it on the requests that follow, in an MCP-Protocol-Version header. Your client does this for you, sending back the revision the handshake answered with. The header is read whichever way your client capitalises its name:

curl -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -H 'MCP-Protocol-Version: 2025-11-25' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Traffic Parrot checks that header. A request naming a revision it does not speak is refused with HTTP 400 and a JSON-RPC error, rather than being served as though the two ends had agreed on a revision they had not. The message names both the revision that arrived and every revision this instance speaks, so the refusal carries what your client needs to correct itself:

{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32000,
    "message": "Unsupported protocol version: 2026-01-01 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"
  }
}

The header is not required. A request that carries none is served exactly as it would be otherwise, so a client that does not set it, or a call you write by hand, keeps working. Sending it with an empty value is not the same as leaving it out: an empty value names a revision Traffic Parrot does not speak, and is refused like any other.

The initialize call is the exception, and is served even when the header names a revision Traffic Parrot does not speak. The handshake is where the revision is settled, and the negotiation above deliberately answers a client that asks for a revision Traffic Parrot does not have with 2025-11-25 rather than refusing it. Refusing that same client on its header would take that back, so the header is not checked on a handshake.

The tools it offers

A client asks for the catalogue with tools/list. Traffic Parrot offers twelve tools. Six of them only read, and are safe to point an assistant at while you are relying on the instance:

Tool What it reads Arguments
list_mappings The HTTP mappings (stubs) the instance is serving from its currently selected scenario, with their request matchers and a summary of each response. None.
get_mapping One mapping in full, by id, including its request matchers and its response. A body held inline comes back in full; a body stored in an external file comes back as bodyFileName only, which means “a body exists that you cannot see” rather than an empty response. id (required).
list_received_requests Requests the instance has received, from the request journal, each tagged with the scenario that served it, so the assistant can see the real traffic a mapping has to match. limit — most recent first. Requests served by scenarios on their own port are always included.
report_coverage Which endpoints of the selected scenario have mappings and which do not, so you can find gaps and drift against a specification. None.
list_scenarios The scenarios defined in the instance, and which one is currently selected. None.
verify_mapping_matches Whether the mappings loaded now still match the requests the instance has received, by re-testing each received request against them. Use it after changing mappings to find traffic that no longer matches. A mapping with a custom request matcher cannot be evaluated this way and is listed under notVerifiableMappings rather than reported as matching nothing. id — check one mapping; omit for all. limit — received requests to check, most recent first; omit for the whole journal.

The other six change something. Each one says where the change lands, because that is what you cannot tell from the tool name: the mapping tools change what the running instance is serving from its selected scenario and the change is written through to that scenario's mapping files, so a scenario directory kept in version control shows it as a local change, while cleanup_mappings writes a new scenario to disk.

Tool What it changes Arguments
create_mapping Adds one new mapping to the running instance. The id is assigned for you and returned. Supplying the id of a mapping that already exists is refused rather than replacing it — that is what update_mapping is for. A mapping's file is named after the mapping, so a name that would produce the same file name as another mapping's is refused rather than overwriting it — rename the mapping and call again. mapping (required) — a WireMock stub mapping JSON object with at least a request and a response, the same shape get_mapping returns. dryRun — returns the mapping that would be created and changes nothing.
update_mapping Replaces one existing mapping, keeping its id. id and mapping (both required; any id inside the mapping is ignored). dryRun — returns the mapping that would be replaced and the full replacement, and changes nothing.
delete_mapping Removes one mapping. id (required). dryRun — returns the mapping that would be removed and changes nothing.
select_scenario Switches which scenario the instance is serving. Every other tool works on the selected scenario, so this decides what they all see. name (required) — as returned by list_scenarios.
generate_mappings_from_openapi Builds mappings from an OpenAPI specification file, so a scenario can be brought back in line with a newer spec. Re-running it for the same file replaces the mappings that file produced last time rather than adding a second copy. specificationFile (required) — the name of a file already in the instance's OpenAPI directory. The specification itself cannot be supplied here, and a path is not accepted. dryRun — returns the mappings that would be added. A preview builds those mappings by reading the specification exactly as a real run does, so an example that points at a URL with externalValue is fetched over the network even when dryRun is true.
cleanup_mappings Turns the recorded mappings of the selected scenario into a flexible mock — hardcoded values become URL patterns, static responses become templates, duplicates are consolidated. The result is written to a new scenario and the recording it was made from is left untouched. The new scenario carries the HTTP mappings and __files only, so it is not a complete copy of the recording: any mapping directory left behind (jms-mappings and the other non-HTTP mapping directories) is named in droppedMappingDirectories, so the assistant can tell you the cleaned scenario is not a drop-in replacement. See What the cleaned scenario does not carry. targetScenario — the new scenario's name, which must be a name inside the instance's scenarios directory; omit for a suggested one. An existing name is refused rather than overwritten, and so is a name that would resolve outside that directory — one climbing out of it with .., or an absolute path. dryRun — returns what the cleanup would do and writes nothing.

Anything that can lose work takes dryRun, and it defaults to true. A call that omits it previews and changes nothing, and the preview tells the assistant to call again with "dryRun": false to apply it. So an assistant that reaches for one of these tools without being explicit shows you what it would do first. Every one of these tools that writes a mapping takes dryRun, create_mapping included. select_scenario is the only one with none: it writes no mapping, and a scenario switch is undone by switching back.

dryRun means nothing in Traffic Parrot changes. It does not mean nothing happens at all. A preview does strictly less than the real call — generate_mappings_from_openapi, for one, skips removing the mappings an earlier import of the same file created — but the work it does still reaches outside the host: generate_mappings_from_openapi reads the specification file and fetches any externalValue example URL in it during a preview, just as it would when applying. That fetch goes out from the Traffic Parrot host, is limited to http and https, follows no redirects, and is capped in size and time; it is not restricted to an allow-list of hosts, so treat a specification the same way whether you preview it or apply it. A fetch that cannot complete — an unreachable host, a non-200 response, or a timeout — does not fail the import: Traffic Parrot puts a short Traffic Parrot could not fetch externalValue: … placeholder in place of that example, and a response larger than the size cap is truncated rather than rejected. Either way the mapping is still created and looks right in the preview listing, but the placeholder or truncated content becomes the response body — or a response header value — that the stub then serves at replay. So if the Traffic Parrot host cannot reach the externalValue URLs, check what the preview actually built before you rely on it.

tools/list reports the arguments each tool accepts, and your client shows them to you. Read them there rather than from a list written down elsewhere: the catalogue ships inside Traffic Parrot, so it always describes the instance you are actually connected to. The table above is the same catalogue at the time of this release.

Running a tool

Your client runs a tool with tools/call, naming it and passing its arguments. You do not normally write these calls yourself — the assistant does — but making one by hand is the quickest way to see the shape of an answer:

curl -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
       "params":{"name":"delete_mapping","arguments":{"id":"8f2b…"}}}'

The result comes back as MCP content blocks. Because delete_mapping takes dryRun and the call above did not say otherwise, nothing was deleted — the answer describes what would happen and how to commit it:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [ { "type": "text", "text": "{\"dryRun\":true,\"wouldDelete\":{…},\"toApply\":\"Call delete_mapping again with \\\"dryRun\\\": false.\"}" } ],
    "isError": false
  }
}

Only the advertised tools can be called, so a call naming a tool this instance does not offer is refused before it reaches anything.

isError: true means the tool ran but could not answer — an unknown mapping id, a disabled request journal. That is different from a JSON-RPC error, which means the call itself was wrong (a missing required argument comes back as -32602, an unknown method as -32601). Both are different again from an empty result, which is a successful answer that says there is nothing there — so an assistant is never left reading “nothing matched” as “the tool failed”. If a tool fails unexpectedly the response says so without detail and the cause, with its stack trace, is in the Traffic Parrot log: what goes back to the assistant also goes to whichever model provider it uses.