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.
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. |
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.
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.