# MCP tests

> How Fixter probes an MCP server the way an agent would, the three cumulative tiers, the fields each needs, and the assertions a failed run reports.

An MCP test connects to your MCP server on a schedule the way an agent would, and fails when
the server stops identifying itself, stops advertising a tool, or a tool call stops returning
a result you accept. An agent creates one through the `create_api_test` MCP tool with type
`MCP`; in the app, pick type `MCP server` in the test editor.

## What a test checks

Each attempt is one short MCP session, in the order an agent would run it: the server
identifies itself (the handshake), the test reads the tools the server advertises
(`tools/list`), and the test calls one tool (`tools/call`). `Check depth` sets how far into that
session a healthy server has to get. The tiers are cumulative; each checks everything the one
before it checks, and the test sends only what its tier needs.

| Tier | App label | What is sent | Passes when |
|---|---|---|---|
| `HANDSHAKE` | `Handshake` | The handshake | The server identifies itself. |
| `TOOLS_LIST` | `Tools list` | The handshake, then `tools/list` | The server lists every tool you expect. |
| `TOOL_CALL` | `Tool call` | The handshake, `tools/list`, then one `tools/call` | The tool you name returns a result you accept. |

How the handshake works depends on the protocol generation: the current, stateless revision
answers `server/discover`, older revisions answer `initialize`, the oldest over a GET event
stream. The test identifies itself as `fixter-monitoring`. See
[MCP protocol revisions](/api-testing/mcp-protocol-revisions/).

