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:
parent
0a8cb1e038
commit
4fe8f3efef
14 changed files with 1558 additions and 0 deletions
62
.github/workflows/conformance.yml
vendored
Normal file
62
.github/workflows/conformance.yml
vendored
Normal 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
1
.gitignore
vendored
|
|
@ -10,6 +10,7 @@ build/
|
|||
out
|
||||
/.gradletasknamecache
|
||||
**/*.flattened-pom.xml
|
||||
**/dependency-reduced-pom.xml
|
||||
|
||||
### IDE - Eclipse/STS ###
|
||||
.apt_generated
|
||||
|
|
|
|||
82
conformance-tests/VALIDATION_RESULTS.md
Normal file
82
conformance-tests/VALIDATION_RESULTS.md
Normal 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
|
||||
135
conformance-tests/client-jdk-http-client/README.md
Normal file
135
conformance-tests/client-jdk-http-client/README.md
Normal 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
|
||||
78
conformance-tests/client-jdk-http-client/pom.xml
Normal file
78
conformance-tests/client-jdk-http-client/pom.xml
Normal 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>
|
||||
|
|
@ -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 <server-url>
|
||||
*
|
||||
* @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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
17
conformance-tests/conformance-baseline.yml
Normal file
17
conformance-tests/conformance-baseline.yml
Normal 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
28
conformance-tests/pom.xml
Normal 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>
|
||||
205
conformance-tests/server-servlet/README.md
Normal file
205
conformance-tests/server-servlet/README.md
Normal 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)
|
||||
69
conformance-tests/server-servlet/pom.xml
Normal file
69
conformance-tests/server-servlet/pom.xml
Normal 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>
|
||||
|
|
@ -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));
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
1
pom.xml
1
pom.xml
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue