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.
envadds to it. The report names those variables, never their values. cwdis 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 echowith its arguments), per result (mcp ← tools/call echo) and per notification (mcp ← notifications/messagewith 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 takenlast 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.