Test MCP servers
An MCP server (Model Context Protocol) offers AI assistants tools, like track_parcel: the assistant reads each tool’s name, description and argument schema, calls tools as it works, and gets their results back. The mcp pack tests your server by doing what an assistant does: it calls the tools and checks what they answer. No model is involved, so the scenarios are deterministic.
Scenario: An assistant tracks a parcel Given a seeds/mcp-track.yaml db seed And the parcels mcp server with the following properties: | url | http://localhost:8400/mcp | When the track_parcel tool is called on the parcels mcp server with the following arguments: | reference | PX-MCP-9101 | Then the track_parcel tool's result is not an error And the track_parcel tool's result has the following properties: | status | OUT_FOR_DELIVERY | | serviceLevel | EXPRESS |Add the pack to the project with axx pack add mcp (Choose packs).
Register the server
Section titled “Register the server”A server is reached over streamable HTTP (a url), or run over stdio (a command), as assistants run local servers.
Which one follows how the server’s clients use it:
- A server its clients start, over stdio. Claude Code, Cursor and the other assistants run a local server as their own child process and talk to it through its stdin and stdout; nothing else can reach it. In a scenario, the scenario is the client, so the step runs it with
command, the way the cli pack runs a command-line tool. It is not a service: nothing goes underservices:inaxx.yaml. Each scenario runs its own server, so a server that keeps data on disk can keep each scenario’s apart (point its data folder, throughenv.<NAME>, at a folder of the scenario’s own). - A server that runs on its own, over HTTP, deployed where clients connect to it. axx starts it as a service in
axx.yaml(Manage the services under test), and the step connects withurl.
Test a server the way its clients reach it: a stdio server switched to HTTP only for its tests is tested over a transport its users never use.
Given the desk mcp server with the following properties: | command | docker compose exec -T app parcels mcp | | dir | ../infra || Property | What it is |
|---|---|
url |
The server’s streamable HTTP endpoint. |
command |
The command that runs the server over stdio, split into words without a shell. The scenario runs it, and ends it, and whatever it started, when it ends. |
dir |
The folder the command runs in, relative to the project. Default: a folder of the scenario’s own. |
env.<NAME> |
An environment variable of the command. |
header.<name> |
A header sent with every request to the url, such as header.Authorization. |
timeout |
How long connecting, or a call, may take. Default 30s. |
protocol version |
The MCP version to speak, like 2025-06-18, to check that the server still answers an earlier one. Default: the latest the server speaks. |
The session agrees on the protocol version with the server: MCP 2026-07-28, which has no sessions, or an earlier one. ${env:..} values are masked in logs and failures, and ${token:..} names a token the scenario registered (Test webhooks and tokens).
What an assistant sees
Section titled “What an assistant sees”Before an assistant calls a tool, it reads the server’s list of tools: their titles, descriptions and hints. Check them, and check that a tool the server must not offer is not there:
Then the parcels mcp server has the track_parcel tool with the following properties: | title | Track a parcel | | annotations.readOnlyHint | true |And the parcels mcp server does not have the cancel_parcel toolCall tools
Section titled “Call tools”When the hold_parcel tool is called on the parcels mcp server with the following arguments: | reference | PX-MCP-9102 | | until | 2026-10-05 |When the hold_parcel tool is called on the parcels mcp server with the mcp/hold-parcel.json arguments- An argument takes the type the tool’s input schema gives it:
01067is text for a string, and a number for an integer. Double quotes make a value text,nullis null, andundefinedleaves it out. A row can be a path into an argument (address.postcode,lines[0].reference). - A file’s arguments are a JSON or YAML object.
- A result that is an error does not fail the call, nor does a call the server refuses: check them.
Check results
Section titled “Check results”Then the hold_parcel tool's result has the following properties: | status | ON_HOLD | | heldUntil | 2026-10-05 |Then the hold_parcel tool's result is an errorAnd the hold_parcel tool's result contains 'it can no longer be held'Then the cancel_parcel tool's call failed with the error code -32602A tool fails in two ways, which the checks tell apart:
- The tool ran, and says what went wrong: its result is an error (
is an error), with text the assistant reads, to tell the user or try again. A parcel that can no longer be held is one. - The server refused the call: a JSON-RPC error, with its code (
failed with the error code):-32602for a tool it does not have or invalid parameters.
has the following properties reads the tool’s structured result, or the JSON of its text for a tool that answers JSON as text. contains reads its text and its structured result. The checks read the tool’s latest result in the scenario; when two servers of the scenario have the same tool, name the server: the track_parcel tool's result on the desk mcp server ....
The schemas are the contract
Section titled “The schemas are the contract”A tool’s arguments are checked against its input schema before they are sent, and its structured result against its output schema. A finding fails the step:
| Key | Found when |
|---|---|
validation.arguments.schema.<keyword> |
the arguments break the tool’s input schema (required, type, enum…) |
validation.result.schema.<keyword> |
the structured result breaks the tool’s output schema |
validation.result.missing |
a tool with an output schema answered no structured result |
A scenario that sends broken arguments on purpose, to see how the server refuses them, relaxes only what it breaks:
Scenario: A hold without its day is refused by the server too Given the MCP validation levels are: | validation.arguments.schema.required | IGNORE | When the hold_parcel tool is called on the parcels mcp server with the following arguments: | reference | PX-MCP-9102 | Then the hold_parcel tool's result is an errorERROR (or FAIL) fails the step, WARN and INFO log the finding, and IGNORE drops it. A key also sets the keys below it, and packs.mcp.levels in axx.yaml sets the defaults the scenarios’ levels are merged over.
Resources and prompts
Section titled “Resources and prompts”When the parcels://PX-MCP-9103/label resource is read from the parcels mcp serverThen the parcels://PX-MCP-9103/label resource has the following properties: | reference | PX-MCP-9103 |When the delivery_update prompt is requested from the parcels mcp server with the following arguments: | reference | PX-MCP-9103 |Then the delivery_update prompt contains 'parcel PX-MCP-9103'A resource is read by its URI; has the following properties reads its text as JSON. A prompt’s check reads the text of its messages.
Mock the MCP servers your service calls
Section titled “Mock the MCP servers your service calls”When your service calls another team’s MCP server, the Axx WireMock image mocks it from a description of its tools, with a mapping file per answer, and the mock pack checks what your service called. See Mock MCP servers and A2A agents.
See the mcp pack’s reference for every step.