feat: Add elicitation support to MCP protocol
Implement elicitation capabilities allowing servers to request additional information from users through clients during interactions. This feature provides a standardized way for servers to gather necessary information dynamically while clients maintain control over user interactions and data sharing. - Add ElicitRequest and ElicitResult classes to McpSchema - Implement elicitation handlers in client classes - Add elicitation capabilities to server exchange classes - Add tests for elicitation functionality with various scenarios
This commit is contained in:
parent
07e7b8fd6b
commit
8a5a591d39
12 changed files with 1106 additions and 28 deletions
|
|
@ -4,7 +4,6 @@
|
||||||
package io.modelcontextprotocol;
|
package io.modelcontextprotocol;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
@ -28,11 +27,11 @@ import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvide
|
||||||
import io.modelcontextprotocol.spec.McpError;
|
import io.modelcontextprotocol.spec.McpError;
|
||||||
import io.modelcontextprotocol.spec.McpSchema;
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.*;
|
import io.modelcontextprotocol.spec.McpSchema.*;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities.CompletionCapabilities;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.netty.DisposableServer;
|
import reactor.netty.DisposableServer;
|
||||||
import reactor.netty.http.server.HttpServer;
|
import reactor.netty.http.server.HttpServer;
|
||||||
|
|
||||||
|
|
@ -41,6 +40,7 @@ import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
|
||||||
import org.springframework.web.client.RestClient;
|
import org.springframework.web.client.RestClient;
|
||||||
import org.springframework.web.reactive.function.client.WebClient;
|
import org.springframework.web.reactive.function.client.WebClient;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
|
@ -331,6 +331,226 @@ class WebFluxSseIntegrationTests {
|
||||||
mcpServer.closeGracefully().block();
|
mcpServer.closeGracefully().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Elicitation Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
|
void testCreateElicitationWithoutElicitationCapabilities(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
exchange.createElicitation(mock(ElicitRequest.class)).block();
|
||||||
|
|
||||||
|
return Mono.just(mock(CallToolResult.class));
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (
|
||||||
|
// Create client without elicitation capabilities
|
||||||
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
|
||||||
|
|
||||||
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}
|
||||||
|
catch (McpError e) {
|
||||||
|
assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be configured with elicitation capabilities");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
|
void testCreateElicitationSuccess(String clientType) {
|
||||||
|
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
|
void testCreateElicitationWithRequestTimeoutSuccess(String clientType) {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(3))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest(name = "{0} : {displayName} ")
|
||||||
|
@ValueSource(strings = { "httpclient", "webflux" })
|
||||||
|
void testCreateElicitationWithRequestTimeoutFail(String clientType) {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
var clientBuilder = clientBuilders.get(clientType);
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(1))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
|
||||||
|
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}).withMessageContaining("within 1000ms");
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Roots Tests
|
// Roots Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -357,6 +357,219 @@ class WebMvcSseIntegrationTests {
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Elicitation Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithoutElicitationCapabilities() {
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
exchange.createElicitation(mock(McpSchema.ElicitRequest.class)).block();
|
||||||
|
|
||||||
|
return Mono.just(mock(CallToolResult.class));
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (
|
||||||
|
// Create client without elicitation capabilities
|
||||||
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
|
||||||
|
|
||||||
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}
|
||||||
|
catch (McpError e) {
|
||||||
|
assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be configured with elicitation capabilities");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationSuccess() {
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
|
||||||
|
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
|
||||||
|
Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithRequestTimeoutSuccess() {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
|
||||||
|
Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(3))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithRequestTimeoutFail() {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new McpSchema.ElicitResult(McpSchema.ElicitResult.Action.ACCEPT,
|
||||||
|
Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(1))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
|
||||||
|
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}).withMessageContaining("Timeout");
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Roots Tests
|
// Roots Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import io.modelcontextprotocol.spec.McpSchema;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||||
|
|
@ -141,6 +143,15 @@ public class McpAsyncClient {
|
||||||
*/
|
*/
|
||||||
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MCP provides a standardized way for servers to request additional information from
|
||||||
|
* users through the client during interactions. This flow allows clients to maintain
|
||||||
|
* control over user interactions and data sharing while enabling servers to gather
|
||||||
|
* necessary information dynamically. Servers can request structured data from users
|
||||||
|
* with optional JSON schemas to validate responses.
|
||||||
|
*/
|
||||||
|
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Client transport implementation.
|
* Client transport implementation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -189,6 +200,15 @@ public class McpAsyncClient {
|
||||||
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elicitation Handler
|
||||||
|
if (this.clientCapabilities.elicitation() != null) {
|
||||||
|
if (features.elicitationHandler() == null) {
|
||||||
|
throw new McpError("Elicitation handler must not be null when client capabilities include elicitation");
|
||||||
|
}
|
||||||
|
this.elicitationHandler = features.elicitationHandler();
|
||||||
|
requestHandlers.put(McpSchema.METHOD_ELICITATION_CREATE, elicitationCreateHandler());
|
||||||
|
}
|
||||||
|
|
||||||
// Notification Handlers
|
// Notification Handlers
|
||||||
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||||
|
|
||||||
|
|
@ -500,6 +520,18 @@ public class McpAsyncClient {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --------------------------
|
||||||
|
// Elicitation
|
||||||
|
// --------------------------
|
||||||
|
private RequestHandler<ElicitResult> elicitationCreateHandler() {
|
||||||
|
return params -> {
|
||||||
|
ElicitRequest request = transport.unmarshalFrom(params, new TypeReference<>() {
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.elicitationHandler.apply(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Tools
|
// Tools
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ import io.modelcontextprotocol.spec.McpTransport;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Implementation;
|
import io.modelcontextprotocol.spec.McpSchema.Implementation;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import io.modelcontextprotocol.util.Assert;
|
import io.modelcontextprotocol.util.Assert;
|
||||||
|
|
@ -175,6 +177,8 @@ public interface McpClient {
|
||||||
|
|
||||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||||
|
|
||||||
|
private Function<ElicitRequest, ElicitResult> elicitationHandler;
|
||||||
|
|
||||||
private SyncSpec(McpClientTransport transport) {
|
private SyncSpec(McpClientTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -283,6 +287,21 @@ public interface McpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom elicitation handler for processing elicitation message requests.
|
||||||
|
* The elicitation handler can modify or validate messages before they are sent to
|
||||||
|
* the server, enabling custom processing logic.
|
||||||
|
* @param elicitationHandler A function that processes elicitation requests and
|
||||||
|
* returns results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
|
*/
|
||||||
|
public SyncSpec elicitation(Function<ElicitRequest, ElicitResult> elicitationHandler) {
|
||||||
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
|
this.elicitationHandler = elicitationHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a consumer to be notified when the available tools change. This allows the
|
* Adds a consumer to be notified when the available tools change. This allows the
|
||||||
* client to react to changes in the server's tool capabilities, such as tools
|
* client to react to changes in the server's tool capabilities, such as tools
|
||||||
|
|
@ -364,7 +383,7 @@ public interface McpClient {
|
||||||
public McpSyncClient build() {
|
public McpSyncClient build() {
|
||||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||||
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
this.roots, this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
||||||
this.loggingConsumers, this.samplingHandler);
|
this.loggingConsumers, this.samplingHandler, this.elicitationHandler);
|
||||||
|
|
||||||
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
McpClientFeatures.Async asyncFeatures = McpClientFeatures.Async.fromSync(syncFeatures);
|
||||||
|
|
||||||
|
|
@ -414,6 +433,8 @@ public interface McpClient {
|
||||||
|
|
||||||
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||||
|
|
||||||
|
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
||||||
|
|
||||||
private AsyncSpec(McpClientTransport transport) {
|
private AsyncSpec(McpClientTransport transport) {
|
||||||
Assert.notNull(transport, "Transport must not be null");
|
Assert.notNull(transport, "Transport must not be null");
|
||||||
this.transport = transport;
|
this.transport = transport;
|
||||||
|
|
@ -522,6 +543,21 @@ public interface McpClient {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets a custom elicitation handler for processing elicitation message requests.
|
||||||
|
* The elicitation handler can modify or validate messages before they are sent to
|
||||||
|
* the server, enabling custom processing logic.
|
||||||
|
* @param elicitationHandler A function that processes elicitation requests and
|
||||||
|
* returns results. Must not be null.
|
||||||
|
* @return This builder instance for method chaining
|
||||||
|
* @throws IllegalArgumentException if elicitationHandler is null
|
||||||
|
*/
|
||||||
|
public AsyncSpec elicitation(Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler) {
|
||||||
|
Assert.notNull(elicitationHandler, "Elicitation handler must not be null");
|
||||||
|
this.elicitationHandler = elicitationHandler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a consumer to be notified when the available tools change. This allows the
|
* Adds a consumer to be notified when the available tools change. This allows the
|
||||||
* client to react to changes in the server's tool capabilities, such as tools
|
* client to react to changes in the server's tool capabilities, such as tools
|
||||||
|
|
@ -606,7 +642,7 @@ public interface McpClient {
|
||||||
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
|
return new McpAsyncClient(this.transport, this.requestTimeout, this.initializationTimeout,
|
||||||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||||
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
this.toolsChangeConsumers, this.resourcesChangeConsumers, this.promptsChangeConsumers,
|
||||||
this.loggingConsumers, this.samplingHandler));
|
this.loggingConsumers, this.samplingHandler, this.elicitationHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,13 +60,15 @@ class McpClientFeatures {
|
||||||
* @param promptsChangeConsumers the prompts change consumers.
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
|
* @param elicitationHandler the elicitation handler.
|
||||||
*/
|
*/
|
||||||
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Function<List<McpSchema.Tool>, Mono<Void>>> toolsChangeConsumers,
|
||||||
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
||||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance and validate the arguments.
|
* Create an instance and validate the arguments.
|
||||||
|
|
@ -77,6 +79,7 @@ class McpClientFeatures {
|
||||||
* @param promptsChangeConsumers the prompts change consumers.
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
|
* @param elicitationHandler the elicitation handler.
|
||||||
*/
|
*/
|
||||||
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots,
|
Map<String, McpSchema.Root> roots,
|
||||||
|
|
@ -84,14 +87,16 @@ class McpClientFeatures {
|
||||||
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
||||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||||
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
List<Function<McpSchema.LoggingMessageNotification, Mono<Void>>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler) {
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler,
|
||||||
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||||
: new McpSchema.ClientCapabilities(null,
|
: new McpSchema.ClientCapabilities(null,
|
||||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||||
|
elicitationHandler != null ? new McpSchema.ClientCapabilities.Elicitation() : null);
|
||||||
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||||
|
|
||||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||||
|
|
@ -99,6 +104,7 @@ class McpClientFeatures {
|
||||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
|
this.elicitationHandler = elicitationHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -138,9 +144,14 @@ class McpClientFeatures {
|
||||||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
||||||
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
||||||
.subscribeOn(Schedulers.boundedElastic());
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
|
||||||
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = r -> Mono
|
||||||
|
.fromCallable(() -> syncSpec.elicitationHandler().apply(r))
|
||||||
|
.subscribeOn(Schedulers.boundedElastic());
|
||||||
|
|
||||||
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
return new Async(syncSpec.clientInfo(), syncSpec.clientCapabilities(), syncSpec.roots(),
|
||||||
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
|
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
|
||||||
samplingHandler);
|
samplingHandler, elicitationHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -156,13 +167,15 @@ class McpClientFeatures {
|
||||||
* @param promptsChangeConsumers the prompts change consumers.
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
|
* @param elicitationHandler the elicitation handler.
|
||||||
*/
|
*/
|
||||||
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance and validate the arguments.
|
* Create an instance and validate the arguments.
|
||||||
|
|
@ -174,20 +187,23 @@ class McpClientFeatures {
|
||||||
* @param promptsChangeConsumers the prompts change consumers.
|
* @param promptsChangeConsumers the prompts change consumers.
|
||||||
* @param loggingConsumers the logging consumers.
|
* @param loggingConsumers the logging consumers.
|
||||||
* @param samplingHandler the sampling handler.
|
* @param samplingHandler the sampling handler.
|
||||||
|
* @param elicitationHandler the elicitation handler.
|
||||||
*/
|
*/
|
||||||
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||||
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers,
|
||||||
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler) {
|
Function<McpSchema.CreateMessageRequest, McpSchema.CreateMessageResult> samplingHandler,
|
||||||
|
Function<McpSchema.ElicitRequest, McpSchema.ElicitResult> elicitationHandler) {
|
||||||
|
|
||||||
Assert.notNull(clientInfo, "Client info must not be null");
|
Assert.notNull(clientInfo, "Client info must not be null");
|
||||||
this.clientInfo = clientInfo;
|
this.clientInfo = clientInfo;
|
||||||
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||||
: new McpSchema.ClientCapabilities(null,
|
: new McpSchema.ClientCapabilities(null,
|
||||||
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
!Utils.isEmpty(roots) ? new McpSchema.ClientCapabilities.RootCapabilities(false) : null,
|
||||||
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null);
|
samplingHandler != null ? new McpSchema.ClientCapabilities.Sampling() : null,
|
||||||
|
elicitationHandler != null ? new McpSchema.ClientCapabilities.Elicitation() : null);
|
||||||
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
this.roots = roots != null ? new HashMap<>(roots) : new HashMap<>();
|
||||||
|
|
||||||
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
this.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||||
|
|
@ -195,6 +211,7 @@ class McpClientFeatures {
|
||||||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||||
this.samplingHandler = samplingHandler;
|
this.samplingHandler = samplingHandler;
|
||||||
|
this.elicitationHandler = elicitationHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ public class McpAsyncServerExchange {
|
||||||
private static final TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
|
private static final TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static final TypeReference<McpSchema.ElicitResult> ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new asynchronous exchange with the client.
|
* Create a new asynchronous exchange with the client.
|
||||||
* @param session The server session representing a 1-1 interaction.
|
* @param session The server session representing a 1-1 interaction.
|
||||||
|
|
@ -93,6 +96,31 @@ public class McpAsyncServerExchange {
|
||||||
CREATE_MESSAGE_RESULT_TYPE_REF);
|
CREATE_MESSAGE_RESULT_TYPE_REF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new elicitation. MCP provides a standardized way for servers to request
|
||||||
|
* additional information from users through the client during interactions. This flow
|
||||||
|
* allows clients to maintain control over user interactions and data sharing while
|
||||||
|
* enabling servers to gather necessary information dynamically. Servers can request
|
||||||
|
* structured data from users with optional JSON schemas to validate responses.
|
||||||
|
* @param elicitRequest The request to create a new elicitation
|
||||||
|
* @return A Mono that completes when the elicitation has been resolved.
|
||||||
|
* @see McpSchema.ElicitRequest
|
||||||
|
* @see McpSchema.ElicitResult
|
||||||
|
* @see <a href=
|
||||||
|
* "https://spec.modelcontextprotocol.io/specification/client/elicitation/">Elicitation
|
||||||
|
* Specification</a>
|
||||||
|
*/
|
||||||
|
public Mono<McpSchema.ElicitResult> createElicitation(McpSchema.ElicitRequest elicitRequest) {
|
||||||
|
if (this.clientCapabilities == null) {
|
||||||
|
return Mono.error(new McpError("Client must be initialized. Call the initialize method first!"));
|
||||||
|
}
|
||||||
|
if (this.clientCapabilities.elicitation() == null) {
|
||||||
|
return Mono.error(new McpError("Client must be configured with elicitation capabilities"));
|
||||||
|
}
|
||||||
|
return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
|
||||||
|
ELICITATION_RESULT_TYPE_REF);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the list of all roots provided by the client.
|
* Retrieves the list of all roots provided by the client.
|
||||||
* @return A Mono that emits the list of roots result.
|
* @return A Mono that emits the list of roots result.
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,24 @@ public class McpSyncServerExchange {
|
||||||
return this.exchange.createMessage(createMessageRequest).block();
|
return this.exchange.createMessage(createMessageRequest).block();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new elicitation. MCP provides a standardized way for servers to request
|
||||||
|
* additional information from users through the client during interactions. This flow
|
||||||
|
* allows clients to maintain control over user interactions and data sharing while
|
||||||
|
* enabling servers to gather necessary information dynamically. Servers can request
|
||||||
|
* structured data from users with optional JSON schemas to validate responses.
|
||||||
|
* @param elicitRequest The request to create a new elicitation
|
||||||
|
* @return A result containing the elicitation response.
|
||||||
|
* @see McpSchema.ElicitRequest
|
||||||
|
* @see McpSchema.ElicitResult
|
||||||
|
* @see <a href=
|
||||||
|
* "https://spec.modelcontextprotocol.io/specification/client/elicitation/">Elicitation
|
||||||
|
* Specification</a>
|
||||||
|
*/
|
||||||
|
public McpSchema.ElicitResult createElicitation(McpSchema.ElicitRequest elicitRequest) {
|
||||||
|
return this.exchange.createElicitation(elicitRequest).block();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the list of all roots provided by the client.
|
* Retrieves the list of all roots provided by the client.
|
||||||
* @return The list of roots result.
|
* @return The list of roots result.
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,9 @@ public final class McpSchema {
|
||||||
// Sampling Methods
|
// Sampling Methods
|
||||||
public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage";
|
public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage";
|
||||||
|
|
||||||
|
// Elicitation Methods
|
||||||
|
public static final String METHOD_ELICITATION_CREATE = "elicitation/create";
|
||||||
|
|
||||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
@ -131,8 +134,8 @@ public final class McpSchema {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed interface Request
|
public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest,
|
||||||
permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest {
|
CompleteRequest, GetPromptRequest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,6 +248,8 @@ public final class McpSchema {
|
||||||
* access to.
|
* access to.
|
||||||
* @param sampling Provides a standardized way for servers to request LLM sampling
|
* @param sampling Provides a standardized way for servers to request LLM sampling
|
||||||
* (“completions” or “generations”) from language models via clients.
|
* (“completions” or “generations”) from language models via clients.
|
||||||
|
* @param elicitation Provides a standardized way for servers to request additional
|
||||||
|
* information from users through the client during interactions.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
|
@ -252,7 +257,8 @@ public final class McpSchema {
|
||||||
public record ClientCapabilities( // @formatter:off
|
public record ClientCapabilities( // @formatter:off
|
||||||
@JsonProperty("experimental") Map<String, Object> experimental,
|
@JsonProperty("experimental") Map<String, Object> experimental,
|
||||||
@JsonProperty("roots") RootCapabilities roots,
|
@JsonProperty("roots") RootCapabilities roots,
|
||||||
@JsonProperty("sampling") Sampling sampling) {
|
@JsonProperty("sampling") Sampling sampling,
|
||||||
|
@JsonProperty("elicitation") Elicitation elicitation) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roots define the boundaries of where servers can operate within the filesystem,
|
* Roots define the boundaries of where servers can operate within the filesystem,
|
||||||
|
|
@ -283,6 +289,18 @@ public final class McpSchema {
|
||||||
public record Sampling() {
|
public record Sampling() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a standardized way for servers to request additional
|
||||||
|
* information from users through the client during interactions.
|
||||||
|
* This flow allows clients to maintain control over user
|
||||||
|
* interactions and data sharing while enabling servers to gather
|
||||||
|
* necessary information dynamically. Servers can request structured
|
||||||
|
* data from users with optional JSON schemas to validate responses.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
public record Elicitation() {
|
||||||
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
@ -291,6 +309,7 @@ public final class McpSchema {
|
||||||
private Map<String, Object> experimental;
|
private Map<String, Object> experimental;
|
||||||
private RootCapabilities roots;
|
private RootCapabilities roots;
|
||||||
private Sampling sampling;
|
private Sampling sampling;
|
||||||
|
private Elicitation elicitation;
|
||||||
|
|
||||||
public Builder experimental(Map<String, Object> experimental) {
|
public Builder experimental(Map<String, Object> experimental) {
|
||||||
this.experimental = experimental;
|
this.experimental = experimental;
|
||||||
|
|
@ -307,8 +326,13 @@ public final class McpSchema {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder elicitation() {
|
||||||
|
this.elicitation = new Elicitation();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public ClientCapabilities build() {
|
public ClientCapabilities build() {
|
||||||
return new ClientCapabilities(experimental, roots, sampling);
|
return new ClientCapabilities(experimental, roots, sampling, elicitation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}// @formatter:on
|
}// @formatter:on
|
||||||
|
|
@ -1088,6 +1112,79 @@ public final class McpSchema {
|
||||||
}
|
}
|
||||||
}// @formatter:on
|
}// @formatter:on
|
||||||
|
|
||||||
|
// Elicitation
|
||||||
|
/**
|
||||||
|
* Used by the server to send an elicitation to the client.
|
||||||
|
*
|
||||||
|
* @param message The body of the elicitation message.
|
||||||
|
* @param requestedSchema The elicitation response schema that must be satisfied.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ElicitRequest(// @formatter:off
|
||||||
|
@JsonProperty("message") String message,
|
||||||
|
@JsonProperty("requestedSchema") Map<String, Object> requestedSchema) implements Request {
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private String message;
|
||||||
|
private Map<String, Object> requestedSchema;
|
||||||
|
|
||||||
|
public Builder message(String message) {
|
||||||
|
this.message = message;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder requestedSchema(Map<String, Object> requestedSchema) {
|
||||||
|
this.requestedSchema = requestedSchema;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElicitRequest build() {
|
||||||
|
return new ElicitRequest(message, requestedSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}// @formatter:on
|
||||||
|
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
public record ElicitResult(// @formatter:off
|
||||||
|
@JsonProperty("action") Action action,
|
||||||
|
@JsonProperty("content") Map<String, Object> content) {
|
||||||
|
|
||||||
|
public enum Action {
|
||||||
|
@JsonProperty("accept") ACCEPT,
|
||||||
|
@JsonProperty("decline") DECLINE,
|
||||||
|
@JsonProperty("cancel") CANCEL
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
private Action action;
|
||||||
|
private Map<String, Object> content;
|
||||||
|
|
||||||
|
public Builder message(Action action) {
|
||||||
|
this.action = action;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder content(Map<String, Object> content) {
|
||||||
|
this.content = content;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ElicitResult build() {
|
||||||
|
return new ElicitResult(action, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}// @formatter:on
|
||||||
|
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
// Pagination Interfaces
|
// Pagination Interfaces
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
import io.modelcontextprotocol.spec.McpSchema.Prompt;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
import io.modelcontextprotocol.spec.McpSchema.Resource;
|
||||||
|
|
@ -422,6 +424,20 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInitializeWithElicitationCapability() {
|
||||||
|
ClientCapabilities capabilities = ClientCapabilities.builder().elicitation().build();
|
||||||
|
ElicitResult elicitResult = ElicitResult.builder()
|
||||||
|
.message(ElicitResult.Action.ACCEPT)
|
||||||
|
.content(Map.of("foo", "bar"))
|
||||||
|
.build();
|
||||||
|
withClient(createMcpTransport(),
|
||||||
|
builder -> builder.capabilities(capabilities).elicitation(request -> Mono.just(elicitResult)),
|
||||||
|
client -> {
|
||||||
|
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testInitializeWithAllCapabilities() {
|
void testInitializeWithAllCapabilities() {
|
||||||
var capabilities = ClientCapabilities.builder()
|
var capabilities = ClientCapabilities.builder()
|
||||||
|
|
@ -433,7 +449,11 @@ public abstract class AbstractMcpAsyncClientTests {
|
||||||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||||
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
.just(CreateMessageResult.builder().message("test").model("test-model").build());
|
||||||
|
|
||||||
withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler),
|
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler = request -> Mono
|
||||||
|
.just(ElicitResult.builder().message(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
|
||||||
|
|
||||||
|
withClient(createMcpTransport(),
|
||||||
|
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler),
|
||||||
client ->
|
client ->
|
||||||
|
|
||||||
StepVerifier.create(client.initialize()).assertNext(result -> {
|
StepVerifier.create(client.initialize()).assertNext(result -> {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Root;
|
import io.modelcontextprotocol.spec.McpSchema.Root;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.EnumSource;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import static io.modelcontextprotocol.spec.McpSchema.METHOD_INITIALIZE;
|
import static io.modelcontextprotocol.spec.McpSchema.METHOD_INITIALIZE;
|
||||||
|
|
@ -349,4 +351,152 @@ class McpAsyncClientResponseHandlerTests {
|
||||||
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
|
.hasMessage("Sampling handler must not be null when client capabilities include sampling");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void testElicitationCreateRequestHandling() {
|
||||||
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
||||||
|
// Create a test elicitation handler that echoes back the input
|
||||||
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isInstanceOf(Map.class);
|
||||||
|
assertThat(request.requestedSchema().get("type")).isEqualTo("object");
|
||||||
|
|
||||||
|
var properties = request.requestedSchema().get("properties");
|
||||||
|
assertThat(properties).isNotNull();
|
||||||
|
assertThat(((Map<String, Object>) properties).get("message")).isInstanceOf(Map.class);
|
||||||
|
|
||||||
|
return Mono.just(McpSchema.ElicitResult.builder()
|
||||||
|
.message(McpSchema.ElicitResult.Action.ACCEPT)
|
||||||
|
.content(Map.of("message", request.message()))
|
||||||
|
.build());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create client with elicitation capability and handler
|
||||||
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(asyncMcpClient.initialize().block()).isNotNull();
|
||||||
|
|
||||||
|
// Create a mock elicitation
|
||||||
|
var elicitRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Simulate incoming request
|
||||||
|
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
|
||||||
|
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
|
||||||
|
transport.simulateIncomingMessage(request);
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
|
||||||
|
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
|
||||||
|
|
||||||
|
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
|
||||||
|
assertThat(response.id()).isEqualTo("test-id");
|
||||||
|
assertThat(response.error()).isNull();
|
||||||
|
|
||||||
|
McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() {
|
||||||
|
});
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content()).isEqualTo(Map.of("message", "Test message"));
|
||||||
|
|
||||||
|
asyncMcpClient.closeGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@EnumSource(value = McpSchema.ElicitResult.Action.class, names = { "DECLINE", "CANCEL" })
|
||||||
|
void testElicitationFailRequestHandling(McpSchema.ElicitResult.Action action) {
|
||||||
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
||||||
|
// Create a test elicitation handler to decline the request
|
||||||
|
Function<McpSchema.ElicitRequest, Mono<McpSchema.ElicitResult>> elicitationHandler = request -> Mono
|
||||||
|
.just(McpSchema.ElicitResult.builder().message(action).build());
|
||||||
|
|
||||||
|
// Create client with elicitation capability and handler
|
||||||
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(asyncMcpClient.initialize().block()).isNotNull();
|
||||||
|
|
||||||
|
// Create a mock elicitation
|
||||||
|
var elicitRequest = McpSchema.ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Simulate incoming request
|
||||||
|
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
|
||||||
|
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
|
||||||
|
transport.simulateIncomingMessage(request);
|
||||||
|
|
||||||
|
// Verify response
|
||||||
|
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
|
||||||
|
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
|
||||||
|
|
||||||
|
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
|
||||||
|
assertThat(response.id()).isEqualTo("test-id");
|
||||||
|
assertThat(response.error()).isNull();
|
||||||
|
|
||||||
|
McpSchema.ElicitResult result = transport.unmarshalFrom(response.result(), new TypeReference<>() {
|
||||||
|
});
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(action);
|
||||||
|
assertThat(result.content()).isNull();
|
||||||
|
|
||||||
|
asyncMcpClient.closeGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCreateRequestHandlingWithoutCapability() {
|
||||||
|
MockMcpClientTransport transport = initializationEnabledTransport();
|
||||||
|
|
||||||
|
// Create client without elicitation capability
|
||||||
|
McpAsyncClient asyncMcpClient = McpClient.async(transport)
|
||||||
|
.capabilities(ClientCapabilities.builder().build()) // No elicitation
|
||||||
|
// capability
|
||||||
|
.build();
|
||||||
|
|
||||||
|
assertThat(asyncMcpClient.initialize().block()).isNotNull();
|
||||||
|
|
||||||
|
// Create a mock elicitation
|
||||||
|
var elicitRequest = new McpSchema.ElicitRequest("test",
|
||||||
|
Map.of("type", "object", "properties", Map.of("test", Map.of("type", "boolean", "defaultValue", true,
|
||||||
|
"description", "test-description", "title", "test-title"))));
|
||||||
|
|
||||||
|
// Simulate incoming request
|
||||||
|
McpSchema.JSONRPCRequest request = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION,
|
||||||
|
McpSchema.METHOD_ELICITATION_CREATE, "test-id", elicitRequest);
|
||||||
|
transport.simulateIncomingMessage(request);
|
||||||
|
|
||||||
|
// Verify error response
|
||||||
|
McpSchema.JSONRPCMessage sentMessage = transport.getLastSentMessage();
|
||||||
|
assertThat(sentMessage).isInstanceOf(McpSchema.JSONRPCResponse.class);
|
||||||
|
|
||||||
|
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sentMessage;
|
||||||
|
assertThat(response.id()).isEqualTo("test-id");
|
||||||
|
assertThat(response.result()).isNull();
|
||||||
|
assertThat(response.error()).isNotNull();
|
||||||
|
assertThat(response.error().message()).contains("Method not found: elicitation/create");
|
||||||
|
|
||||||
|
asyncMcpClient.closeGracefully();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testElicitationCreateRequestHandlingWithNullHandler() {
|
||||||
|
MockMcpClientTransport transport = new MockMcpClientTransport();
|
||||||
|
|
||||||
|
// Create client with elicitation capability but null handler
|
||||||
|
assertThatThrownBy(() -> McpClient.async(transport)
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.build()).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Elicitation handler must not be null when client capabilities include elicitation");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
|
import io.modelcontextprotocol.spec.McpSchema.ModelPreferences;
|
||||||
import io.modelcontextprotocol.spec.McpSchema.Role;
|
import io.modelcontextprotocol.spec.McpSchema.Role;
|
||||||
|
|
@ -339,6 +341,217 @@ class HttpServletSseServerTransportProviderIntegrationTests {
|
||||||
mcpServer.close();
|
mcpServer.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------
|
||||||
|
// Elicitation Tests
|
||||||
|
// ---------------------------------------
|
||||||
|
@Test
|
||||||
|
@Disabled
|
||||||
|
void testCreateElicitationWithoutElicitationCapabilities() {
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
exchange.createElicitation(mock(ElicitRequest.class)).block();
|
||||||
|
|
||||||
|
return Mono.just(mock(CallToolResult.class));
|
||||||
|
});
|
||||||
|
|
||||||
|
var server = McpServer.async(mcpServerTransportProvider).serverInfo("test-server", "1.0.0").tools(tool).build();
|
||||||
|
|
||||||
|
try (
|
||||||
|
// Create client without elicitation capabilities
|
||||||
|
var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build()) {
|
||||||
|
|
||||||
|
assertThat(client.initialize()).isNotNull();
|
||||||
|
|
||||||
|
try {
|
||||||
|
client.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}
|
||||||
|
catch (McpError e) {
|
||||||
|
assertThat(e).isInstanceOf(McpError.class)
|
||||||
|
.hasMessage("Client must be configured with elicitation capabilities");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
server.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationSuccess() {
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
try (var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build()) {
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
}
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithRequestTimeoutSuccess() {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(3))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
|
||||||
|
assertThat(response).isNotNull();
|
||||||
|
assertThat(response).isEqualTo(callResponse);
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationWithRequestTimeoutFail() {
|
||||||
|
|
||||||
|
// Client
|
||||||
|
|
||||||
|
Function<ElicitRequest, ElicitResult> elicitationHandler = request -> {
|
||||||
|
assertThat(request.message()).isNotEmpty();
|
||||||
|
assertThat(request.requestedSchema()).isNotNull();
|
||||||
|
try {
|
||||||
|
TimeUnit.SECONDS.sleep(2);
|
||||||
|
}
|
||||||
|
catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return new ElicitResult(ElicitResult.Action.ACCEPT, Map.of("message", request.message()));
|
||||||
|
};
|
||||||
|
|
||||||
|
var mcpClient = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
|
||||||
|
.capabilities(ClientCapabilities.builder().elicitation().build())
|
||||||
|
.elicitation(elicitationHandler)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Server
|
||||||
|
|
||||||
|
CallToolResult callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")),
|
||||||
|
null);
|
||||||
|
|
||||||
|
McpServerFeatures.AsyncToolSpecification tool = new McpServerFeatures.AsyncToolSpecification(
|
||||||
|
new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), (exchange, request) -> {
|
||||||
|
|
||||||
|
var elicitationRequest = ElicitRequest.builder()
|
||||||
|
.message("Test message")
|
||||||
|
.requestedSchema(
|
||||||
|
Map.of("type", "object", "properties", Map.of("message", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
StepVerifier.create(exchange.createElicitation(elicitationRequest)).consumeNextWith(result -> {
|
||||||
|
assertThat(result).isNotNull();
|
||||||
|
assertThat(result.action()).isEqualTo(ElicitResult.Action.ACCEPT);
|
||||||
|
assertThat(result.content().get("message")).isEqualTo("Test message");
|
||||||
|
}).verifyComplete();
|
||||||
|
|
||||||
|
return Mono.just(callResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
var mcpServer = McpServer.async(mcpServerTransportProvider)
|
||||||
|
.serverInfo("test-server", "1.0.0")
|
||||||
|
.requestTimeout(Duration.ofSeconds(1))
|
||||||
|
.tools(tool)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
InitializeResult initResult = mcpClient.initialize();
|
||||||
|
assertThat(initResult).isNotNull();
|
||||||
|
|
||||||
|
assertThatExceptionOfType(McpError.class).isThrownBy(() -> {
|
||||||
|
mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
|
||||||
|
}).withMessageContaining("Timeout");
|
||||||
|
|
||||||
|
mcpClient.closeGracefully();
|
||||||
|
mcpServer.closeGracefully().block();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
// Roots Tests
|
// Roots Tests
|
||||||
// ---------------------------------------
|
// ---------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -807,6 +807,40 @@ public class McpSchemaTests {
|
||||||
{"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"endTurn"}"""));
|
{"role":"assistant","content":{"type":"text","text":"Assistant response"},"model":"gpt-4","stopReason":"endTurn"}"""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Elicitation Tests
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationRequest() throws Exception {
|
||||||
|
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder()
|
||||||
|
.requestedSchema(Map.of("type", "object", "required", List.of("a"), "properties",
|
||||||
|
Map.of("foo", Map.of("type", "string"))))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String value = mapper.writeValueAsString(request);
|
||||||
|
|
||||||
|
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isObject()
|
||||||
|
.isEqualTo(json("""
|
||||||
|
{"requestedSchema":{"properties":{"foo":{"type":"string"}},"required":["a"],"type":"object"}}"""));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testCreateElicitationResult() throws Exception {
|
||||||
|
McpSchema.ElicitResult result = McpSchema.ElicitResult.builder()
|
||||||
|
.content(Map.of("foo", "bar"))
|
||||||
|
.message(McpSchema.ElicitResult.Action.ACCEPT)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
String value = mapper.writeValueAsString(result);
|
||||||
|
|
||||||
|
assertThatJson(value).when(Option.IGNORING_ARRAY_ORDER)
|
||||||
|
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
|
||||||
|
.isObject()
|
||||||
|
.isEqualTo(json("""
|
||||||
|
{"action":"accept","content":{"foo":"bar"}}"""));
|
||||||
|
}
|
||||||
|
|
||||||
// Roots Tests
|
// Roots Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue