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 |
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 |