# MCP servers

> Call the tools of MCP servers (Model Context Protocol) as an AI assistant calls them, and check their results, with the tools' own schemas as the contract. Read their resources and request their prompts.

Source: https://axx.nimbusxr.us/references/packs/mcp/

Call the tools of MCP servers (Model Context Protocol) as an AI assistant calls them, and check their results, with the tools' own schemas as the contract. Read their resources and request their prompts.

```gherkin
Given 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 |
```

- **Over stdio or streamable HTTP:** a `command` runs the server for the scenario, which ends it when it ends; a `url` reaches it. The session agrees on the protocol version with the server: 2026-07-28, or an earlier one the server speaks.
- **Run it as its clients do:** a server assistants start over stdio is the scenario's `command`, like a command-line tool, and not a service in axx.yaml; a server that runs on its own over HTTP is a service, and the `url` reaches it.
- **A tool's schemas are the contract:** its arguments take the types its input schema gives them (`10115` is text for a string), and are checked against it before the call; its structured result is checked against its output schema.
- **Two kinds of failure:** a tool's result that is an error (the tool ran, and says what went wrong), and a call the server refused (an unknown tool, invalid parameters), with its JSON-RPC error code.

Findings have keys and levels, like the REST pack's OpenAPI findings:

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

- **Levels:** `ERROR` (or `FAIL`) fails the step, `WARN` and `INFO` log the finding, `IGNORE` drops it. Every finding is an `ERROR` unless `packs.mcp.levels` in axx.yaml, or the scenario's `the MCP validation levels are:`, says otherwise.
- **Secrets stay secret:** `${env:..}` values in the properties are masked in logs and failures, and `${token:..}` names a token of the scenario.

Add it to a project with `axx pack add mcp`.

## Settings

In `axx.yaml`, under `packs.mcp`:

| Setting | Takes | Values |
|---|---|---|
| `levels` | Validation key -> ERROR (or FAIL), WARN, INFO or IGNORE, for every scenario. | names and values |

## Steps

### `mcp.server`

```gherkin
Given the {word} mcp server with the following properties:
  | property | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Register an MCP server under a name, and connect to it: over stdio (a `command` the scenario runs, and ends when it ends) or over streamable HTTP (a `url`). The session agrees on the protocol version with the server.

- A server its clients start over stdio is the scenario's `command`, as assistants run it, not a service in axx.yaml; a server that runs on its own over HTTP is a service, which the `url` reaches.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

| Property | Takes | Default |
|---|---|---|
| `command` | the command that runs the server over stdio, split into words without a shell, like `docker compose exec -T app parcels mcp` | |
| `url` | the server's streamable HTTP endpoint | |
| `dir` | the folder the command runs in, relative to the project | `a folder of the scenario's own` |
| `env.<NAME>` | an environment variable of the command, such as `env.PARCELS_DB_URL` | |
| `header.<name>` | a header sent with every request to the url, such as `header.Authorization` | |
| `timeout` | how long connecting, or a call, may take, like `10s` | `30s` |
| `protocol version` | the MCP version to speak, to check that the server still answers an earlier one, like `2025-06-18` | `the latest the server speaks` |

A `command` or a `url` is required. Values are expanded (`${env:..}`, `${sys:..}`, `${token:..}`); what `${env:..}` references expand to is masked everywhere.

**Example:**

```gherkin
Given the parcels mcp server with the following properties:
  | url | http://localhost:8400/mcp |
Given the parcels mcp server with the following properties:
  | command | ${sys:parcels.mcp} |
  | dir     | ../infra           |
```

_Since 0.1.5._

### `mcp.tool`

```gherkin
Then the {word} mcp server has the {word} tool[[ with the following properties:]]
```

Check that the server lists the tool, and that it has the table's properties: what an AI assistant reads before it calls the tool, like its description and its hints.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

A row is a path into the tool as the server lists it (`description`, `annotations.readOnlyHint`, `inputSchema.required[0]`) and its value.

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} mcp server has the {word} tool`
- `the {word} mcp server has the {word} tool with the following properties:`

**Example:**

```gherkin
Then the parcels mcp server has the track_parcel tool
Then the parcels mcp server has the track_parcel tool with the following properties:
  | title                    | Track a parcel |
  | annotations.readOnlyHint | true           |
