mcptest docs GitHub

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-test server: 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-level default_server:, and stepwise: 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

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:

RuleBehavior
Both server: and servers: setRejected.
default_server: names an unknown serverRejected with a "did you mean" hint.
A step's server: names an unknown serverRejected 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 schema does not change as these land; your YAML files keep validating. These are planned for a future release.

References