This documentation is available as Markdown. For the complete index, see llms.txt. Skip to content

Test an MCP server

For the complete documentation index, see 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.

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.