Native test-framework SDKs
mcptest gives you two ways to write the same test, both driving the same engine over a co-process (mcptest exec --connection-server):
- A YAML suite. Write tools and expectations in
mcptest.yml; a thin SDK collects each entry as a native test case (onepytest/vitest/go test/cargo test/JUnit/xUnit item per entry). The suite is language-agnostic, so the same file runs unchanged under every SDK and themcptest runCLI. - In code. Write the whole test in your language: connect to a server you name in code, call tools, and assert with the SDK's helpers. No
mcptest.ymlis involved.
Both styles ship in every SDK and can live side by side in one project. Use YAML when you want one suite shared across languages and the CLI; use in code when you want loops, fixtures, and assertions in your host test framework.
The SDKs are published as mcptest (PyPI), @mcptest/sdk (npm), github.com/soapbucket/mcptest/sdks/go, mcptest-runner (crates.io), com.soapbucket.mcptest:mcptest-junit5 (Maven), and Mcptest.Sdk (NuGet). Each SDK shells out to the mcptest binary, so it must be on PATH (or pointed at with MCPTEST_BIN). Full runnable starters live under examples/ (python-sdk/, typescript-sdk/, go-sdk/, rust-sdk/, jvm-sdk/, dotnet-sdk/).
The YAML way
Write one suite. The server under test is declared once, in the servers: map:
# mcptest.yml
servers:
weather:
command: ["./my-server"]
tools:
- name: get_weather returns text for a known city
server: weather
tool: get_weather
args:
city: Sacramento
expect:
- target: result.content[0].text
matcher: { icontains: "sacramento" }
Each SDK collects that file into native test cases:
# Python: the pytest plugin auto-collects mcptest.yml in the project root.
# Run `pytest`; you get one item named mcptest::weather::<test name>.
// TypeScript: import the framework entry; it auto-collects mcptest.yml.
import "@mcptest/sdk/vitest";
// Go
func TestMCP(t *testing.T) {
mcptest.Register(t, mcptest.Config{Suite: "mcptest.yml"})
}
// Rust: one #[test] per YAML entry, generated at compile time.
mcptest_runner::mcptest_suite!("mcptest.yml");
// JVM (JUnit 5)
@TestFactory
Stream<DynamicTest> runSuite() {
return McptestSuite.discover(Path.of("mcptest.yml"), client);
}
// .NET (xUnit)
public sealed class WeatherSuite() : McptestSuiteData("mcptest.yml");
[Theory]
[ClassData(typeof(WeatherSuite))]
public void Run(McptestCase testCase) => _fixture.Run(testCase);
The in-code way
No mcptest.yml. You name the server in code, call tools, and assert with the same matcher set the YAML path uses (equals, contains, icontains, regex, subset). Swap the example's mcptest mock command for your own server, for example ["./my-server"].
# Python: define the server in an mcp_config fixture; the mcp fixture spawns it.
import pytest
from mcptest import assert_successful_mcp_call
@pytest.fixture
def mcp_config():
return {"command": ["./my-server"]}
def test_weather_in_code(mcp):
result = mcp.call("get_weather", city="Sacramento")
assert_successful_mcp_call(result)
assert "sacramento" in result.content[0].text.lower()
// TypeScript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { spawnCoProcess, MCPClient, mcpMatchers, type CoProcess } from "@mcptest/sdk";
expect.extend(mcpMatchers);
describe("weather", () => {
let cp: CoProcess;
let mcp: MCPClient;
beforeAll(async () => {
cp = spawnCoProcess({ serverCommand: ["./my-server"] });
mcp = new MCPClient({ coprocess: cp });
await mcp.initialize();
});
afterAll(async () => { await cp.close(); });
it("returns text for Sacramento", async () => {
const result = await mcp.call("get_weather", { city: "Sacramento" });
expect(result).toBeSuccessfulMcpCall();
expect(result.content[0].text.toLowerCase()).toContain("sacramento");
});
});
// Go
func TestWeatherInCode(t *testing.T) {
client, done, err := mcptest.Connect(mcptest.ConnectConfig{
ServerCommand: []string{"./my-server"},
})
if err != nil {
t.Fatal(err)
}
defer done()
res := client.Call("get_weather", map[string]any{"city": "Sacramento"})
if !res.Success {
t.Fatalf("call failed: %+v", res.Error)
}
if msg := mcptest.Expect(res, "result.content[0].text", map[string]any{"icontains": "sacramento"}); msg != "" {
t.Fatal(msg)
}
}
// Rust
use mcptest_runner::{expect, Session};
use serde_json::json;
#[test]
fn weather_in_code() {
let mut session = Session::connect(&["./my-server"]).expect("connect");
let res = session
.call("get_weather", json!({ "city": "Sacramento" }))
.expect("call");
assert!(res.success);
expect(&res, "result.content[0].text", json!({ "icontains": "sacramento" }))
.expect("assertion");
}
// JVM (JUnit 5)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class McpInCodeTest {
private Coprocess coprocess;
private McptestClient client;
@BeforeAll
void setUp() {
coprocess = Coprocess.spawnConnectionServer("mcptest", List.of("./my-server"));
client = new McptestClient(coprocess);
}
@AfterAll
void tearDown() { coprocess.close(); }
@Test
void weatherInCode() {
ToolResult result = client.call("get_weather", Map.of("city", "Sacramento"));
assertTrue(result.success());
assertNull(Matchers.expect(
result, "result.content[0].text", Map.of("icontains", "sacramento")));
}
}
// .NET (xUnit)
[Fact]
public void WeatherInCode()
{
using var coprocess = Coprocess.SpawnConnectionServer("mcptest", new[] { "./my-server" });
var client = new McptestClient(coprocess);
var result = client.Call(
"get_weather", new Dictionary<string, object?> { ["city"] = "Sacramento" });
Assert.True(result.Success);
Assert.Null(Matchers.Expect(
result, "result.content[0].text", new { icontains = "sacramento" }));
}
Which should I use?
- YAML when one suite should run across several languages and the
mcptest runCLI, when non-engineers help author tests, or when you want the full matcher and scoring surface (schema, llm-judge, compositions) without writing host-language glue. - In code when the test needs language-native control flow (loops over cases, computed arguments, shared fixtures) or when you would rather keep assertions in
pytest/vitest/go test/cargo test/JUnit/xUnit.
The two compose: a project can collect a shared mcptest.yml and also add in-code tests for the cases that need them.