```

_Since 0.1.5._

### `mcp.noTool`

```gherkin
Then the {word} mcp server does not have the {word} tool
```

Check that the server does not list the tool: one it must not offer an AI assistant.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Example:**

```gherkin
Then the parcels mcp server does not have the cancel_parcel tool
```

_Since 0.1.5._

### `mcp.call`

```gherkin
When the {word} tool is called on the {word} mcp server[[ with the following arguments:]]
```

Call the tool, with the table's arguments when the step has one. The arguments are checked against the tool's input schema before they are sent, and its structured result against its output schema.

- A result that is an error does not fail the step: check it with `the {word} tool's result is an error`.
- Nor does a call the server refuses: check it with `the {word} tool's call failed with the error code {int}`.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

Each row is an argument, or a path into one (`address.postcode`), and its value: a value takes the type the tool's input schema gives its argument, double quotes make it text, `null` is null and `undefined` leaves it out.

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool is called on the {word} mcp server`
- `the {word} tool is called on the {word} mcp server with the following arguments:`

**Example:**

```gherkin
When the track_parcel tool is called on the parcels mcp server with the following arguments:
  | reference | PX-MCP-9101 |
```

_Since 0.1.5._

### `mcp.call.file`

```gherkin
When the {word} tool is called on the {word} mcp server with the {filepath} arguments
```

Call the tool with the arguments of a file of the project: a JSON or YAML object of the arguments' names and values. They are checked against the tool's input schema before they are sent.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |
| `{filepath}` | a file of the project: a path relative to the feature file's directory, the `resources` directories or axx.yaml's directory (looked for in that order), or an absolute path, quoted when it has a space | `seeds/parcels.yaml`, `kafka/scan-delivered.json` |

**Example:**

```gherkin
When the hold_parcel tool is called on the parcels mcp server with the mcp/hold-parcel.json arguments
```

_Since 0.1.5._

### `mcp.notError`

```gherkin
Then the {word} tool's result[[ on the {word} mcp server]] is not an error
```

Check that the tool answered, with a result that is not an error.

The result is the tool's latest in the scenario, on the server the step names, or on the only server the scenario called the tool on.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool's result is not an error`
- `the {word} tool's result on the {word} mcp server is not an error`

**Example:**

```gherkin
Then the track_parcel tool's result is not an error
```

_Since 0.1.5._

### `mcp.error`

```gherkin
Then the {word} tool's result[[ on the {word} mcp server]] is an error
```

Check that the tool answered with a result that is an error: the tool ran, and says what went wrong, for the assistant to tell the user or try again. Check what it says with `contains`.

The result is the tool's latest in the scenario, on the server the step names, or on the only server the scenario called the tool on.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool's result is an error`
- `the {word} tool's result on the {word} mcp server is an error`

**Example:**

```gherkin
Then the hold_parcel tool's result is an error
```

_Since 0.1.5._

### `mcp.contains`

```gherkin
Then the {word} tool's result[[ on the {word} mcp server]] contains {string}
```

Check that the tool's result has a text: in its content (text, and embedded resources' text), or in its structured content's JSON.

The result is the tool's latest in the scenario, on the server the step names, or on the only server the scenario called the tool on.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |
| `{string}` | text in double or single quotes, which the step leaves out | `"Get a quote"`, `'Express'` |

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool's result contains {string}`
- `the {word} tool's result on the {word} mcp server contains {string}`

**Example:**

```gherkin
Then the hold_parcel tool's result contains 'already out for delivery'
```

_Since 0.1.5._

### `mcp.properties`

```gherkin
Then the {word} tool's result[[ on the {word} mcp server]] has the following properties:
  | property | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Check the tool's structured result: its structured content, or the JSON of its text for a tool that answers JSON as text.

- Paths are dotted (`lastScan.location`) or JSONPath; `null` and `undefined` work as in the other property steps.
- The result is the tool's latest in the scenario, on the server the step names, or on the only server the scenario called the tool on.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

A row is a path into the structured result, and its value.

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool's result has the following properties:`
- `the {word} tool's result on the {word} mcp server has the following properties:`