Every tier also checks `Max latency` when it is set, and a pinned protocol revision. To check
that a server keeps accepting several revisions, create one pinned `Handshake` test per
revision; see
[Watching several revisions](/api-testing/mcp-protocol-revisions/#watching-several-revisions).

## Protocol revision

:::note[Note]
Protocol revision handling is under active development. This section reflects current behaviour
and may be revised as that work progresses.
:::

`Protocol revision` defaults to `Newest supported`: the test runs against the newest revision
the server speaks and moves with it when the server changes, and a move is not a failure. Pick a
dated revision instead to pin the test; it then fails when the server stops accepting that
revision. You do not have to know what your server speaks: once the URL looks complete, the
editor detects it (after a short pause in typing, at once on paste or when you leave the field),
sets the picker to `Newest supported` and shows `currently <revision>` beside it. The revisions,
detection and pinning are on [MCP protocol revisions](/api-testing/mcp-protocol-revisions/).

## Fields per tier

| Field | MCP parameter | App label | Tier | What it takes |
|---|---|---|---|---|
| URL | `mcpUrl` | `URL` | All | An absolute `http://` or `https://` URL with a host, reachable from the public internet. Any other URL is never sent: the run fails with `malformed target URL`, so the test cannot be saved. Takes [request variables](/api-testing/request-variables/). |
| Credential | `mcpCredentialId` | `Credential` | All | `None`, or a saved credential. See [Credentials](/api-testing/credentials/). |
| Check depth | `mcpTier` | `Check depth` | All | `HANDSHAKE`, `TOOLS_LIST` or `TOOL_CALL`. Required. See [The three tiers](/api-testing/mcp-tests/#what-a-test-checks). |
| Protocol revision | `mcpProtocolRevision` | `Protocol revision` | All | A dated revision to pin, or `NEWEST_SUPPORTED`. Optional; omitted, it is `NEWEST_SUPPORTED`. See [Protocol revision](/api-testing/mcp-tests/#protocol-revision). |
| Expected tools | `expectedTools` | `Expected tools` | `TOOLS_LIST` | Tool names the server must advertise. Required for this tier. |
| Tool name | `toolName` | `Tool name` | `TOOL_CALL` | The tool to call. Required. |
| Tool arguments | `toolArgumentsJson` | `Tool arguments (JSON)` | `TOOL_CALL` | The arguments, as a JSON object. Required: a tool that takes none needs `{}`. Takes request variables. |
| Result validation | `mcpResultValidationTier`, `mcpResultValidationSample` | `Result validation`, `Result sample` | `TOOL_CALL` | The same five tiers as an HTTP body. Optional; `None` checks nothing. |
| Max latency | `mcpMaxLatencyMillis` | `Max latency` (ms) | All | Optional; left empty, speed is not checked. |

An MCP test has no method and no request headers. Request variables such as `{{uuid}}` apply
to the URL and the tool arguments only, not to the tool name or the expected tools. Values are
minted once per attempt, so a retry gets fresh ones.

The result validation reads the tool result's `structuredContent` as JSON when the result has
it, and otherwise the text of its `text` content items, joined. The tiers then apply exactly as
they do to an HTTP response body: see [Body validation](/api-testing/http-tests/#body-validation).
A result marked `isError: true` fails the test whatever the result validation says.

`Max latency` is compared with the attempt's total time across the session's requests.

## Redirects and headers

An MCP test follows 301, 302, 303, 307 and 308 redirects, up to five, and never from https to
http. Past five, or on a downgrade, it stops following and takes the redirect response itself as
the server's answer. Every hop must be a public address.

Every request of a run carries:

- `User-Agent: FixterMonitoring/1.0 (+https://fixter.dev)`.
- A `traceparent` header with the run's trace id, the same on every attempt of the run. See
  [Runs, states and uptime](/api-testing/runs-and-uptime/).
- The credential's headers, and its query parameters merged into the URL, when the test has a
  credential. See [Credentials](/api-testing/credentials/).

## What a failed run says

A run checks every assertion its tier includes and lists every failure; none stops the others.

| Assertion | Fails when | The run says |
|---|---|---|
| `mcp.initialize` | The server did not identify itself (every tier). | `MCP server did not identify itself` |
| `mcp.tools.contains` | An expected tool is not advertised, one failure per tool (`TOOLS_LIST`). | `Tool <name> is missing` |
| `mcp.tool.isError` | The tool call returned an error result (`TOOL_CALL`). | `Tool <toolName> returned an error` |
| `mcp.protocol.revision` | The test is pinned and the server stops accepting that revision. | `Server no longer speaks protocol <pin>` |
| `maxLatencyMillis` | The attempt took longer than `Max latency` (every tier). | `Took <actual> ms, limit <limit> ms` |

A failed result validation reads as it does for an HTTP body, for example
`Response body is missing expected fields`; the sentences say "Response body" for a tool result
too.

When nothing at the URL identified itself as an MCP server on any revision, the run fails
`mcp.initialize` with the message `the target did not identify itself as an MCP server`.

Once the server has identified itself, a JSON-RPC error in reply to `tools/list` leaves no tool
names, so every expected tool reads as missing. In a `Tool call` test it also reads as
`Tool <toolName> returned an error`, even though the tool was never called. A JSON-RPC error in
reply to `tools/call` reads the same. A plain error status with no JSON-RPC reply fails the run
with `MCP server rejected <method>`, for example `MCP server rejected tools/list`.

## Example

Two tests on an orders MCP server, which assumes a `Bearer token` credential named
`Orders MCP token` already exists. The first checks that two tools are advertised; the second
calls one of them. In the app, `Expected tools` is shown only for `Tools list`, so the two
checks are two tests.

| Field | First test | Second test |
|---|---|---|
| `Name` | `Orders MCP tools` | `Orders MCP search` |
| `Type` | `MCP server` | `MCP server` |
| `URL` | `https://mcp.example.com/mcp` | `https://mcp.example.com/mcp` |
| `Credential` | `Orders MCP token` | `Orders MCP token` |
| `Protocol revision` | `Newest supported` | `Newest supported` |
| `Check depth` | `Tools list` | `Tool call` |
| `Expected tools` | `search_orders`, `get_order` | |
| `Tool name` | | `search_orders` |
| `Max latency` | `2000` | `2000` |
| `Result validation` | | `Contains fields` |

The second test's `Tool arguments (JSON)`:

```json
{"query": "{{uuid}}"}
```

Its `Result sample`:

```json
{"results": []}
```

Each attempt searches for a fresh `{{uuid}}`, so the call never depends on data left by an
earlier run. The sample asks for a `results` array; an empty sample array accepts any array, so
`{"results": [], "total": 0}` passes.

If the server drops `get_order`, the first test fails with `Tool get_order is missing`. If the
search tool starts returning an error result, the second fails with
`Tool search_orders returned an error`. A session that took 2400 ms fails either test with
`Took 2400 ms, limit 2000 ms`.

An agent can fold both checks into one test: `create_api_test` with tier `TOOL_CALL` also
accepts `expectedTools`, and the run then checks the names as well as the call.

`Create API test` runs the test once before saving it, and saves it only if that run passes.