Testing with McpAssured

This guide covers testing MCP servers using the McpAssured testing utility, which provides a fluent API for writing comprehensive integration tests.

Overview

McpAssured is a testing library designed specifically for MCP servers. It provides:

  • Fluent API: Readable, chainable test methods

  • Transport Support: Test Streamable HTTP, WebSocket, STDIO, and (legacy) SSE transports

  • Type-Safe Assertions: Strongly-typed response validation

  • Batch Testing: Send multiple requests and validate results together

  • Request/Response Inspection: Access raw JSON-RPC messages for advanced scenarios

Prefer the McpStreamableTestClient (Streamable HTTP transport) for new tests. The McpSseTestClient (HTTP/SSE transport) is legacy and kept only for backwards compatibility.

If you target the latest version of the MCP specification (2026-07-28, the stateless protocol), use a streamable client switched to stateless mode:

McpStreamableTestClient client = McpAssured.newStreamableClient()
    .setStateless() (1)
    .build()
    .connect();
1 Uses the first stateless protocol version (2026-07-28); in this mode connect() performs a server/discover instead of the initialize/notifications/initialized handshake.

Adding the Dependency

Add the test dependency to your project:

<dependency>
    <groupId>io.quarkiverse.mcp</groupId>
    <artifactId>quarkus-mcp-server-test</artifactId>
    <scope>test</scope>
</dependency>

Creating Test Clients

McpAssured provides four transport-specific client types.

Streamable HTTP Transport Client

This is the recommended client for new tests.

import io.quarkiverse.mcp.server.test.McpAssured;
import io.quarkiverse.mcp.server.test.McpAssured.McpStreamableTestClient;

@Test
public void testStreamableTransport() {
    McpStreamableTestClient client =
        McpAssured.newConnectedStreamableClient(); (1)

    // Test operations...

    client.disconnect(); (2)
}
1 Creates and connects a new Streamable HTTP client
2 Clean up the connection after testing

To test the stateless protocol (2026-07-28), switch the client to stateless mode:

McpStreamableTestClient client = McpAssured.newStreamableClient()
    .setStateless() (1)
    .build()
    .connect(); (2)
1 Uses the first stateless protocol version (2026-07-28)
2 In stateless mode connect() performs a server/discover operation instead of the initialize/notifications/initialized handshake

SSE Transport Client

The HTTP/SSE transport is legacy. Prefer the Streamable HTTP client for new tests.

import io.quarkiverse.mcp.server.test.McpAssured.McpSseTestClient;

@Test
public void testSseTransport() {
    McpSseTestClient client = McpAssured.newConnectedSseClient(); (1)

    // Test operations...

    client.disconnect();
}
1 Creates and connects a new SSE client with default configuration

WebSocket Transport Client

import io.quarkiverse.mcp.server.test.McpAssured.McpWebSocketTestClient;

@Test
public void testWebSocketTransport() {
    McpWebSocketTestClient client =
        McpAssured.newConnectedWebSocketClient(); (1)

    // Test operations...

    client.disconnect();
}
1 Creates and connects a new WebSocket client

STDIO Transport Client

Unlike the HTTP and WebSocket clients which connect to an already-running Quarkus application, the STDIO client launches the MCP server as a subprocess and communicates via stdin/stdout using newline-delimited JSON messages. Therefore, McpStdioTestClient is designed for integration tests executed by maven-failsafe-plugin, i.e. after the application has been built.

import io.quarkiverse.mcp.server.test.McpAssured;
import io.quarkiverse.mcp.server.test.McpAssured.McpStdioTestClient;

@Test
public void testStdioTransport() {
    try (McpStdioTestClient client = McpAssured.newConnectedStdioClient()) { (1)
        client.when()
            .toolsList(page -> assertEquals(1, page.size()))
            .thenAssertResults();
    } (2)
}
1 By default, launches java -jar target/quarkus-app/quarkus-run.jar in the current working directory
2 close() terminates the subprocess

The client builder allows customization of the server command, working directory, environment variables, and stderr handling:

McpStdioTestClient client = McpAssured.newStdioClient()
    .setCommand("java", "-jar", "/path/to/app.jar") (1)
    .setWorkingDirectory(Path.of("/path/to/workdir")) (2)
    .setEnvironment(Map.of("MY_VAR", "value")) (3)
    .setStderrHandler(line -> LOG.info(line)) (4)
    .build()
    .connect();
