Add MCP conformance tests with client and server test suites (#767)

Implement conformance testing modules with comprehensive client/server test scenarios,
sync conversion for client/servlet code, CI integration, and documentation updates.

---------

Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
This commit is contained in:
Dariusz Jędrzejczyk 2026-01-30 15:51:19 +01:00 • committed by GitHub
commit 4fe8f3efef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1558 additions and 0 deletions

View file

@ -0,0 +1,135 @@
# MCP Conformance Tests - JDK HTTP Client
This module provides a conformance test client implementation for the Java MCP SDK using the JDK HTTP Client with Streamable HTTP transport.
## Overview
The conformance test client is designed to work with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance). It validates that the Java MCP SDK client properly implements the MCP specification.
## Architecture
The client reads test scenarios from environment variables and accepts the server URL as a command-line argument, following the conformance framework's conventions:
- **MCP_CONFORMANCE_SCENARIO**: Environment variable specifying which test scenario to run
- **Server URL**: Passed as the last command-line argument
## Supported Scenarios
Currently implemented scenarios:
- **initialize**: Tests the MCP client initialization handshake only
- ✅ Validates protocol version negotiation
- ✅ Validates clientInfo (name and version)
- ✅ Validates proper handling of server capabilities
- Does NOT call any tools or perform additional operations
- **tools_call**: Tests tool discovery and invocation
- ✅ Initializes the client
- ✅ Lists available tools from the server
- ✅ Calls the `add_numbers` tool with test arguments (a=5, b=3)
- ✅ Validates the tool result
- **elicitation-sep1034-client-defaults**: Tests client applies default values for omitted elicitation fields (SEP-1034)
- ✅ Initializes the client
- ✅ Lists available tools from the server
- ✅ Calls the `test_client_elicitation_defaults` tool
- ✅ Validates that the client properly applies default values from JSON schema to elicitation responses (5/5 checks pass)
- **sse-retry**: Tests client respects SSE retry field timing and reconnects properly (SEP-1699)
- ⚠️ Initializes the client
- ⚠️ Lists available tools from the server
- ⚠️ Calls the `test_reconnection` tool which triggers SSE stream closure
- ✅ Client reconnects after stream closure (PASSING)
- ❌ Client does not respect retry timing (FAILING)
- ⚠️ Client does not send Last-Event-ID header (WARNING - SHOULD requirement)
## Building
Build the executable JAR:
```bash
cd conformance-tests/client-jdk-http-client
../../mvnw clean package -DskipTests
```
This creates an executable JAR at:
```
target/client-jdk-http-client-0.18.0-SNAPSHOT.jar
```
## Running Tests
### Using the Conformance Framework
Run a single scenario:
```bash
npx @modelcontextprotocol/conformance client \
--command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario initialize
npx @modelcontextprotocol/conformance client \
--command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario tools_call
npx @modelcontextprotocol/conformance client \
--command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario elicitation-sep1034-client-defaults
npx @modelcontextprotocol/conformance client \
--command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario sse-retry
```
Run with verbose output:
```bash
npx @modelcontextprotocol/conformance client \
--command "java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario initialize \
--verbose
```
### Manual Testing
You can also run the client manually if you have a test server:
```bash
export MCP_CONFORMANCE_SCENARIO=initialize
java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar http://localhost:3000/mcp
```
## Test Results
The conformance framework generates test results showing:
**Current Status (3/4 scenarios passing):**
- ✅ initialize: 1/1 checks passed
- ✅ tools_call: 1/1 checks passed
- ✅ elicitation-sep1034-client-defaults: 5/5 checks passed
- ⚠️ sse-retry: 1/2 checks passed, 1 warning
Test result files are generated in `results/<scenario>-<timestamp>/`:
- `checks.json`: Array of conformance check results with pass/fail status
- `stdout.txt`: Client stdout output
- `stderr.txt`: Client stderr output
### Known Issue: SSE Retry Handling
The `sse-retry` scenario currently fails because:
1. The client treats the SSE `retry:` field as invalid instead of parsing it
2. The client does not implement retry timing (reconnects immediately)
3. The client does not send the Last-Event-ID header on reconnection
This is a known limitation in the `HttpClientStreamableHttpTransport` implementation.
## Next Steps
Future enhancements:
- Fix SSE retry field handling (SEP-1699) to properly parse and respect retry timing
- Implement Last-Event-ID header on reconnection for resumability
- Add auth scenarios (currently excluded as per requirements)
- Implement a comprehensive "everything-client" pattern
- Add to CI/CD pipeline
- Create expected-failures baseline for known issues

