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
14 changed files with 1558 additions and 0 deletions

62
.github/workflows/conformance.yml vendored Normal file
View file

@ -0,0 +1,62 @@
name: Conformance Tests
on:
pull_request: {}
push:
branches: [main]
workflow_dispatch:
jobs:
server:
name: Server Conformance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build and start server
run: |
mvn clean install -DskipTests
mvn exec:java -pl conformance-tests/server-servlet -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet" &
timeout 30 bash -c 'until curl -s http://localhost:8080/mcp > /dev/null 2>&1; do sleep 0.5; done'
- name: Run conformance tests
uses: modelcontextprotocol/conformance@v0.1.11
with:
mode: server
url: http://localhost:8080/mcp
suite: active
expected-failures: ./conformance-tests/conformance-baseline.yml
client:
name: Client Conformance
runs-on: ubuntu-latest
strategy:
matrix:
scenario: [initialize, tools_call, elicitation-sep1034-client-defaults, sse-retry]
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- name: Build client
run: mvn clean install -DskipTests
- name: Run conformance test
uses: modelcontextprotocol/conformance@v0.1.11
with:
mode: client
command: 'java -jar conformance-tests/client-jdk-http-client/target/client-jdk-http-client-0.18.0-SNAPSHOT.jar'
scenario: ${{ matrix.scenario }}
expected-failures: ./conformance-tests/conformance-baseline.yml

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ build/
out
/.gradletasknamecache
**/*.flattened-pom.xml
**/dependency-reduced-pom.xml
### IDE - Eclipse/STS ###
.apt_generated

View file

@ -0,0 +1,82 @@
# MCP Java SDK Conformance Test Validation Results
## Summary
**Server Tests:** 37/40 passed (92.5%)
**Client Tests:** 3/4 scenarios passed (9/10 checks passed)
## Server Test Results
### Passing (37/40)
- **Lifecycle & Utilities (4/4):** initialize, ping, logging-set-level, completion-complete
- **Tools (11/11):** All scenarios including progress notifications ✨
- **Elicitation (10/10):** SEP-1034 defaults (5 checks), SEP-1330 enums (5 checks)
- **Resources (4/6):** list, read-text, read-binary, templates-read
- **Prompts (4/4):** list, simple, with-args, embedded-resource, with-image
- **SSE Transport (2/2):** Multiple streams
- **Security (1/2):** Localhost validation passes
### Failing (3/40)
1. **resources-subscribe** - Not implemented in SDK
2. **resources-unsubscribe** - Not implemented in SDK
3. **dns-rebinding-protection** - Missing Host/Origin validation (1/2 checks)
## Client Test Results
### Passing (3/4 scenarios, 9/10 checks)
- **initialize (1/1):** Protocol negotiation, clientInfo, capabilities
- **tools_call (1/1):** Tool discovery and invocation
- **elicitation-sep1034-client-defaults (5/5):** Default values for string, integer, number, enum, boolean
### Partially Passing (1/4 scenarios, 1/2 checks)
- **sse-retry (1/2 + 1 warning):**
- ✅ Reconnects after stream closure
- ❌ Does not respect retry timing
- ⚠️ Does not send Last-Event-ID header (SHOULD requirement)
**Issue:** Client treats `retry:` SSE field as invalid instead of parsing it for reconnection timing.
## Known Limitations
1. **Resource Subscriptions:** SDK doesn't implement `resources/subscribe` and `resources/unsubscribe` handlers
2. **Client SSE Retry:** Client doesn't parse or respect the `retry:` field, reconnects immediately, and doesn't send Last-Event-ID header
3. **DNS Rebinding Protection:** Missing Host/Origin header validation in server transport
## Running Tests
### Server
```bash
# Start server
cd conformance-tests/server-servlet
../../mvnw compile exec:java -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
# Run tests (in another terminal)
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --suite active
```
### Client
```bash
# Build
cd conformance-tests/client-jdk-http-client
../../mvnw clean package -DskipTests
# Run all scenarios
for scenario in initialize tools_call elicitation-sep1034-client-defaults sse-retry; do
npx @modelcontextprotocol/conformance client \
--command "java -jar target/client-jdk-http-client-0.18.0-SNAPSHOT.jar" \
--scenario $scenario
done
```
## Recommendations
### High Priority
1. Fix client SSE retry field handling in `HttpClientStreamableHttpTransport`
2. Implement resource subscription handlers in `McpStatelessAsyncServer`
### Medium Priority
3. Add Host/Origin validation in `HttpServletStreamableServerTransportProvider` for DNS rebinding protection

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>

