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;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
|
@ -28,11 +27,11 @@ import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvide
|
|||
import io.modelcontextprotocol.spec.McpError;
|
||||
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.BeforeEach;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.DisposableServer;
|
||||
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.reactive.function.client.WebClient;
|
||||
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.assertThatExceptionOfType;
|
||||
|
|
@ -331,6 +331,226 @@ class WebFluxSseIntegrationTests {
|
|||
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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -357,6 +357,219 @@ class WebMvcSseIntegrationTests {
|
|||
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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import io.modelcontextprotocol.spec.McpSchema;
|
|||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||
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.GetPromptResult;
|
||||
import io.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||
|
|
@ -141,6 +143,15 @@ public class McpAsyncClient {
|
|||
*/
|
||||
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.
|
||||
*/
|
||||
|
|
@ -189,6 +200,15 @@ public class McpAsyncClient {
|
|||
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
|
||||
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
|
||||
// --------------------------
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import io.modelcontextprotocol.spec.McpTransport;
|
|||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||
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.Root;
|
||||
import io.modelcontextprotocol.util.Assert;
|
||||
|
|
@ -175,6 +177,8 @@ public interface McpClient {
|
|||
|
||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||
|
||||
private Function<ElicitRequest, ElicitResult> elicitationHandler;
|
||||
|
||||
private SyncSpec(McpClientTransport transport) {
|
||||
Assert.notNull(transport, "Transport must not be null");
|
||||
this.transport = transport;
|
||||
|
|
@ -283,6 +287,21 @@ public interface McpClient {
|
|||
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
|
||||
* client to react to changes in the server's tool capabilities, such as tools
|
||||
|
|
@ -364,7 +383,7 @@ public interface McpClient {
|
|||
public McpSyncClient build() {
|
||||
McpClientFeatures.Sync syncFeatures = new McpClientFeatures.Sync(this.clientInfo, this.capabilities,
|
||||
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);
|
||||
|
||||
|
|
@ -414,6 +433,8 @@ public interface McpClient {
|
|||
|
||||
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
|
||||
|
||||
private Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler;
|
||||
|
||||
private AsyncSpec(McpClientTransport transport) {
|
||||
Assert.notNull(transport, "Transport must not be null");
|
||||
this.transport = transport;
|
||||
|
|
@ -522,6 +543,21 @@ public interface McpClient {
|
|||
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
|
||||
* 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,
|
||||
new McpClientFeatures.Async(this.clientInfo, this.capabilities, this.roots,
|
||||
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 loggingConsumers the logging consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
record Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||
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.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
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.
|
||||
|
|
@ -77,6 +79,7 @@ class McpClientFeatures {
|
|||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
public Async(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||
Map<String, McpSchema.Root> roots,
|
||||
|
|
@ -84,14 +87,16 @@ class McpClientFeatures {
|
|||
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers,
|
||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers,
|
||||
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");
|
||||
this.clientInfo = clientInfo;
|
||||
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||
: new McpSchema.ClientCapabilities(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.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||
|
|
@ -99,6 +104,7 @@ class McpClientFeatures {
|
|||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
this.samplingHandler = samplingHandler;
|
||||
this.elicitationHandler = elicitationHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -138,9 +144,14 @@ class McpClientFeatures {
|
|||
Function<McpSchema.CreateMessageRequest, Mono<McpSchema.CreateMessageResult>> samplingHandler = r -> Mono
|
||||
.fromCallable(() -> syncSpec.samplingHandler().apply(r))
|
||||
.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(),
|
||||
toolsChangeConsumers, resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers,
|
||||
samplingHandler);
|
||||
samplingHandler, elicitationHandler);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -156,13 +167,15 @@ class McpClientFeatures {
|
|||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
public record Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
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.
|
||||
|
|
@ -174,20 +187,23 @@ class McpClientFeatures {
|
|||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @param elicitationHandler the elicitation handler.
|
||||
*/
|
||||
public Sync(McpSchema.Implementation clientInfo, McpSchema.ClientCapabilities clientCapabilities,
|
||||
Map<String, McpSchema.Root> roots, List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers,
|
||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers,
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers,
|
||||
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");
|
||||
this.clientInfo = clientInfo;
|
||||
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||
: new McpSchema.ClientCapabilities(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.toolsChangeConsumers = toolsChangeConsumers != null ? toolsChangeConsumers : List.of();
|
||||
|
|
@ -195,6 +211,7 @@ class McpClientFeatures {
|
|||
this.promptsChangeConsumers = promptsChangeConsumers != null ? promptsChangeConsumers : List.of();
|
||||
this.loggingConsumers = loggingConsumers != null ? loggingConsumers : List.of();
|
||||
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.ElicitResult> ELICITATION_RESULT_TYPE_REF = new TypeReference<>() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new asynchronous exchange with the client.
|
||||
* @param session The server session representing a 1-1 interaction.
|
||||
|
|
@ -93,6 +96,31 @@ public class McpAsyncServerExchange {
|
|||
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.
|
||||
* @return A Mono that emits the list of roots result.
|
||||
|
|
|
|||
|
|
@ -64,6 +64,24 @@ public class McpSyncServerExchange {
|
|||
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.
|
||||
* @return The list of roots result.
|
||||
|
|
|
|||
|
|
@ -94,6 +94,9 @@ public final class McpSchema {
|
|||
// Sampling Methods
|
||||
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();
|
||||
|
||||
// ---------------------------
|
||||
|
|
@ -131,8 +134,8 @@ public final class McpSchema {
|
|||
|
||||
}
|
||||
|
||||
public sealed interface Request
|
||||
permits InitializeRequest, CallToolRequest, CreateMessageRequest, CompleteRequest, GetPromptRequest {
|
||||
public sealed interface Request permits InitializeRequest, CallToolRequest, CreateMessageRequest, ElicitRequest,
|
||||
CompleteRequest, GetPromptRequest {
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -221,7 +224,7 @@ public final class McpSchema {
|
|||
public record InitializeRequest( // @formatter:off
|
||||
@JsonProperty("protocolVersion") String protocolVersion,
|
||||
@JsonProperty("capabilities") ClientCapabilities capabilities,
|
||||
@JsonProperty("clientInfo") Implementation clientInfo) implements Request {
|
||||
@JsonProperty("clientInfo") Implementation clientInfo) implements Request {
|
||||
} // @formatter:on
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
|
|
@ -245,6 +248,8 @@ public final class McpSchema {
|
|||
* access to.
|
||||
* @param sampling Provides a standardized way for servers to request LLM sampling
|
||||
* (“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)
|
||||
|
|
@ -252,7 +257,8 @@ public final class McpSchema {
|
|||
public record ClientCapabilities( // @formatter:off
|
||||
@JsonProperty("experimental") Map<String, Object> experimental,
|
||||
@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,
|
||||
|
|
@ -264,7 +270,7 @@ public final class McpSchema {
|
|||
* has changed since the last time the server checked.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public record RootCapabilities(
|
||||
@JsonProperty("listChanged") Boolean listChanged) {
|
||||
}
|
||||
|
|
@ -279,10 +285,22 @@ public final class McpSchema {
|
|||
* image-based interactions and optionally include context
|
||||
* from MCP servers in their prompts.
|
||||
*/
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
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() {
|
||||
return new Builder();
|
||||
}
|
||||
|
|
@ -291,6 +309,7 @@ public final class McpSchema {
|
|||
private Map<String, Object> experimental;
|
||||
private RootCapabilities roots;
|
||||
private Sampling sampling;
|
||||
private Elicitation elicitation;
|
||||
|
||||
public Builder experimental(Map<String, Object> experimental) {
|
||||
this.experimental = experimental;
|
||||
|
|
@ -307,8 +326,13 @@ public final class McpSchema {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder elicitation() {
|
||||
this.elicitation = new Elicitation();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientCapabilities build() {
|
||||
return new ClientCapabilities(experimental, roots, sampling);
|
||||
return new ClientCapabilities(experimental, roots, sampling, elicitation);
|
||||
}
|
||||
}
|
||||
}// @formatter:on
|
||||
|
|
@ -326,11 +350,11 @@ public final class McpSchema {
|
|||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record CompletionCapabilities() {
|
||||
}
|
||||
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record LoggingCapabilities() {
|
||||
}
|
||||
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record PromptCapabilities(
|
||||
@JsonProperty("listChanged") Boolean listChanged) {
|
||||
|
|
@ -727,11 +751,11 @@ public final class McpSchema {
|
|||
@JsonProperty("name") String name,
|
||||
@JsonProperty("description") String description,
|
||||
@JsonProperty("inputSchema") JsonSchema inputSchema) {
|
||||
|
||||
|
||||
public Tool(String name, String description, String schema) {
|
||||
this(name, description, parseSchema(schema));
|
||||
}
|
||||
|
||||
|
||||
} // @formatter:on
|
||||
|
||||
private static JsonSchema parseSchema(String schema) {
|
||||
|
|
@ -758,7 +782,7 @@ public final class McpSchema {
|
|||
@JsonProperty("arguments") Map<String, Object> arguments) implements Request {
|
||||
|
||||
public CallToolRequest(String name, String jsonArguments) {
|
||||
this(name, parseJsonArguments(jsonArguments));
|
||||
this(name, parseJsonArguments(jsonArguments));
|
||||
}
|
||||
|
||||
private static Map<String, Object> parseJsonArguments(String jsonArguments) {
|
||||
|
|
@ -893,7 +917,7 @@ public final class McpSchema {
|
|||
@JsonProperty("costPriority") Double costPriority,
|
||||
@JsonProperty("speedPriority") Double speedPriority,
|
||||
@JsonProperty("intelligencePriority") Double intelligencePriority) {
|
||||
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
|
@ -963,7 +987,7 @@ public final class McpSchema {
|
|||
@JsonProperty("includeContext") ContextInclusionStrategy includeContext,
|
||||
@JsonProperty("temperature") Double temperature,
|
||||
@JsonProperty("maxTokens") int maxTokens,
|
||||
@JsonProperty("stopSequences") List<String> stopSequences,
|
||||
@JsonProperty("stopSequences") List<String> stopSequences,
|
||||
@JsonProperty("metadata") Map<String, Object> metadata) implements Request {
|
||||
|
||||
public enum ContextInclusionStrategy {
|
||||
|
|
@ -971,7 +995,7 @@ public final class McpSchema {
|
|||
@JsonProperty("thisServer") THIS_SERVER,
|
||||
@JsonProperty("allServers") ALL_SERVERS
|
||||
}
|
||||
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
|
@ -1040,7 +1064,7 @@ public final class McpSchema {
|
|||
@JsonProperty("content") Content content,
|
||||
@JsonProperty("model") String model,
|
||||
@JsonProperty("stopReason") StopReason stopReason) {
|
||||
|
||||
|
||||
public enum StopReason {
|
||||
@JsonProperty("endTurn") END_TURN,
|
||||
@JsonProperty("stopSequence") STOP_SEQUENCE,
|
||||
|
|
@ -1088,6 +1112,79 @@ public final class McpSchema {
|
|||
}
|
||||
}// @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
|
||||
// ---------------------------
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolRequest;
|
|||
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||
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.Prompt;
|
||||
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
|
||||
void testInitializeWithAllCapabilities() {
|
||||
var capabilities = ClientCapabilities.builder()
|
||||
|
|
@ -433,7 +449,11 @@ public abstract class AbstractMcpAsyncClientTests {
|
|||
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
|
||||
.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 ->
|
||||
|
||||
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.Root;
|
||||
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 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");
|
||||
}
|
||||
|
||||
@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.CreateMessageRequest;
|
||||
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.ModelPreferences;
|
||||
import io.modelcontextprotocol.spec.McpSchema.Role;
|
||||
|
|
@ -339,6 +341,217 @@ class HttpServletSseServerTransportProviderIntegrationTests {
|
|||
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
|
||||
// ---------------------------------------
|
||||
|
|
|
|||
|
|
@ -807,6 +807,40 @@ public class McpSchemaTests {
|
|||
{"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
|
||||
|
||||
@Test
|
||||
|
|
|
|||
Loading…
Reference in a new issue