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 SSE, Streamable HTTP, WebSocket, and STDIO 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

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.

SSE Transport Client

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

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

    // Test operations...

    client.disconnect(); (2)
}
1 Creates and connects a new SSE client with default configuration
2 Clean up the connection after testing

Streamable HTTP Transport Client

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

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

    // Test operations...

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

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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newSseClient()
        .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() {
    McpSseTestClient client = McpAssured.newSseClient()
        .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() {
    McpSseTestClient client = McpAssured.newSseClient()
        .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() {
    McpSseTestClient client = McpAssured.newSseClient()
        .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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    // 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() {
    McpSseTestClient client = McpAssured.newConnectedSseClient();

    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