View file

@ -0,0 +1,17 @@
# MCP Java SDK Conformance Test Baseline
# This file lists known failing scenarios that are expected to fail until fixed.
# See: https://github.com/modelcontextprotocol/conformance/blob/main/SDK_INTEGRATION.md
server:
# Resource subscription not implemented in SDK
- resources-subscribe
- resources-unsubscribe
# DNS rebinding protection missing Host/Origin validation
- dns-rebinding-protection
client:
# SSE retry field handling not implemented
# - Client does not parse or respect retry: field timing
# - Client does not send Last-Event-ID header
- sse-retry

28
conformance-tests/pom.xml Normal file
View file

@ -0,0 +1,28 @@
<?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>mcp-parent</artifactId>
<version>0.18.0-SNAPSHOT</version>
</parent>
<artifactId>conformance-tests</artifactId>
<packaging>pom</packaging>
<name>MCP Conformance Tests</name>
<description>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>
<modules>
<module>client-jdk-http-client</module>
<module>server-servlet</module>
</modules>
</project>

View file

@ -0,0 +1,205 @@
# MCP Conformance Tests - Servlet Server
This module contains a comprehensive MCP (Model Context Protocol) server implementation for conformance testing using the servlet stack with an embedded Tomcat server and streamable HTTP transport.
## Conformance Test Results
**Status: 37 out of 40 tests passing (92.5%)**
The server has been validated against the official [MCP conformance test suite](https://github.com/modelcontextprotocol/conformance). See [VALIDATION_RESULTS.md](../VALIDATION_RESULTS.md) for detailed results.
### What's Implemented
✅ **Lifecycle & Utilities** (4/4)
- Server initialization, ping, logging, completion
✅ **Tools** (11/11)
- Text, image, audio, embedded resources, mixed content
- Logging, error handling, sampling, elicitation
- Progress notifications
✅ **Elicitation** (10/10)
- SEP-1034: Default values for all primitive types
- SEP-1330: All enum schema variants
✅ **Resources** (4/6)
- List, read text/binary, templates
- ⚠️ Subscribe/unsubscribe (SDK limitation)
✅ **Prompts** (4/4)
- Simple, parameterized, embedded resources, images
✅ **SSE Transport** (2/2)
- Multiple streams support
⚠️ **Security** (1/2)
- ⚠️ DNS rebinding protection (SDK limitation)
## Features
- Embedded Tomcat servlet container
- MCP server using HttpServletStreamableServerTransportProvider
- Comprehensive test coverage with 15+ tools
- Streamable HTTP transport with SSE on `/mcp` endpoint
- Support for all MCP content types (text, image, audio, resources)
- Advanced features: sampling, elicitation, progress (partial), completion
## Running the Server
To run the conformance server:
```bash
cd conformance-tests/server-servlet
../../mvnw compile exec:java -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
```
Or from the root directory:
```bash
./mvnw compile exec:java -pl conformance-tests/server-servlet -Dexec.mainClass="io.modelcontextprotocol.conformance.server.ConformanceServlet"
```
The server will start on port 8080 with the MCP endpoint at `/mcp`.
## Running Conformance Tests
Once the server is running, you can validate it against the official MCP conformance test suite using `npx`:
### Run Full Active Test Suite
```bash
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --suite active
```
### Run Specific Scenarios
```bash
# Test tools
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario tools-list --verbose
# Test prompts
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario prompts-list --verbose
# Test resources
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario resources-read-text --verbose
# Test elicitation with defaults
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --scenario elicitation-sep1034-defaults --verbose
```
### Available Test Suites
- `active` (default) - All active/stable tests (30 scenarios)
- `all` - All tests including pending/experimental
- `pending` - Only pending/experimental tests
### Common Scenarios
**Lifecycle & Utilities:**
- `server-initialize` - Server initialization
- `ping` - Ping utility
- `logging-set-level` - Logging configuration
- `completion-complete` - Argument completion
**Tools:**
- `tools-list` - List available tools
- `tools-call-simple-text` - Simple text response
- `tools-call-image` - Image content
- `tools-call-audio` - Audio content
- `tools-call-with-logging` - Logging during execution
- `tools-call-with-progress` - Progress notifications
- `tools-call-sampling` - LLM sampling
- `tools-call-elicitation` - User input requests
**Resources:**
- `resources-list` - List resources
- `resources-read-text` - Read text resource
- `resources-read-binary` - Read binary resource
- `resources-templates-read` - Resource templates
- `resources-subscribe` - Subscribe to resource updates
- `resources-unsubscribe` - Unsubscribe from updates
**Prompts:**
- `prompts-list` - List prompts
- `prompts-get-simple` - Simple prompt
- `prompts-get-with-args` - Parameterized prompt
- `prompts-get-embedded-resource` - Prompt with resource
- `prompts-get-with-image` - Prompt with image
**Elicitation:**
- `elicitation-sep1034-defaults` - Default values (SEP-1034)
- `elicitation-sep1330-enums` - Enum schemas (SEP-1330)
## Testing with curl
You can also test the endpoint manually:
```bash
# Check endpoint (will show SSE requirement)
curl -X GET http://localhost:8080/mcp
# Initialize session with proper headers
curl -X POST http://localhost:8080/mcp \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "mcp-session-id: test-session-123" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}'
```
## Architecture
- **Transport**: HttpServletStreamableServerTransportProvider (streamable HTTP with SSE)
- **Container**: Embedded Apache Tomcat
- **Protocol**: Streamable HTTP with Server-Sent Events
- **Port**: 8080 (default)
- **Endpoint**: `/mcp`
- **Request Timeout**: 30 seconds
## Implemented Tools
### Content Type Tools
- `test_simple_text` - Returns simple text content
- `test_image_content` - Returns a minimal PNG image (1x1 red pixel)
- `test_audio_content` - Returns a minimal WAV audio file
- `test_embedded_resource` - Returns embedded resource content
- `test_multiple_content_types` - Returns mixed text, image, and resource content
### Behavior Tools
- `test_tool_with_logging` - Sends log notifications during execution
- `test_error_handling` - Intentionally returns an error for testing
- `test_tool_with_progress` - Reports progress notifications (⚠️ SDK issue)
### Interactive Tools
- `test_sampling` - Requests LLM sampling from client
- `test_elicitation` - Requests user input from client
- `test_elicitation_sep1034_defaults` - Elicitation with default values (SEP-1034)
- `test_elicitation_sep1330_enums` - Elicitation with enum schemas (SEP-1330)
## Implemented Prompts
- `test_simple_prompt` - Simple prompt without arguments
- `test_prompt_with_arguments` - Prompt with required arguments (arg1, arg2)
- `test_prompt_with_embedded_resource` - Prompt with embedded resource content
- `test_prompt_with_image` - Prompt with image content
## Implemented Resources
- `test://static-text` - Static text resource
- `test://static-binary` - Static binary resource (PNG image)
- `test://watched-resource` - Resource that can be subscribed to
- `test://template/{id}/data` - Resource template with parameter substitution
## Known Limitations
See [VALIDATION_RESULTS.md](../VALIDATION_RESULTS.md) for details on:
1. **Resource Subscriptions** - Not implemented in Java SDK
2. **DNS Rebinding Protection** - Missing Host/Origin validation
These are SDK-level limitations that require fixes in the core framework.
## References
- [MCP Specification](https://modelcontextprotocol.io/specification/)
- [MCP Conformance Tests](https://github.com/modelcontextprotocol/conformance)
- [SDK Integration Guide](https://github.com/modelcontextprotocol/conformance/blob/main/SDK_INTEGRATION.md)

View file

@ -0,0 +1,69 @@
<?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>server-servlet</artifactId>
<packaging>jar</packaging>
<name>MCP Conformance Tests - Servlet Server</name>
<description>Servlet Server 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>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j-api.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>${jakarta.servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<mainClass>io.modelcontextprotocol.conformance.server.ConformanceServlet</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,564 @@
package io.modelcontextprotocol.conformance.server;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema.*;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConformanceServlet {
private static final Logger logger = LoggerFactory.getLogger(ConformanceServlet.class);
private static final int PORT = 8080;
private static final String MCP_ENDPOINT = "/mcp";
private static final JsonSchema EMPTY_JSON_SCHEMA = new JsonSchema("object", Collections.emptyMap(), null, null,
null, null);
// Minimal 1x1 red pixel PNG (base64 encoded)
private static final String RED_PIXEL_PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==";
// Minimal WAV file (base64 encoded) - 1 sample at 8kHz
private static final String MINIMAL_WAV = "UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQAAAAA=";
public static void main(String[] args) throws Exception {
logger.info("Starting MCP Conformance Tests - Servlet Server");
HttpServletStreamableServerTransportProvider transportProvider = HttpServletStreamableServerTransportProvider
.builder()
.mcpEndpoint(MCP_ENDPOINT)
.keepAliveInterval(Duration.ofSeconds(30))
.build();
// Build server with all conformance test features
var mcpServer = McpServer.sync(transportProvider)
.serverInfo("mcp-conformance-server", "1.0.0")
.capabilities(ServerCapabilities.builder()
.completions()
.resources(true, false)
.tools(false)
.prompts(false)
.build())
.tools(createToolSpecs())
.prompts(createPromptSpecs())
.resources(createResourceSpecs())
.resourceTemplates(createResourceTemplateSpecs())
.completions(createCompletionSpecs())
.requestTimeout(Duration.ofSeconds(30))
.build();
// Set up embedded Tomcat
Tomcat tomcat = createEmbeddedTomcat(transportProvider);
try {
tomcat.start();
logger.info("Conformance MCP Servlet Server started on port {} with endpoint {}", PORT, MCP_ENDPOINT);
logger.info("Server URL: http://localhost:{}{}", PORT, MCP_ENDPOINT);
// Keep the server running
tomcat.getServer().await();
}
catch (LifecycleException e) {
logger.error("Failed to start Tomcat server", e);
throw e;
}
finally {
logger.info("Shutting down MCP server...");
mcpServer.closeGracefully();
try {
tomcat.stop();
tomcat.destroy();
}
catch (LifecycleException e) {
logger.error("Error during Tomcat shutdown", e);
}
}
}
private static Tomcat createEmbeddedTomcat(HttpServletStreamableServerTransportProvider transportProvider) {
Tomcat tomcat = new Tomcat();
tomcat.setPort(PORT);
String baseDir = System.getProperty("java.io.tmpdir");
tomcat.setBaseDir(baseDir);
Context context = tomcat.addContext("", baseDir);
// Add the MCP servlet to Tomcat
org.apache.catalina.Wrapper wrapper = context.createWrapper();
wrapper.setName("mcpServlet");
wrapper.setServlet(transportProvider);
wrapper.setLoadOnStartup(1);
wrapper.setAsyncSupported(true);
context.addChild(wrapper);
context.addServletMappingDecoded("/*", "mcpServlet");
var connector = tomcat.getConnector();
connector.setAsyncTimeout(30000);
return tomcat;
}
private static List<McpServerFeatures.SyncToolSpecification> createToolSpecs() {
return List.of(
// test_simple_text - Returns simple text content
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_simple_text")
.description("Returns simple text content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_simple_text' called");
return CallToolResult.builder()
.content(List.of(new TextContent("This is a simple text response for testing.")))
.isError(false)
.build();
})
.build(),
// test_image_content - Returns image content
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_image_content")
.description("Returns image content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_image_content' called");
return CallToolResult.builder()
.content(List.of(new ImageContent(null, RED_PIXEL_PNG, "image/png")))
.isError(false)
.build();
})
.build(),
// test_audio_content - Returns audio content
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_audio_content")
.description("Returns audio content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_audio_content' called");
return CallToolResult.builder()
.content(List.of(new AudioContent(null, MINIMAL_WAV, "audio/wav")))
.isError(false)
.build();
})
.build(),
// test_embedded_resource - Returns embedded resource content
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_embedded_resource")
.description("Returns embedded resource content for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_embedded_resource' called");
TextResourceContents resourceContents = new TextResourceContents("test://embedded-resource",
"text/plain", "This is an embedded resource content.");
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
return CallToolResult.builder().content(List.of(embeddedResource)).isError(false).build();
})
.build(),
// test_multiple_content_types - Returns multiple content types
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_multiple_content_types")
.description("Returns multiple content types for testing")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_multiple_content_types' called");
TextResourceContents resourceContents = new TextResourceContents(
"test://mixed-content-resource", "application/json",
"{\"test\":\"data\",\"value\":123}");
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
return CallToolResult.builder()
.content(List.of(new TextContent("Multiple content types test:"),
new ImageContent(null, RED_PIXEL_PNG, "image/png"), embeddedResource))
.isError(false)
.build();
})
.build(),
// test_tool_with_logging - Tool that sends log messages during execution
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_tool_with_logging")
.description("Tool that sends log messages during execution")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_tool_with_logging' called");
// Send log notifications
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Tool execution started")
.build());
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Tool processing data")
.build());
exchange.loggingNotification(LoggingMessageNotification.builder()
.level(LoggingLevel.INFO)
.data("Tool execution completed")
.build());
return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed with logging")))
.isError(false)
.build();
})
.build(),
// test_error_handling - Tool that always returns an error
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_error_handling")
.description("Tool that returns an error for testing error handling")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_error_handling' called");
return CallToolResult.builder()
.content(List.of(new TextContent("This tool intentionally returns an error for testing")))
.isError(true)
.build();
})
.build(),
// test_tool_with_progress - Tool that reports progress
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_tool_with_progress")
.description("Tool that reports progress notifications")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_tool_with_progress' called");
Object progressToken = request.meta().get("progressToken");
if (progressToken != null) {
// Send progress notifications sequentially
exchange.progressNotification(new ProgressNotification(progressToken, 0.0, 100.0, null));
// try {
// Thread.sleep(50);
// }
// catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
exchange.progressNotification(new ProgressNotification(progressToken, 50.0, 100.0, null));
// try {
// Thread.sleep(50);
// }
// catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
exchange.progressNotification(new ProgressNotification(progressToken, 100.0, 100.0, null));
return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed with progress")))
.isError(false)
.build();
}
else {
// No progress token, just execute with delays
// try {
// Thread.sleep(100);
// }
// catch (InterruptedException e) {
// Thread.currentThread().interrupt();
// }
return CallToolResult.builder()
.content(List.of(new TextContent("Tool execution completed without progress")))
.isError(false)
.build();
}
})
.build(),
// test_sampling - Tool that requests LLM sampling from client
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_sampling")
.description("Tool that requests LLM sampling from client")
.inputSchema(new JsonSchema("object",
Map.of("prompt",
Map.of("type", "string", "description", "The prompt to send to the LLM")),
List.of("prompt"), null, null, null))
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_sampling' called");
String prompt = (String) request.arguments().get("prompt");
// Request sampling from client
CreateMessageRequest samplingRequest = CreateMessageRequest.builder()
.messages(List.of(new SamplingMessage(Role.USER, new TextContent(prompt))))
.maxTokens(100)
.build();
CreateMessageResult response = exchange.createMessage(samplingRequest);
String responseText = "LLM response: " + ((TextContent) response.content()).text();
return CallToolResult.builder()
.content(List.of(new TextContent(responseText)))
.isError(false)
.build();
})
.build(),
// test_elicitation - Tool that requests user input from client
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_elicitation")
.description("Tool that requests user input from client")
.inputSchema(new JsonSchema("object",
Map.of("message",
Map.of("type", "string", "description", "The message to show the user")),
List.of("message"), null, null, null))
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation' called");
String message = (String) request.arguments().get("message");
// Request elicitation from client
Map<String, Object> requestedSchema = Map.of("type", "object", "properties",
Map.of("username", Map.of("type", "string", "description", "User's response"), "email",
Map.of("type", "string", "description", "User's email address")),
"required", List.of("username", "email"));
ElicitRequest elicitRequest = new ElicitRequest(message, requestedSchema);
ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "User response: action=" + response.action() + ", content="
+ response.content();
return CallToolResult.builder()
.content(List.of(new TextContent(responseText)))
.isError(false)
.build();
})
.build(),
// test_elicitation_sep1034_defaults - Tool with default values for all
// primitive types
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_elicitation_sep1034_defaults")
.description("Tool that requests elicitation with default values for all primitive types")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation_sep1034_defaults' called");
// Create schema with default values for all primitive types
Map<String, Object> requestedSchema = Map.of("type", "object", "properties",
Map.of("name", Map.of("type", "string", "default", "John Doe"), "age",
Map.of("type", "integer", "default", 30), "score",
Map.of("type", "number", "default", 95.5), "status",
Map.of("type", "string", "enum", List.of("active", "inactive", "pending"),
"default", "active"),
"verified", Map.of("type", "boolean", "default", true)),
"required", List.of("name", "age", "score", "status", "verified"));
ElicitRequest elicitRequest = new ElicitRequest("Please provide your information with defaults",
requestedSchema);
ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ response.content();
return CallToolResult.builder()
.content(List.of(new TextContent(responseText)))
.isError(false)
.build();
})
.build(),
// test_elicitation_sep1330_enums - Tool with enum schema improvements
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder()
.name("test_elicitation_sep1330_enums")
.description("Tool that requests elicitation with enum schema improvements")
.inputSchema(EMPTY_JSON_SCHEMA)
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'test_elicitation_sep1330_enums' called");
// Create schema with all 5 enum variants
Map<String, Object> requestedSchema = Map.of("type", "object", "properties", Map.of(
// 1. Untitled single-select
"untitledSingle",
Map.of("type", "string", "enum", List.of("option1", "option2", "option3")),
// 2. Titled single-select using oneOf with const/title
"titledSingle",
Map.of("type", "string", "oneOf",
List.of(Map.of("const", "value1", "title", "First Option"),
Map.of("const", "value2", "title", "Second Option"),
Map.of("const", "value3", "title", "Third Option"))),
// 3. Legacy titled using enumNames (deprecated)
"legacyEnum",
Map.of("type", "string", "enum", List.of("opt1", "opt2", "opt3"), "enumNames",
List.of("Option One", "Option Two", "Option Three")),
// 4. Untitled multi-select
"untitledMulti",
Map.of("type", "array", "items",
Map.of("type", "string", "enum", List.of("option1", "option2", "option3"))),
// 5. Titled multi-select using items.anyOf with
// const/title
"titledMulti",
Map.of("type", "array", "items",
Map.of("anyOf",
List.of(Map.of("const", "value1", "title", "First Choice"),
Map.of("const", "value2", "title", "Second Choice"),
Map.of("const", "value3", "title", "Third Choice"))))),
"required", List.of("untitledSingle", "titledSingle", "legacyEnum", "untitledMulti",
"titledMulti"));
ElicitRequest elicitRequest = new ElicitRequest("Select your preferences", requestedSchema);
ElicitResult response = exchange.createElicitation(elicitRequest);
String responseText = "Elicitation completed: action=" + response.action() + ", content="
+ response.content();
return CallToolResult.builder()
.content(List.of(new TextContent(responseText)))
.isError(false)
.build();
})
.build());
}
private static List<McpServerFeatures.SyncPromptSpecification> createPromptSpecs() {
return List.of(
// test_simple_prompt - Simple prompt without arguments
new McpServerFeatures.SyncPromptSpecification(
new Prompt("test_simple_prompt", null, "A simple prompt for testing", List.of()),
(exchange, request) -> {
logger.info("Prompt 'test_simple_prompt' requested");
return new GetPromptResult(null, List.of(new PromptMessage(Role.USER,
new TextContent("This is a simple prompt for testing."))));
}),
// test_prompt_with_arguments - Prompt with arguments
new McpServerFeatures.SyncPromptSpecification(
new Prompt("test_prompt_with_arguments", null, "A prompt with arguments for testing",
List.of(new PromptArgument("arg1", "First test argument", true),
new PromptArgument("arg2", "Second test argument", true))),
(exchange, request) -> {
logger.info("Prompt 'test_prompt_with_arguments' requested");
String arg1 = (String) request.arguments().get("arg1");
String arg2 = (String) request.arguments().get("arg2");
String text = String.format("Prompt with arguments: arg1='%s', arg2='%s'", arg1, arg2);
return new GetPromptResult(null,
List.of(new PromptMessage(Role.USER, new TextContent(text))));
}),
// test_prompt_with_embedded_resource - Prompt with embedded resource
new McpServerFeatures.SyncPromptSpecification(
new Prompt("test_prompt_with_embedded_resource", null,
"A prompt with embedded resource for testing",
List.of(new PromptArgument("resourceUri", "URI of the resource to embed", true))),
(exchange, request) -> {
logger.info("Prompt 'test_prompt_with_embedded_resource' requested");
String resourceUri = (String) request.arguments().get("resourceUri");
TextResourceContents resourceContents = new TextResourceContents(resourceUri, "text/plain",
"Embedded resource content for testing.");
EmbeddedResource embeddedResource = new EmbeddedResource(null, resourceContents);
return new GetPromptResult(null,
List.of(new PromptMessage(Role.USER, embeddedResource), new PromptMessage(Role.USER,
new TextContent("Please process the embedded resource above."))));
}),
// test_prompt_with_image - Prompt with image content
new McpServerFeatures.SyncPromptSpecification(new Prompt("test_prompt_with_image", null,
"A prompt with image content for testing", List.of()), (exchange, request) -> {
logger.info("Prompt 'test_prompt_with_image' requested");
return new GetPromptResult(null, List.of(
new PromptMessage(Role.USER, new ImageContent(null, RED_PIXEL_PNG, "image/png")),
new PromptMessage(Role.USER, new TextContent("Please analyze the image above."))));
}));
}
private static List<McpServerFeatures.SyncResourceSpecification> createResourceSpecs() {
return List.of(
// test://static-text - Static text resource
new McpServerFeatures.SyncResourceSpecification(Resource.builder()
.uri("test://static-text")
.name("Static Text Resource")
.description("A static text resource for testing")
.mimeType("text/plain")
.build(), (exchange, request) -> {
logger.info("Resource 'test://static-text' requested");
return new ReadResourceResult(List.of(new TextResourceContents("test://static-text",
"text/plain", "This is the content of the static text resource.")));
}),
// test://static-binary - Static binary resource (image)
new McpServerFeatures.SyncResourceSpecification(Resource.builder()
.uri("test://static-binary")
.name("Static Binary Resource")
.description("A static binary resource for testing")
.mimeType("image/png")
.build(), (exchange, request) -> {
logger.info("Resource 'test://static-binary' requested");
return new ReadResourceResult(
List.of(new BlobResourceContents("test://static-binary", "image/png", RED_PIXEL_PNG)));
}),
// test://watched-resource - Resource that can be subscribed to
new McpServerFeatures.SyncResourceSpecification(Resource.builder()
.uri("test://watched-resource")
.name("Watched Resource")
.description("A resource that can be subscribed to for updates")
.mimeType("text/plain")
.build(), (exchange, request) -> {
logger.info("Resource 'test://watched-resource' requested");
return new ReadResourceResult(List.of(new TextResourceContents("test://watched-resource",
"text/plain", "This is a watched resource content.")));
}));
}
private static List<McpServerFeatures.SyncResourceTemplateSpecification> createResourceTemplateSpecs() {
return List.of(
// test://template/{id}/data - Resource template with parameter
// substitution
new McpServerFeatures.SyncResourceTemplateSpecification(ResourceTemplate.builder()
.uriTemplate("test://template/{id}/data")
.name("Template Resource")
.description("A resource template for testing parameter substitution")
.mimeType("application/json")
.build(), (exchange, request) -> {
logger.info("Resource template 'test://template/{{id}}/data' requested for URI: {}",
request.uri());
// Extract id from URI
String uri = request.uri();
String id = uri.replaceAll("test://template/(.+)/data", "$1");
String jsonContent = String
.format("{\"id\":\"%s\",\"templateTest\":true,\"data\":\"Data for ID: %s\"}", id, id);
return new ReadResourceResult(
List.of(new TextResourceContents(uri, "application/json", jsonContent)));
}));
}
private static List<McpServerFeatures.SyncCompletionSpecification> createCompletionSpecs() {
return List.of(
// Completion for test_prompt_with_arguments
new McpServerFeatures.SyncCompletionSpecification(new PromptReference("test_prompt_with_arguments"),
(exchange, request) -> {
logger.info("Completion requested for prompt 'test_prompt_with_arguments', argument: {}",
request.argument().name());
// Return minimal completion with required fields
return new CompleteResult(new CompleteResult.CompleteCompletion(List.of(), 0, false));
}));
}
}

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
<logger name="io.modelcontextprotocol" level="DEBUG" />
</configuration>

View file

@ -113,6 +113,7 @@
<module>mcp-spring/mcp-spring-webflux</module>
<module>mcp-spring/mcp-spring-webmvc</module>
<module>mcp-test</module>
<module>conformance-tests</module>
</modules>
<build>