Multi-server test suites
Status: the common case works today. A suite that declares several servers in the object-map
servers:form and routes each test with a per-testserver:field runs now: the runner connects every referenced server into a pool and dispatches each test to its own server. The array-of-entries form, file-leveldefault_server:, andstepwise:per-step server routing are still maturing and; the schema accepts them today so configs that opt in early stay valid.
Most mcptest test files target one MCP server. A growing set of real workflows depend on more than one: an agent that calls an issues server and a notifications server, or a CI suite that validates pairs of related servers interoperate. This page documents the multi-server surface.
At a glance
servers:accepts both the canonical object-map form and a new array-of-entries form. Pick whichever reads better.default_server:at the file level names the fallback for stepwise tests that omit a per-stepserver:.stepwise:tests carry asteps:array; each step can target a different server viaserver: <name>.- The single-server
server:shorthand at the file level is mutually exclusive withservers:. Setting both raises a loader error.
Object-map form (unchanged)
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
filesystem:
command: ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
issues:
url: "https://issues.example.com/v1"
auth:
bearer_token_env: "ISSUES_TOKEN"
tools:
- name: "lists files"
server: filesystem
tool: list_directory
args: { path: "/tmp" }
This is exactly today's shape. Nothing changes for existing files.
Array-of-entries form (new)
The array form embeds the server name inside each entry. It reads top-to-bottom as a sequence, which matches how contract tests think:
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
- name: issues
url: "https://issues.example.com/v1"
auth:
bearer_token_env: "ISSUES_TOKEN"
- name: notifications
url: "https://notifications.example.com/v1"
auth:
bearer_token_env: "NOTIFICATIONS_TOKEN"
Names must be unique. The loader rejects duplicates with a clear error.
A single server
For a suite with one server, use the servers: map with a single entry and reference it by name. Every tool test names its server, including this case.
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
issues:
url: "https://issues.example.com/v1"
tools:
- name: "lists issues"
server: issues
tool: list_issues
A server: (singular) shorthand that lets a one-server file drop the named map is committed in the schema, but its runner support is deferred to a future release. Until it lands, use the servers: map as above. server: and servers: are mutually exclusive; setting both raises a loader error.
Stepwise tests with per-step server
A stepwise: test walks a sequence of steps. Each step has an optional server: field; when omitted it falls back to the test's default_server: (or the file-level default_server:).
# yaml-language-server: $schema=https://mcptest.sh/schema/v1.json
servers:
- name: issues
url: "https://issues.example.com/v1"
- name: notifications
url: "https://notifications.example.com/v1"
default_server: issues
stepwise:
- name: "create issue triggers a notification"
steps:
- server: issues
call:
tool: create_issue
args:
title: "test"
capture:
issue_id: "result.content[0].id"
- server: notifications
call:
tool: list
args:
for_issue: "${issue_id}"
expect:
- target: "result.content"
matcher:
schema:
type: array
minItems: 1
The capture: block on a step grabs values out of the response for use in later steps via ${name} interpolation. The future runner walks the chain top-to-bottom, swapping connections as the server: field changes.
Validation rules
The loader catches these at load time, before the runner starts:
| Rule | Behavior |
|---|---|
Both server: and servers: set | Rejected. |
default_server: names an unknown server | Rejected with a "did you mean" hint. |
A step's server: names an unknown server | Rejected with a JSON Pointer at the offending step. |
Duplicate name in the array form of servers: | Rejected with a clear error. |
Today: what the runner does
The object-map form with several servers and per-test server: routing runs now. The runner connects every server referenced by a test (or an agent) into a pool and dispatches each test to the named server, so a suite that exercises two or three servers passes test-by-test against the right one.
Still maturing (tracked): the array-of-entries form, file-level default_server:, stepwise: per-step routing, and cross-server capture: interpolation. The schema accepts them today, so YAML you author against those sections stays valid as the runtime catches up.
Roadmap
tracks the remaining surface:
- The array-of-entries
servers:form and the singularserver:shorthand. stepwise:tests with per-stepserver:and cross-servercapture:interpolation.- Per-server reporter rollups (pretty, JSON, JUnit gain a server dimension).
The schema does not change as these land; your YAML files keep validating. These are planned for a future release.
References
- The schema versioning policy that allows additive optional fields without a major bump