View file

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>conformance-tests</artifactId>
<version>0.18.0-SNAPSHOT</version>
</parent>
<artifactId>client-jdk-http-client</artifactId>
<packaging>jar</packaging>
<name>MCP Conformance Tests - JDK HTTP Client</name>
<description>JDK HTTP Client conformance tests for the Java MCP SDK</description>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
<scm>
<url>https://github.com/modelcontextprotocol/java-sdk</url>
<connection>git://github.com/modelcontextprotocol/java-sdk.git</connection>
<developerConnection>git@github.com/modelcontextprotocol/java-sdk.git</developerConnection>
</scm>
<dependencies>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
<version>0.18.0-SNAPSHOT</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Shade Plugin for creating executable JAR with dependencies -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>io.modelcontextprotocol.conformance.client.ConformanceJdkClientMcpClient</mainClass>
</transformer>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,286 @@
package io.modelcontextprotocol.conformance.client;
import java.time.Duration;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.spec.McpSchema;
/**
* MCP Conformance Test Client - JDK HTTP Client Implementation
*
* <p>
* This client is designed to work with the MCP conformance test framework. It reads the
* test scenario from the MCP_CONFORMANCE_SCENARIO environment variable and the server URL
* from command-line arguments.
*
* <p>
* Usage: ConformanceJdkClientMcpClient &lt;server-url&gt;
*
* @see <a href= "https://github.com/modelcontextprotocol/conformance">MCP Conformance
* Test Framework</a>
*/
public class ConformanceJdkClientMcpClient {
public static void main(String[] args) {
if (args.length == 0) {
System.err.println("Usage: ConformanceJdkClientMcpClient <server-url>");
System.err.println("The server URL must be provided as the last command-line argument.");
System.err.println("The MCP_CONFORMANCE_SCENARIO environment variable must be set.");
System.exit(1);
}
String scenario = System.getenv("MCP_CONFORMANCE_SCENARIO");
if (scenario == null || scenario.isEmpty()) {
System.err.println("Error: MCP_CONFORMANCE_SCENARIO environment variable is not set");
System.exit(1);
}
String serverUrl = args[args.length - 1];
try {
switch (scenario) {
case "initialize":
runInitializeScenario(serverUrl);
break;
case "tools_call":
runToolsCallScenario(serverUrl);
break;
case "elicitation-sep1034-client-defaults":
runElicitationDefaultsScenario(serverUrl);
break;
case "sse-retry":
runSSERetryScenario(serverUrl);
break;
default:
System.err.println("Unknown scenario: " + scenario);
System.err.println("Available scenarios:");
System.err.println(" - initialize");
System.err.println(" - tools_call");
System.err.println(" - elicitation-sep1034-client-defaults");
System.err.println(" - sse-retry");
System.exit(1);
}
System.exit(0);
}
catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
/**
* Helper method to create and configure an MCP client with transport.
* @param serverUrl the URL of the MCP server
* @return configured McpSyncClient instance
*/
private static McpSyncClient createClient(String serverUrl) {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
return McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
.requestTimeout(Duration.ofSeconds(30))
.build();
}
/**
* Helper method to create and configure an MCP client with elicitation support.
* @param serverUrl the URL of the MCP server
* @return configured McpSyncClient instance with elicitation handler
*/
private static McpSyncClient createClientWithElicitation(String serverUrl) {
HttpClientStreamableHttpTransport transport = HttpClientStreamableHttpTransport.builder(serverUrl).build();
// Build client capabilities with elicitation support
var capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
return McpClient.sync(transport)
.clientInfo(new McpSchema.Implementation("test-client", "1.0.0"))
.requestTimeout(Duration.ofSeconds(30))
.capabilities(capabilities)
.elicitation(request -> {
// Apply default values from the schema to create the content
var content = new java.util.HashMap<String, Object>();
var schema = request.requestedSchema();
if (schema != null && schema.containsKey("properties")) {
@SuppressWarnings("unchecked")
var properties = (java.util.Map<String, Object>) schema.get("properties");
// Apply defaults for each property
properties.forEach((key, propDef) -> {
@SuppressWarnings("unchecked")
var propMap = (java.util.Map<String, Object>) propDef;
if (propMap.containsKey("default")) {
content.put(key, propMap.get("default"));
}
});
}
// Return accept action with the defaults applied
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT, content, null);
})
.build();
}
/**
* Initialize scenario: Tests MCP client initialization handshake.
* @param serverUrl the URL of the MCP server
* @throws Exception if any error occurs during execution
*/
private static void runInitializeScenario(String serverUrl) throws Exception {
McpSyncClient client = createClient(serverUrl);
try {
// Initialize client
client.initialize();
System.out.println("Successfully connected to MCP server");
}
finally {
// Close the client (which will close the transport)
client.close();
System.out.println("Connection closed successfully");
}
}
/**
* Tools call scenario: Tests tool listing and invocation functionality.
* @param serverUrl the URL of the MCP server
* @throws Exception if any error occurs during execution
*/
private static void runToolsCallScenario(String serverUrl) throws Exception {
McpSyncClient client = createClient(serverUrl);
try {
// Initialize client
client.initialize();
System.out.println("Successfully connected to MCP server");
// List available tools
McpSchema.ListToolsResult toolsResult = client.listTools();
System.out.println("Successfully listed tools");
// Call the add_numbers tool if it exists
if (toolsResult != null && toolsResult.tools() != null) {
for (McpSchema.Tool tool : toolsResult.tools()) {
if ("add_numbers".equals(tool.name())) {
// Call the add_numbers tool with test arguments
var arguments = new java.util.HashMap<String, Object>();
arguments.put("a", 5);
arguments.put("b", 3);
McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("add_numbers", arguments));
System.out.println("Successfully called add_numbers tool");
if (result != null && result.content() != null) {
System.out.println("Tool result: " + result.content());
}
break;
}
}
}
}
finally {
// Close the client (which will close the transport)
client.close();
System.out.println("Connection closed successfully");
}
}
/**
* Elicitation defaults scenario: Tests client applies default values for omitted
* elicitation fields (SEP-1034).
* @param serverUrl the URL of the MCP server
* @throws Exception if any error occurs during execution
*/
private static void runElicitationDefaultsScenario(String serverUrl) throws Exception {
McpSyncClient client = createClientWithElicitation(serverUrl);
try {
// Initialize client
client.initialize();
System.out.println("Successfully connected to MCP server");
// List available tools
McpSchema.ListToolsResult toolsResult = client.listTools();
System.out.println("Successfully listed tools");
// Call the test_client_elicitation_defaults tool if it exists
if (toolsResult != null && toolsResult.tools() != null) {
for (McpSchema.Tool tool : toolsResult.tools()) {
if ("test_client_elicitation_defaults".equals(tool.name())) {
// Call the tool which will trigger an elicitation request
var arguments = new java.util.HashMap<String, Object>();
McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("test_client_elicitation_defaults", arguments));
System.out.println("Successfully called test_client_elicitation_defaults tool");
if (result != null && result.content() != null) {
System.out.println("Tool result: " + result.content());
}
break;
}
}
}
}
finally {
// Close the client (which will close the transport)
client.close();
System.out.println("Connection closed successfully");
}
}
/**
* SSE retry scenario: Tests client respects SSE retry field timing and reconnects
* properly (SEP-1699).
* @param serverUrl the URL of the MCP server
* @throws Exception if any error occurs during execution
*/
private static void runSSERetryScenario(String serverUrl) throws Exception {
McpSyncClient client = createClient(serverUrl);
try {
// Initialize client
client.initialize();
System.out.println("Successfully connected to MCP server");
// List available tools
McpSchema.ListToolsResult toolsResult = client.listTools();
System.out.println("Successfully listed tools");
// Call the test_reconnection tool if it exists
if (toolsResult != null && toolsResult.tools() != null) {
for (McpSchema.Tool tool : toolsResult.tools()) {
if ("test_reconnection".equals(tool.name())) {
// Call the tool which will trigger SSE stream closure and
// reconnection
var arguments = new java.util.HashMap<String, Object>();
McpSchema.CallToolResult result = client
.callTool(new McpSchema.CallToolRequest("test_reconnection", arguments));
System.out.println("Successfully called test_reconnection tool");
if (result != null && result.content() != null) {
System.out.println("Tool result: " + result.content());
}
break;
}
}
}
}
finally {
// Close the client (which will close the transport)
client.close();
System.out.println("Connection closed successfully");
}
}
}

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- Set default log level -->
<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
<!-- More verbose logging for MCP client -->
<logger name="io.modelcontextprotocol" level="DEBUG" />
</configuration>