1 Override the default command
2 Override the default working directory
3 Set additional environment variables for the server process
4 Custom handler for server stderr lines; by default stderr is forwarded to System.err

The server’s stderr output can also be inspected in tests via client.stderrLines(). This is useful for asserting that the server logged expected messages, since Quarkus redirects console logging to stderr for the STDIO transport.

Testing Tools

Listing Tools

Test that your tools are registered and exposed correctly:

import static org.junit.jupiter.api.Assertions.*;

@Test
public void testToolsList() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsList(page -> {
            assertEquals(3, page.size()); (1)

            ToolInfo tool = page.findByName("calculate"); (2)
            assertEquals("Perform calculations", tool.description());
            assertNotNull(tool.inputSchema()); (3)
        })
        .thenAssertResults(); (4)
}
1 Verify the number of tools
2 Find a specific tool by name
3 Verify tool metadata (schema, description, etc.)
4 Execute assertions

Calling Tools

Test tool execution and response validation:

@Test
public void testToolCall() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsCall("greet",
            Map.of("name", "Alice"), (1)
            response -> {
                assertFalse(response.isError()); (2)
                TextContent content = response.firstContent().asText(); (3)
                assertEquals("Hello, Alice!", content.text());
            })
        .thenAssertResults();
}
1 Pass tool arguments as a Map
2 Verify the call succeeded
3 Extract and validate response content

Testing Tool Errors

Verify error handling in your tools:

@Test
public void testToolError() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsCall("divide",
            Map.of("a", 10, "b", 0), (1)
            response -> {
                assertTrue(response.isError()); (2)
                String errorText = response.firstContent().asText().text();
                assertTrue(errorText.contains("division by zero")); (3)
            })
        .thenAssertResults();
}
1 Trigger an error condition
2 Verify the response indicates an error
3 Check the error message content

Testing Protocol Errors

Test JSON-RPC protocol errors (McpException):

import io.quarkiverse.mcp.server.JsonRpcErrorCodes;

@Test
public void testProtocolError() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsCall("secureOperation")
        .withErrorAssert(error -> { (1)
            assertEquals(JsonRpcErrorCodes.SECURITY_ERROR, error.code()); (2)
            assertEquals("Unauthorized", error.message());
        })
        .send()
        .thenAssertResults();
}
1 Use withErrorAssert for protocol errors
2 Validate the JSON-RPC error code and message

Testing Resources

Listing Resources

@Test
public void testResourcesList() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .resourcesList(page -> {
            assertEquals(2, page.size());

            ResourceInfo resource = page.findByUri("file:///config.json");
            assertEquals("application/json", resource.mimeType());
            assertEquals("Configuration", resource.name());
        })
        .thenAssertResults();
}

Reading Resources

@Test
public void testResourceRead() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .resourcesRead("file:///config.json", response -> {
            TextResourceContents contents =
                response.contents().get(0).asText();
            assertTrue(contents.text().contains("\"enabled\": true"));
            assertEquals("file:///config.json", contents.uri());
        })
        .thenAssertResults();
}

Testing Resource Templates

@Test
public void testResourceTemplates() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .resourcesTemplatesList(page -> {
            ResourceTemplateInfo template =
                page.findByUriTemplate("file:///{path}");
            assertEquals("Dynamic file access", template.description());
        })
        .resourcesRead("file:///data/users.json", response -> {
            // Validate templated resource
            assertFalse(response.contents().isEmpty());
        })
        .thenAssertResults();
}

Testing Prompts

Listing Prompts

@Test
public void testPromptsList() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .promptsList(page -> {
            PromptInfo prompt = page.findByName("code_review");
            assertEquals("Code Review Assistant", prompt.title());

            // Verify prompt arguments
            List<PromptArgument> args = prompt.arguments();
            assertEquals(2, args.size());
            assertTrue(args.stream()
                .anyMatch(arg -> arg.name().equals("language")));
        })
        .thenAssertResults();
}

Getting Prompts

@Test
public void testPromptGet() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .promptsGet("code_review",
            Map.of("language", "java", "level", "senior"),
            response -> {
                assertEquals(1, response.messages().size());
                PromptMessage message = response.messages().get(0);
                assertEquals(Role.USER, message.role());
                String text = message.content().asText().text();
                assertTrue(text.contains("java"));
            })
        .thenAssertResults();
}

