# Test an MCP server

> For the complete documentation index, see [llms.txt](/llms.txt)

A scenario can be the client of an MCP server. It starts the server over
stdio, calls its tools and waits for the notifications the server sends.
Every call, result and notification lands in the run's report next to the
browser steps. A failing step quotes the last MCP message the server
exchanged. The `mcp*` functions are in `@unotest/web` 0.51.0 and later.

```js
function test_echo_notifies() {
  step("echo answers and notifies", () => {
    const srv = mcpStart("node", ["server.js"], { env: { API_TOKEN: API_TOKEN } });
    const res = mcpCall(srv, "echo", { text: "hi" });
    assertTrue(res.content[0].text == "hi", "echo answered something else");
    mcpWaitNotification(srv, "notifications/message", { match: { "data.echoed": "hi" } });
  });
}
```

## Start

`mcpStart(command, args, {env, cwd, timeoutMs})` runs the command and
finishes the MCP handshake. It returns the handle the other functions
take.

- The server inherits the runner's environment, minus the runner's own
  credentials. `env` adds to it. The report names those variables, never
  their values.
- `cwd` is relative to the project.
- A server that does not come up errors the test, with the tail of its
  stderr.

## Call

`mcpCall(server, tool, args, {timeoutMs, allowError})` returns the tool's
result: `content`, and `structuredContent` when the tool sends it. The
step fails on a JSON-RPC error, on no answer within the timeout, or when
the server exits mid-call. It also fails on a result marked `isError`;
`{allowError: true}` hands that result back for the scenario to check.

## Wait for a notification

`mcpWaitNotification(server, method, {match, timeoutMs})` returns the
notification's `params`. Notifications are kept from the moment the
server started, and each is handed out once. So the natural order works:
call first, then wait for what the call set off.

`match` compares scalars at dot paths of the params, as `waitForJsonLine`
does: `{"data.status": "done"}`. On a timeout the failure lists the
notifications that did arrive.

## Stop

`mcpStop(server)` stops the server. It is optional: a server the test
leaves running is stopped when the test ends, passed or failed.

## What the report shows

- A note per call (`mcp → tools/call echo` with its arguments), per
  result (`mcp ← tools/call echo`) and per notification
  (`mcp ← notifications/message` with its params).
- On a red step, the error ends with the last MCP message, as sent or
  received:

```
mcpWaitNotification: no "notifications/message" matching {"data.echoed":"never"} notification from MCP server mcp-1 (node) within 300ms; no other notification is waiting to be taken
last MCP message (received, response): {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"hi"}]}}
```

The failure bundle carries that error, so the server's last word is
there without a re-run.

Defaults: 30 seconds for the start, for each call and for each wait.