**Example:**

```gherkin
Then the track_parcel tool's result has the following properties:
  | status       | OUT_FOR_DELIVERY |
  | lastLocation | Leipzig          |
```

_Since 0.1.5._

### `mcp.refused`

```gherkin
Then the {word} tool's call[[ on the {word} mcp server]] failed with the error code {int}
```

Check that the server refused the call, with a JSON-RPC error of that code: `-32602` for a tool it does not have or invalid parameters, `-32603` for an internal error.

The result is the tool's latest in the scenario, on the server the step names, or on the only server the scenario called the tool on.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |
| `{int}` | a whole number | `200`, `3` |

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} tool's call failed with the error code {int}`
- `the {word} tool's call on the {word} mcp server failed with the error code {int}`

**Example:**

```gherkin
Then the cancel_parcel tool's call failed with the error code -32602
```

_Since 0.1.5._

### `mcp.resource.read`

```gherkin
When the {word} resource is read from the {word} mcp server
```

Read a resource of the server, by its URI.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

**Example:**

```gherkin
When the parcels://PX-MCP-9103/label resource is read from the parcels mcp server
```

_Since 0.1.5._

### `mcp.resource.contains`

```gherkin
Then the {word} resource contains {string}
```

Check that the text of the resource, as the scenario last read it, has a text.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |
| `{string}` | text in double or single quotes, which the step leaves out | `"Get a quote"`, `'Express'` |

**Example:**

```gherkin
Then the parcels://PX-MCP-9103/label resource contains 'PX-MCP-9103'
```

_Since 0.1.5._

### `mcp.resource.properties`

```gherkin
Then the {word} resource has the following properties:
  | property | value |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Check the resource, as the scenario last read it, when its text is JSON.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

A row is a path into the resource's JSON, and its value.

**Example:**

```gherkin
Then the parcels://PX-MCP-9103/label resource has the following properties:
  | reference | PX-MCP-9103 |
  | service   | EXPRESS     |
```

_Since 0.1.5._

### `mcp.prompt`

```gherkin
When the {word} prompt is requested from the {word} mcp server[[ with the following arguments:]]
```

Request a prompt of the server, with the table's arguments when the step has one: the messages it offers an AI assistant.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |

A row is an argument of the prompt, and its value, as text.

**Variants**, the parts in `[[...]]` said or left out:

- `the {word} prompt is requested from the {word} mcp server`
- `the {word} prompt is requested from the {word} mcp server with the following arguments:`

**Example:**

```gherkin
When the delivery_update prompt is requested from the parcels mcp server with the following arguments:
  | reference | PX-MCP-9104 |
```

_Since 0.1.5._

### `mcp.prompt.contains`

```gherkin
Then the {word} prompt contains {string}
```

Check that the text of the prompt's messages, as the scenario last requested it, has a text.

| Parameter | Takes | For example |
|---|---|---|
| `{word}` | one word, with no spaces | `parcels`, `PX-4101` |
| `{string}` | text in double or single quotes, which the step leaves out | `"Get a quote"`, `'Express'` |

**Example:**

```gherkin
Then the delivery_update prompt contains 'PX-MCP-9104'
```

_Since 0.1.5._

### `mcp.levels`

```gherkin
Given the MCP validation levels are:
  | validation key | level |
```

The table may start with the row naming its columns, as above, or go straight to its rows.

Relax, for this scenario, the checks of tools' arguments and results against their schemas.

A row is a validation key and its level: `ERROR` (or `FAIL`), `WARN`, `INFO` or `IGNORE`. The key must be one the pack reports, or a prefix of such keys, like `validation.arguments`: any other fails the step, which names the closest keys.

**Example:**

```gherkin
Given the MCP validation levels are:
  | validation.arguments.schema.required | IGNORE |
```

_Since 0.1.5._
