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