Batch Testing

Send multiple requests and validate them together:

@Test
public void testBatchOperations() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsCall("add", Map.of("a", 5, "b", 3),
            r -> assertEquals("8", r.firstContent().asText().text()))
        .toolsCall("multiply", Map.of("a", 5, "b", 3),
            r -> assertEquals("15", r.firstContent().asText().text()))
        .toolsCall("subtract", Map.of("a", 5, "b", 3),
            r -> assertEquals("2", r.firstContent().asText().text()))
        .thenAssertResults(); (1)
}
1 All requests are sent and validated together

Advanced Client Configuration

Custom Base URI

@Test
public void testCustomUri() {
    McpStreamableTestClient client = McpAssured.newStreamableClient()
        .setBaseUri(URI.create("http://localhost:8081")) (1)
        .build()
        .connect();

    // Test operations...
}
1 Override the default base URI

Client Capabilities

import io.quarkiverse.mcp.server.ClientCapability;

@Test
public void testClientCapabilities() {
    McpStreamableTestClient client = McpAssured.newStreamableClient()
        .setClientCapabilities(
            ClientCapability.SAMPLING, (1)
            ClientCapability.ROOTS)
        .build()
        .connect();

    // Server will see these capabilities during initialization
}
1 Specify which client capabilities to advertise

Custom Headers (HTTP Transports)

@Test
public void testCustomHeaders() {
    McpStreamableTestClient client = McpAssured.newStreamableClient()
        .setAdditionalHeaders(msg -> {
            MultiMap headers = MultiMap.caseInsensitiveMultiMap();
            headers.add("X-API-Key", "secret123"); (1)
            headers.add("X-Request-ID", UUID.randomUUID().toString());
            return headers;
        })
        .build()
        .connect();
}
1 Add custom HTTP headers for each request

Basic Authentication

@Test
public void testBasicAuth() {
    McpStreamableTestClient client = McpAssured.newStreamableClient()
        .setBasicAuth("username", "password") (1)
        .build()
        .connect();
}
1 Enable HTTP Basic Authentication

Inspecting Raw Messages

Access raw JSON-RPC messages for advanced testing:

@Test
public void testRawMessages() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsCall("echo", Map.of("message", "test"))
        .send()
        .thenAssertResults();

    Snapshot snapshot = client.snapshot(); (1)

    assertEquals(1, snapshot.requests().size()); (2)
    JsonObject request = snapshot.requests().get(0);
    assertEquals("tools/call", request.getString("method"));

    assertEquals(1, snapshot.responses().size()); (3)
    JsonObject response = snapshot.responses().get(0);
    assertNotNull(response.getJsonObject("result"));
}
1 Capture a snapshot of all messages
2 Inspect raw request messages
3 Inspect raw response messages

Testing Server Notifications

Wait for and validate server-initiated notifications:

@Test
public void testProgressNotifications() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    // Trigger an operation that sends progress notifications
    client.sendAndForget(
        client.newRequest("tools/call")
            .put("params", new JsonObject()
                .put("name", "long_operation")));

    Snapshot snapshot = client.waitForNotifications(2); (1)

    assertEquals(2, snapshot.notifications().size());
    JsonObject notification = snapshot.notifications().get(0);
    assertEquals("notifications/progress", notification.getString("method")); (2)
}
1 Wait for expected number of notifications
2 Validate notification content

Testing Metadata

Validate metadata in tools, resources, and responses:

@Test
public void testMetadata() {
    McpStreamableTestClient client = McpAssured.newConnectedStreamableClient();

    client.when()
        .toolsList(page -> {
            ToolInfo tool = page.findByName("alpha");
            JsonObject meta = tool.meta(); (1)
            assertNotNull(meta);
            assertEquals("high", meta.getString("priceLevel"));
            assertEquals(100, meta.getInteger("price"));
        })
        .toolsCall("alpha", Map.of("price", 1), response -> {
            TextContent content = response.firstContent().asText();
            assertEquals(1, content._meta().size()); (2)

            Map<MetaKey, Object> responseMeta = response._meta(); (3)
            assertTrue(responseMeta.containsKey(new MetaKey("alpha-foo")));
        })
        .thenAssertResults();
}
1 Tool-level metadata
2 Content-level metadata
3 Response-level metadata

See Also