Skip to content

How to test MCP servers

Attach servers with CopilotEval(mcp_servers={...}). Ask for a task that needs their tools, then assert on completed operation data or application state. Use the server's ordinary unit tests for implementation behavior that does not need Copilot.

Local stdio server

This configuration starts the bundled banking fixture server in the current Python environment:

from __future__ import annotations

import json
import sys

from pytest_skill_engineering.copilot import CopilotEval

BANKING_MCP = {
    "banking": {
        "command": sys.executable,
        "args": ["-m", "pytest_skill_engineering.testing.banking_mcp"],
        "tools": ["*"],
    }
}


async def test_balance(copilot_eval):
    agent = CopilotEval(
        name="banking-balance",
        model="gpt-5.6-sol",
        instructions="Use banking tools for account requests.",
        mcp_servers=BANKING_MCP,
    )
    result = await copilot_eval(agent, "Use get_balance to retrieve the checking balance.")

    assert result.success, result.error
    calls = result.tool_calls_for("banking-get_balance")
    assert calls
    call = calls[-1]
    assert call.arguments == {"account": "checking"}
    assert call.success is True and call.evidence_complete
    assert call.result is not None
    display, structured = call.result.rsplit("\n\n", 1)
    assert json.loads(structured) == {"result": display}
    assert json.loads(display)["balance"] == 1500

The string-result decoding above is specific to this server's display and structured MCP output. Use your server's actual output contract rather than assuming every MCP response has this shape. A completed protocol call can still contain an application error.

For mutations, initialize disposable state and verify it through an independent application fixture. Reading a known balance here verifies the returned data; it does not assess the assistant's prose.

SSE server

This configuration requires an already-running test service:

REMOTE_MCP = {
    "crm": {
        "url": "http://localhost:8000/sse",
        "type": "sse",
        "tools": ["*"],
    }
}

Streamable HTTP server

import os

REMOTE_MCP = {
    "crm": {
        "url": "http://localhost:8000/mcp",
        "type": "http",
        "headers": {"Authorization": f"Bearer {os.environ['CRM_TEST_TOKEN']}"},
        "tools": ["*"],
    }
}

Use an authorized test credential supplied by your environment, not a token embedded in source. Your fixtures own remote-service startup, readiness, state reset, and cleanup. Cancelling a local request does not prove a remote action stopped.

Multiple servers

Add another named entry to the mapping:

ASSISTANT_MCP = {
    **BANKING_MCP,
    "todo": {
        "command": sys.executable,
        "args": ["-m", "pytest_skill_engineering.testing.todo_mcp"],
        "tools": ["*"],
    },
}

Use actual captured tool names, such as banking-get_balance, in result helpers. Server prefixes distinguish tools with similar names; there is no CopilotResult.tool_was_called_from_server() helper.

Restrict what the eval can use

The server's tools list selects the tools exposed to the session. For example, replace ["*"] with ["get_balance", "get_all_balances"] for a read-only banking test. ["*"] exposes all tools from that server.

allowed_tools and excluded_tools configure the SDK's overall available and excluded tool lists. Use the runtime tool identifiers for your environment; do not assume a server-local name is the same as a captured runtime name. These filters do not replace guarded adapters, server authorization, or application-level permissions.

When to use lower-level helpers

Use MCPServer, Wait, and MCPServerProcess for direct process control, readiness checks, or prompt-template discovery. Their configuration shape differs from the SDK's mcp_servers dictionary. A Copilot-facing test should still show the final CopilotEval configuration and its output assertions.

See Compare server variants when investigating a description, schema, or backend change.