Traffic Parrot 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.
This page is the reference for that endpoint: who can reach it, the handshake, the tools and their arguments, and what dryRun does. To point a client at it and get going, start with Connect your assistant with MCP on the AI-agent page, which has the client configuration; come here when you need the detail. The install directory also ships AGENTS.md, a brief for the assistant itself that covers this endpoint, the client registration, the tools.
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 for AI agents 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.
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 request the authenticator refuses 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. That makes three refusals in all, and no single field tells all three apart. Each is identifiable from its status and shape, so your assistant can tell you what you are looking at:
| Status | Body | Refused by | What to send or change |
|---|---|---|---|
| 401 | A JSON-RPC error whose message names the property that switched the login on and the file the credentials come from. | The web UI login, always. | HTTP BASIC credentials from trafficparrot.gui.login.properties, on every request. |
| 403 | A JSON-RPC error whose message says what to send. | The trafficparrot.mcp.authenticator setting above. | Authorization: Bearer <token>, matching trafficparrot.mcp.token. |
| 403 | A small plain JSON object with no error.code. | The web UI login again, refusing a signed-in user whose trafficparrot.gui.login.properties entry does not grant traffic-parrot-gui-role. | The login entry: grant it traffic-parrot-gui-role. |
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.
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.
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. |
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 tables above are the same catalogue at the time of this release.
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: <url> (<reason>) 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.
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.