Remove redundand code
Resolves #4 Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
parent
413d138d0c
commit
f91fe15ec6
6 changed files with 7 additions and 1533 deletions
|
|
@ -1,283 +0,0 @@
|
|||
package org.springframework.ai.mcp.server.transport;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.Timeout;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.Disposable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.JSONRPCRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerSentEvent;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for the {@link SseServerTransport} class.
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
@Timeout(15)
|
||||
class SseServerTransportTests {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SseServerTransportTests.class);
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private String messageEndpoint;
|
||||
|
||||
private SseServerTransport transport;
|
||||
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
objectMapper = new ObjectMapper();
|
||||
messageEndpoint = "/message";
|
||||
transport = new SseServerTransport(objectMapper, messageEndpoint);
|
||||
webTestClient = WebTestClient.bindToRouterFunction(transport.getRouterFunction()).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructorValidation() {
|
||||
assertThatThrownBy(() -> new SseServerTransport(null, "/message")).isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("ObjectMapper must not be null");
|
||||
|
||||
assertThatThrownBy(() -> new SseServerTransport(new ObjectMapper(), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Message endpoint must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSseConnectionEstablishment() {
|
||||
List<ServerSentEvent<String>> events = new ArrayList<>();
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/sse")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectHeader()
|
||||
.contentTypeCompatibleWith(MediaType.TEXT_EVENT_STREAM)
|
||||
.returnResult(String.class)
|
||||
.getResponseBody()
|
||||
.map(data -> ServerSentEvent.<String>builder().data(data).build())
|
||||
.take(1) // Take only the initial endpoint event
|
||||
.subscribe(events::add);
|
||||
|
||||
// Wait a bit for the event to be received
|
||||
StepVerifier.create(Mono.delay(Duration.ofMillis(500))).expectNextCount(1).verifyComplete();
|
||||
|
||||
assertThat(events).hasSize(1);
|
||||
assertThat(events.get(0).data()).isEqualTo(messageEndpoint);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMessageHandling() {
|
||||
AtomicReference<McpSchema.JSONRPCMessage> receivedMessage = new AtomicReference<>();
|
||||
|
||||
// Set up message handler
|
||||
transport.connect(message -> {
|
||||
message.doOnNext(receivedMessage::set).subscribe();
|
||||
return Mono.empty();
|
||||
}).block();
|
||||
|
||||
// Create a test message
|
||||
JSONRPCRequest testMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "test-method", "test-id",
|
||||
Map.of("key", "value"));
|
||||
|
||||
// Send message to endpoint
|
||||
webTestClient.post()
|
||||
.uri(messageEndpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(testMessage)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk();
|
||||
|
||||
// Verify the message was received and processed
|
||||
assertThat(receivedMessage.get()).isNotNull();
|
||||
McpSchema.JSONRPCRequest receivedRequest = (McpSchema.JSONRPCRequest) receivedMessage.get();
|
||||
assertThat(receivedRequest.id()).isEqualTo(testMessage.id());
|
||||
assertThat(receivedRequest.method()).isEqualTo(testMessage.method());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("Flaky test")
|
||||
void testBroadcastMessage() {
|
||||
// Create test clients
|
||||
int clientCount = 3;
|
||||
CountDownLatch connectLatch = new CountDownLatch(clientCount);
|
||||
CountDownLatch messageLatch = new CountDownLatch(clientCount);
|
||||
|
||||
List<List<ServerSentEvent<String>>> allReceivedEvents = new ArrayList<>();
|
||||
List<Disposable> subscriptions = new ArrayList<>();
|
||||
|
||||
// Connect clients
|
||||
for (int i = 0; i < clientCount; i++) {
|
||||
List<ServerSentEvent<String>> clientEvents = new ArrayList<>();
|
||||
allReceivedEvents.add(clientEvents);
|
||||
|
||||
Flux<ServerSentEvent<String>> eventStream = webTestClient.get()
|
||||
.uri("/sse")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.returnResult(String.class)
|
||||
.getResponseBody()
|
||||
.map(data -> ServerSentEvent.<String>builder().data(data).build());
|
||||
|
||||
Disposable subscription = eventStream.doOnNext(event -> {
|
||||
clientEvents.add(event);
|
||||
if (event.event() != null && event.event().equals(SseServerTransport.ENDPOINT_EVENT_TYPE)) {
|
||||
connectLatch.countDown();
|
||||
}
|
||||
else if (event.event() != null && event.event().equals(SseServerTransport.MESSAGE_EVENT_TYPE)) {
|
||||
messageLatch.countDown();
|
||||
}
|
||||
}).subscribe();
|
||||
|
||||
subscriptions.add(subscription);
|
||||
}
|
||||
|
||||
// Wait for all clients to connect
|
||||
try {
|
||||
assertThat(connectLatch.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Verify initial connections
|
||||
for (List<ServerSentEvent<String>> events : allReceivedEvents) {
|
||||
assertThat(events).hasSize(1);
|
||||
assertThat(events.get(0).data()).isEqualTo(messageEndpoint);
|
||||
}
|
||||
|
||||
// Give clients time to fully establish their subscriptions
|
||||
logger.debug("Waiting for subscriptions to stabilize...");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
logger.debug("Sending broadcast message to {} clients", clientCount);
|
||||
|
||||
// Send broadcast message
|
||||
JSONRPCRequest broadcastMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "broadcast", "broadcast-id",
|
||||
Map.of("message", "Hello all!"));
|
||||
|
||||
// Send the message
|
||||
transport.sendMessage(broadcastMessage).block(Duration.ofSeconds(5));
|
||||
|
||||
// Wait for all clients to receive the broadcast
|
||||
try {
|
||||
assertThat(messageLatch.await(5, TimeUnit.SECONDS)).isTrue();
|
||||
}
|
||||
catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// Verify each client received both messages
|
||||
for (List<ServerSentEvent<String>> events : allReceivedEvents) {
|
||||
assertThat(events).hasSize(2);
|
||||
assertThat(events.get(0).data()).isEqualTo(messageEndpoint);
|
||||
assertThat(events.get(1).data()).contains("broadcast-id");
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
subscriptions.forEach(Disposable::dispose);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGracefulShutdown() {
|
||||
// Connect a client
|
||||
Flux<ServerSentEvent<String>> eventStream = webTestClient.get()
|
||||
.uri("/sse")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.returnResult(String.class)
|
||||
.getResponseBody()
|
||||
.map(data -> ServerSentEvent.<String>builder().data(data).build());
|
||||
|
||||
List<ServerSentEvent<String>> receivedEvents = new ArrayList<>();
|
||||
eventStream.subscribe(receivedEvents::add);
|
||||
|
||||
// Wait for connection
|
||||
StepVerifier.create(Mono.delay(Duration.ofMillis(500))).expectNextCount(1).verifyComplete();
|
||||
|
||||
// Initiate shutdown
|
||||
transport.closeGracefully().block(Duration.ofSeconds(5));
|
||||
|
||||
// Verify server rejects new connections with timeout
|
||||
webTestClient.get()
|
||||
.uri("/sse")
|
||||
.accept(MediaType.TEXT_EVENT_STREAM)
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Server is shutting down");
|
||||
|
||||
// Verify server rejects new messages with timeout
|
||||
webTestClient.post()
|
||||
.uri(messageEndpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("""
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "test",
|
||||
"id": "1",
|
||||
"params": {}
|
||||
}
|
||||
""")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isEqualTo(HttpStatus.SERVICE_UNAVAILABLE)
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("Server is shutting down");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvalidMessageHandling() {
|
||||
// Test invalid JSON
|
||||
webTestClient.post()
|
||||
.uri(messageEndpoint)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue("invalid json")
|
||||
.exchange()
|
||||
.expectStatus()
|
||||
.isBadRequest();
|
||||
|
||||
// Test invalid message format
|
||||
webTestClient.post().uri(messageEndpoint).contentType(MediaType.APPLICATION_JSON).bodyValue("""
|
||||
{
|
||||
"invalid": "message"
|
||||
}
|
||||
""").exchange().expectStatus().isBadRequest();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,24 +15,21 @@ import java.util.function.Function;
|
|||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.modelcontextprotocol.spec.ClientMcpTransport;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession;
|
||||
import org.modelcontextprotocol.spec.McpError;
|
||||
import org.modelcontextprotocol.spec.McpSchema;
|
||||
import org.modelcontextprotocol.spec.McpTransport;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession.NotificationHandler;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession.RequestHandler;
|
||||
import org.modelcontextprotocol.spec.McpError;
|
||||
import org.modelcontextprotocol.spec.McpSchema;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
import org.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
|
||||
import org.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
|
||||
import org.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
|
||||
import org.modelcontextprotocol.spec.McpSchema.GetPromptResult;
|
||||
import org.modelcontextprotocol.spec.McpSchema.Implementation;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ListPromptsResult;
|
||||
import org.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import org.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import org.modelcontextprotocol.spec.McpSchema.PaginatedRequest;
|
||||
import org.modelcontextprotocol.spec.McpSchema.Root;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ClientCapabilities.RootCapabilities;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ClientCapabilities.Sampling;
|
||||
import org.modelcontextprotocol.spec.McpTransport;
|
||||
import org.modelcontextprotocol.util.Assert;
|
||||
import org.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
|
|
@ -221,107 +218,6 @@ public class McpAsyncClient {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new McpAsyncClient with the given transport and session request-response
|
||||
* timeout.
|
||||
* @param transport the transport to use.
|
||||
* @param requestTimeout the session request-response timeout.
|
||||
* @param clientInfo the client implementation information.
|
||||
* @param clientCapabilities the client capabilities.
|
||||
* @param roots the roots.
|
||||
* @param toolsChangeConsumers the tools change consumers.
|
||||
* @param resourcesChangeConsumers the resources change consumers.
|
||||
* @param promptsChangeConsumers the prompts change consumers.
|
||||
* @param loggingConsumers the logging consumers.
|
||||
* @param samplingHandler the sampling handler.
|
||||
* @deprecated Use {@link McpClient#async(ClientMcpTransport)} to obtain an instance.
|
||||
*/
|
||||
@Deprecated
|
||||
public McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Implementation clientInfo,
|
||||
ClientCapabilities clientCapabilities, Map<String, 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<CreateMessageRequest, CreateMessageResult> samplingHandler) {
|
||||
|
||||
Assert.notNull(transport, "Transport must not be null");
|
||||
Assert.notNull(requestTimeout, "Request timeout must not be null");
|
||||
Assert.notNull(clientInfo, "Client info must not be null");
|
||||
|
||||
this.protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
|
||||
this.clientInfo = clientInfo;
|
||||
|
||||
this.clientCapabilities = (clientCapabilities != null) ? clientCapabilities
|
||||
: new McpSchema.ClientCapabilities(null, !Utils.isEmpty(roots) ? new RootCapabilities(false) : null,
|
||||
samplingHandler != null ? new Sampling() : null);
|
||||
|
||||
this.transport = transport;
|
||||
|
||||
this.roots = roots != null ? new ConcurrentHashMap<>(roots) : new ConcurrentHashMap<>();
|
||||
|
||||
// Request Handlers
|
||||
Map<String, RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||
|
||||
// Roots List Request Handler
|
||||
if (this.clientCapabilities.roots() != null) {
|
||||
requestHandlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
|
||||
}
|
||||
|
||||
// Sampling Handler
|
||||
if (this.clientCapabilities.sampling() != null) {
|
||||
if (samplingHandler == null) {
|
||||
throw new McpError("Sampling handler must not be null when client capabilities include sampling");
|
||||
}
|
||||
this.samplingHandler = r -> Mono.fromCallable(() -> samplingHandler.apply(r))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
requestHandlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
||||
}
|
||||
|
||||
// Notification Handlers
|
||||
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||
|
||||
// Tools Change Notification
|
||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumersFinal = new ArrayList<>();
|
||||
toolsChangeConsumersFinal.add((notification) -> logger.info("Tools changed: {}", notification));
|
||||
if (!Utils.isEmpty(toolsChangeConsumers)) {
|
||||
toolsChangeConsumersFinal.addAll(toolsChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED,
|
||||
toolsChangeNotificationHandler(toolsChangeConsumersFinal));
|
||||
|
||||
// Resources Change Notification
|
||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumersFinal = new ArrayList<>();
|
||||
resourcesChangeConsumersFinal.add((notification) -> logger.info("Resources changed: {}", notification));
|
||||
if (!Utils.isEmpty(resourcesChangeConsumers)) {
|
||||
resourcesChangeConsumersFinal.addAll(resourcesChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED,
|
||||
resourcesChangeNotificationHandler(resourcesChangeConsumersFinal));
|
||||
|
||||
// Prompts Change Notification
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumersFinal = new ArrayList<>();
|
||||
promptsChangeConsumersFinal.add((notification) -> logger.info("Prompts changed: {}", notification));
|
||||
if (!Utils.isEmpty(promptsChangeConsumers)) {
|
||||
promptsChangeConsumersFinal.addAll(promptsChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED,
|
||||
promptsChangeNotificationHandler(promptsChangeConsumersFinal));
|
||||
|
||||
// Utility Logging Notification
|
||||
List<Consumer<LoggingMessageNotification>> loggingConsumersFinal = new ArrayList<>();
|
||||
loggingConsumersFinal.add((notification) -> logger.info("Logging: {}", notification));
|
||||
if (!Utils.isEmpty(loggingConsumers)) {
|
||||
loggingConsumersFinal.addAll(loggingConsumers);
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_MESSAGE,
|
||||
loggingNotificationHandler(loggingConsumersFinal));
|
||||
|
||||
this.mcpSession = new DefaultMcpSession(requestTimeout, transport, requestHandlers, notificationHandlers);
|
||||
|
||||
}
|
||||
|
||||
// --------------------------
|
||||
// Lifecycle
|
||||
// --------------------------
|
||||
|
|
@ -587,32 +483,6 @@ public class McpAsyncClient {
|
|||
LIST_TOOLS_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification handler for tools/list_changed notifications from the
|
||||
* server. When the server's available tools change, it sends a notification to inform
|
||||
* connected clients. This handler automatically fetches the updated tool list and
|
||||
* distributes it to all registered consumers.
|
||||
* @param toolsChangeConsumers List of consumers that will be notified when the tools
|
||||
* list changes. Each consumer receives the complete updated list of tools.
|
||||
* @return A NotificationHandler that processes tools/list_changed notifications by:
|
||||
* 1. Fetching the current list of tools from the server 2. Distributing the updated
|
||||
* list to all registered consumers 3. Handling any errors that occur during this
|
||||
* process
|
||||
*/
|
||||
@Deprecated
|
||||
private NotificationHandler toolsChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers) {
|
||||
|
||||
return params -> listTools().flatMap(listToolsResult -> Mono.fromRunnable(() -> {
|
||||
for (Consumer<List<McpSchema.Tool>> toolsChangeConsumer : toolsChangeConsumers) {
|
||||
toolsChangeConsumer.accept(listToolsResult.tools());
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic())).onErrorResume(error -> {
|
||||
logger.error("Error handling tools list change notification", error);
|
||||
return Mono.empty();
|
||||
}).then(); // Convert to Mono<Void>
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification handler for tools/list_changed notifications from the
|
||||
* server. When the server's available tools change, it sends a notification to inform
|
||||
|
|
@ -745,20 +615,6 @@ public class McpAsyncClient {
|
|||
VOID_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private NotificationHandler resourcesChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers) {
|
||||
|
||||
return params -> listResources().flatMap(listResourcesResult -> Mono.fromRunnable(() -> {
|
||||
for (Consumer<List<McpSchema.Resource>> resourceChangeConsumer : resourcesChangeConsumers) {
|
||||
resourceChangeConsumer.accept(listResourcesResult.resources());
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic())).onErrorResume(error -> {
|
||||
logger.error("Error handling resources list change notification", error);
|
||||
return Mono.empty();
|
||||
}).then();
|
||||
}
|
||||
|
||||
private NotificationHandler asyncResourcesChangeNotificationHandler(
|
||||
List<Function<List<McpSchema.Resource>, Mono<Void>>> resourcesChangeConsumers) {
|
||||
return params -> listResources().flatMap(listResourcesResult -> Flux.fromIterable(resourcesChangeConsumers)
|
||||
|
|
@ -816,22 +672,6 @@ public class McpAsyncClient {
|
|||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private NotificationHandler promptsChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
|
||||
|
||||
return params -> {// @formatter:off
|
||||
return listPrompts().flatMap(listPromptsResult -> Mono.fromRunnable(() -> {
|
||||
for (Consumer<List<McpSchema.Prompt>> promptChangeConsumer : promptsChangeConsumers) {
|
||||
promptChangeConsumer.accept(listPromptsResult.prompts());
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic())).onErrorResume(error -> {
|
||||
logger.error("Error handling prompts list change notification", error);
|
||||
return Mono.empty();
|
||||
}).then(); // Convert to Mono<Void>
|
||||
}; // @formatter:on
|
||||
}
|
||||
|
||||
private NotificationHandler asyncPromptsChangeNotificationHandler(
|
||||
List<Function<List<McpSchema.Prompt>, Mono<Void>>> promptsChangeConsumers) {
|
||||
return params -> listPrompts().flatMap(listPromptsResult -> Flux.fromIterable(promptsChangeConsumers)
|
||||
|
|
|
|||
|
|
@ -135,262 +135,6 @@ public interface McpClient {
|
|||
return new AsyncSpec(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building an MCP client with the specified transport layer. The transport
|
||||
* layer handles the low-level communication between client and server using protocols
|
||||
* like stdio or Server-Sent Events (SSE).
|
||||
* @param transport The transport layer implementation for MCP communication. Common
|
||||
* implementations include {@code StdioClientTransport} for stdio-based communication
|
||||
* and {@code SseClientTransport} for SSE-based communication.
|
||||
* @return A new builder instance for configuring the client
|
||||
* @throws IllegalArgumentException if transport is null
|
||||
* @deprecated Use {@link #sync(ClientMcpTransport)} or
|
||||
* {@link #async(ClientMcpTransport)} specification builder to configure the client
|
||||
* and build an instance.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Builder using(ClientMcpTransport transport) {
|
||||
return new Builder(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for creating and configuring MCP clients. This class follows the
|
||||
* builder pattern to provide a fluent API for setting up clients with custom
|
||||
* configurations.
|
||||
*
|
||||
* <p>
|
||||
* The builder supports configuration of:
|
||||
* <ul>
|
||||
* <li>Transport layer for client-server communication
|
||||
* <li>Request timeouts for operation boundaries
|
||||
* <li>Client capabilities for feature negotiation
|
||||
* <li>Client implementation details for version tracking
|
||||
* <li>Root URIs for resource access
|
||||
* <li>Change notification handlers for tools, resources, and prompts
|
||||
* <li>Custom message sampling logic
|
||||
* </ul>
|
||||
*
|
||||
* @deprecated Use {@link #sync(ClientMcpTransport)} or
|
||||
* {@link #async(ClientMcpTransport)} specification builder to instantiate an
|
||||
* instance.
|
||||
*/
|
||||
@Deprecated
|
||||
public static class Builder {
|
||||
|
||||
private final ClientMcpTransport transport;
|
||||
|
||||
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
|
||||
|
||||
private ClientCapabilities capabilities;
|
||||
|
||||
private Implementation clientInfo = new Implementation("Spring AI MCP Client", "0.3.1");
|
||||
|
||||
private Map<String, Root> roots = new HashMap<>();
|
||||
|
||||
private List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers = new ArrayList<>();
|
||||
|
||||
private List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers = new ArrayList<>();
|
||||
|
||||
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
|
||||
|
||||
private List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
|
||||
|
||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||
|
||||
private Builder(ClientMcpTransport transport) {
|
||||
Assert.notNull(transport, "Transport must not be null");
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the duration to wait for server responses before timing out requests. This
|
||||
* timeout applies to all requests made through the client, including tool calls,
|
||||
* resource access, and prompt operations.
|
||||
* @param requestTimeout The duration to wait before timing out requests. Must not
|
||||
* be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if requestTimeout is null
|
||||
*/
|
||||
public Builder requestTimeout(Duration requestTimeout) {
|
||||
Assert.notNull(requestTimeout, "Request timeout must not be null");
|
||||
this.requestTimeout = requestTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client capabilities that will be advertised to the server during
|
||||
* connection initialization. Capabilities define what features the client
|
||||
* supports, such as tool execution, resource access, and prompt handling.
|
||||
* @param capabilities The client capabilities configuration. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if capabilities is null
|
||||
*/
|
||||
public Builder capabilities(ClientCapabilities capabilities) {
|
||||
Assert.notNull(capabilities, "Capabilities must not be null");
|
||||
this.capabilities = capabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the client implementation information that will be shared with the server
|
||||
* during connection initialization. This helps with version compatibility and
|
||||
* debugging.
|
||||
* @param clientInfo The client implementation details including name and version.
|
||||
* Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if clientInfo is null
|
||||
*/
|
||||
public Builder clientInfo(Implementation clientInfo) {
|
||||
Assert.notNull(clientInfo, "Client info must not be null");
|
||||
this.clientInfo = clientInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root URIs that this client can access. Roots define the base URIs for
|
||||
* resources that the client can request from the server. For example, a root
|
||||
* might be "file://workspace" for accessing workspace files.
|
||||
* @param roots A list of root definitions. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if roots is null
|
||||
*/
|
||||
public Builder roots(List<Root> roots) {
|
||||
Assert.notNull(roots, "Roots must not be null");
|
||||
for (Root root : roots) {
|
||||
this.roots.put(root.uri(), root);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the root URIs that this client can access, using a varargs parameter for
|
||||
* convenience. This is an alternative to {@link #roots(List)}.
|
||||
* @param roots An array of root definitions. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if roots is null
|
||||
* @see #roots(List)
|
||||
*/
|
||||
public Builder roots(Root... roots) {
|
||||
Assert.notNull(roots, "Roots must not be null");
|
||||
for (Root root : roots) {
|
||||
this.roots.put(root.uri(), root);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a custom sampling handler for processing message creation requests. The
|
||||
* sampling handler can modify or validate messages before they are sent to the
|
||||
* server, enabling custom processing logic.
|
||||
* @param samplingHandler A function that processes message requests and returns
|
||||
* results. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if samplingHandler is null
|
||||
*/
|
||||
public Builder sampling(Function<CreateMessageRequest, CreateMessageResult> samplingHandler) {
|
||||
Assert.notNull(samplingHandler, "Sampling handler must not be null");
|
||||
this.samplingHandler = samplingHandler;
|
||||
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
|
||||
* being added or removed.
|
||||
* @param toolsChangeConsumer A consumer that receives the updated list of
|
||||
* available tools. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if toolsChangeConsumer is null
|
||||
*/
|
||||
public Builder toolsChangeConsumer(Consumer<List<McpSchema.Tool>> toolsChangeConsumer) {
|
||||
Assert.notNull(toolsChangeConsumer, "Tools change consumer must not be null");
|
||||
this.toolsChangeConsumers.add(toolsChangeConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a consumer to be notified when the available resources change. This allows
|
||||
* the client to react to changes in the server's resource availability, such as
|
||||
* files being added or removed.
|
||||
* @param resourcesChangeConsumer A consumer that receives the updated list of
|
||||
* available resources. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if resourcesChangeConsumer is null
|
||||
*/
|
||||
public Builder resourcesChangeConsumer(Consumer<List<McpSchema.Resource>> resourcesChangeConsumer) {
|
||||
Assert.notNull(resourcesChangeConsumer, "Resources change consumer must not be null");
|
||||
this.resourcesChangeConsumers.add(resourcesChangeConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a consumer to be notified when the available prompts change. This allows
|
||||
* the client to react to changes in the server's prompt templates, such as new
|
||||
* templates being added or existing ones being modified.
|
||||
* @param promptsChangeConsumer A consumer that receives the updated list of
|
||||
* available prompts. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if promptsChangeConsumer is null
|
||||
*/
|
||||
public Builder promptsChangeConsumer(Consumer<List<McpSchema.Prompt>> promptsChangeConsumer) {
|
||||
Assert.notNull(promptsChangeConsumer, "Prompts change consumer must not be null");
|
||||
this.promptsChangeConsumers.add(promptsChangeConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a consumer to be notified when logging messages are received from the
|
||||
* server. This allows the client to react to log messages, such as warnings or
|
||||
* errors, that are sent by the server.
|
||||
* @param loggingConsumer A consumer that receives logging messages. Must not be
|
||||
* null.
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public Builder loggingConsumer(Consumer<McpSchema.LoggingMessageNotification> loggingConsumer) {
|
||||
Assert.notNull(loggingConsumer, "Logging consumer must not be null");
|
||||
this.loggingConsumers.add(loggingConsumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple consumers to be notified when logging messages are received from
|
||||
* the server. This allows the client to react to log messages, such as warnings
|
||||
* or errors, that are sent by the server.
|
||||
* @param loggingConsumers A list of consumers that receive logging messages. Must
|
||||
* not be null.
|
||||
* @return This builder instance for method chaining
|
||||
*/
|
||||
public Builder loggingConsumers(List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers) {
|
||||
Assert.notNull(loggingConsumers, "Logging consumers must not be null");
|
||||
this.loggingConsumers.addAll(loggingConsumers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a synchronous MCP client that provides blocking operations. Synchronous
|
||||
* clients wait for each operation to complete before returning, making them
|
||||
* simpler to use but potentially less performant for concurrent operations.
|
||||
* @return A new instance of {@link McpSyncClient} configured with this builder's
|
||||
* settings
|
||||
*/
|
||||
public McpSyncClient sync() {
|
||||
return new McpSyncClient(async());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an asynchronous MCP client that provides non-blocking operations.
|
||||
* Asynchronous clients return CompletableFuture objects immediately, allowing for
|
||||
* concurrent operations and reactive programming patterns.
|
||||
* @return A new instance of {@link McpAsyncClient} configured with this builder's
|
||||
* settings
|
||||
*/
|
||||
public McpAsyncClient async() {
|
||||
return new McpAsyncClient(transport, requestTimeout, clientInfo, capabilities, roots, toolsChangeConsumers,
|
||||
resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers, samplingHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronous client specification. This class follows the builder pattern to provide
|
||||
* a fluent API for setting up clients with custom configurations.
|
||||
|
|
|
|||
|
|
@ -11,29 +11,24 @@ import java.util.Map;
|
|||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import org.modelcontextprotocol.server.McpServer.PromptRegistration;
|
||||
import org.modelcontextprotocol.server.McpServer.ResourceRegistration;
|
||||
import org.modelcontextprotocol.server.McpServer.ToolRegistration;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession.NotificationHandler;
|
||||
import org.modelcontextprotocol.spec.McpError;
|
||||
import org.modelcontextprotocol.spec.McpSchema;
|
||||
import org.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import org.modelcontextprotocol.spec.DefaultMcpSession.NotificationHandler;
|
||||
import org.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
import org.modelcontextprotocol.spec.McpSchema.LoggingLevel;
|
||||
import org.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
|
||||
import org.modelcontextprotocol.spec.McpSchema.Tool;
|
||||
import org.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import org.modelcontextprotocol.util.Utils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* The Model Context Protocol (MCP) server implementation that provides asynchronous
|
||||
|
|
@ -183,102 +178,6 @@ public class McpAsyncServer {
|
|||
notificationHandlers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||
* @param mcpTransport The transport layer implementation for MCP communication
|
||||
* @param serverInfo The server implementation details
|
||||
* @param serverCapabilities The server capabilities
|
||||
* @param tools The list of tool registrations
|
||||
* @param resources The map of resource registrations
|
||||
* @param resourceTemplates The list of resource templates
|
||||
* @param prompts The map of prompt registrations
|
||||
* @param rootsChangeConsumers The list of consumers that will be notified when the
|
||||
* roots list changes
|
||||
* @deprecated Use {@link McpServer#sync(ServerMcpTransport)} or
|
||||
* {@link McpServer#async(ServerMcpTransport)} to create a new server instance.
|
||||
*/
|
||||
@Deprecated
|
||||
public McpAsyncServer(ServerMcpTransport mcpTransport, McpSchema.Implementation serverInfo,
|
||||
McpSchema.ServerCapabilities serverCapabilities, List<ToolRegistration> tools,
|
||||
Map<String, ResourceRegistration> resources, List<McpSchema.ResourceTemplate> resourceTemplates,
|
||||
Map<String, PromptRegistration> prompts, List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||
|
||||
this.protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
|
||||
this.serverInfo = serverInfo;
|
||||
if (!Utils.isEmpty(tools)) {
|
||||
this.tools.addAll(McpServer.mapDeprecatedTools(tools));
|
||||
}
|
||||
if (!Utils.isEmpty(resources)) {
|
||||
this.resources.putAll(McpServer.mapDeprecatedResources(resources));
|
||||
}
|
||||
if (!Utils.isEmpty(resourceTemplates)) {
|
||||
this.resourceTemplates.addAll(resourceTemplates);
|
||||
}
|
||||
if (!Utils.isEmpty(prompts)) {
|
||||
this.prompts.putAll(McpServer.mapDeprecatedPrompts(prompts));
|
||||
}
|
||||
|
||||
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
|
||||
null, // experimental
|
||||
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable logging
|
||||
// by default
|
||||
!Utils.isEmpty(this.prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
|
||||
!Utils.isEmpty(this.resources) ? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false)
|
||||
: null,
|
||||
!Utils.isEmpty(this.tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
|
||||
|
||||
Map<String, DefaultMcpSession.RequestHandler<?>> requestHandlers = new HashMap<>();
|
||||
|
||||
// Initialize request handlers for standard MCP methods
|
||||
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
|
||||
|
||||
// Ping MUST respond with an empty data, but not NULL response.
|
||||
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(""));
|
||||
|
||||
// Add tools API handlers if the tool capability is enabled
|
||||
if (this.serverCapabilities.tools() != null) {
|
||||
requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
|
||||
requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
|
||||
}
|
||||
|
||||
// Add resources API handlers if provided
|
||||
if (!Utils.isEmpty(this.resources)) {
|
||||
requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
|
||||
requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
|
||||
}
|
||||
|
||||
// Add resource templates API handlers if provided.
|
||||
if (!Utils.isEmpty(this.resourceTemplates)) {
|
||||
requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
|
||||
}
|
||||
|
||||
// Add prompts API handlers if provider exists
|
||||
if (!Utils.isEmpty(this.prompts)) {
|
||||
requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
|
||||
requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
|
||||
}
|
||||
|
||||
// Add logging API handlers if the logging capability is enabled
|
||||
if (this.serverCapabilities.logging() != null) {
|
||||
requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
|
||||
}
|
||||
|
||||
Map<String, NotificationHandler> notificationHandlers = new HashMap<>();
|
||||
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
|
||||
|
||||
if (Utils.isEmpty(rootsChangeConsumers)) {
|
||||
rootsChangeConsumers = List.of((roots) -> logger
|
||||
.warn("Roots list changed notification, but no consumers provided. Roots list changed: {}", roots));
|
||||
}
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
|
||||
rootsListChnagedNotificationHandler(rootsChangeConsumers));
|
||||
|
||||
this.transport = mcpTransport;
|
||||
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
||||
notificationHandlers);
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Lifecycle Management
|
||||
// ---------------------------------------
|
||||
|
|
@ -309,33 +208,6 @@ public class McpAsyncServer {
|
|||
};
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private DefaultMcpSession.RequestHandler<McpSchema.InitializeResult> initializeRequestHandler() {
|
||||
return params -> {
|
||||
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.InitializeRequest>() {
|
||||
});
|
||||
|
||||
this.clientCapabilities = initializeRequest.capabilities();
|
||||
this.clientInfo = initializeRequest.clientInfo();
|
||||
|
||||
logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
|
||||
initializeRequest.protocolVersion(), initializeRequest.capabilities(),
|
||||
initializeRequest.clientInfo());
|
||||
|
||||
if (!McpSchema.LATEST_PROTOCOL_VERSION.equals(initializeRequest.protocolVersion())) {
|
||||
return Mono.<McpSchema.InitializeResult>error(new McpError(
|
||||
"Unsupported protocol version from client: " + initializeRequest.protocolVersion()))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
return Mono
|
||||
.just(new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, this.serverCapabilities,
|
||||
this.serverInfo, null))
|
||||
.publishOn(Schedulers.boundedElastic());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the server capabilities that define the supported features and functionality.
|
||||
* @return The server capabilities
|
||||
|
|
@ -404,20 +276,6 @@ public class McpAsyncServer {
|
|||
LIST_ROOTS_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private NotificationHandler rootsListChnagedNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers) {
|
||||
|
||||
return params -> {
|
||||
return listRoots().flatMap(listRootsResult -> Mono.fromRunnable(() -> {
|
||||
rootsChangeConsumers.stream().forEach(consumer -> consumer.accept(listRootsResult.roots()));
|
||||
}).subscribeOn(Schedulers.boundedElastic())).onErrorResume(error -> {
|
||||
logger.error("Error handling roots list change notification", error);
|
||||
return Mono.empty();
|
||||
}).then();
|
||||
};
|
||||
}
|
||||
|
||||
private NotificationHandler asyncRootsListChangedNotificationHandler(
|
||||
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
|
||||
return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
|
||||
|
|
@ -469,40 +327,6 @@ public class McpAsyncServer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new tool registration at runtime.
|
||||
* @param toolRegistration The tool registration to add
|
||||
* @return Mono that completes when clients have been notified of the change
|
||||
* @deprecated Use {@link #addTool(McpServerFeatures.AsyncToolRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public Mono<Void> addTool(ToolRegistration toolRegistration) {
|
||||
if (toolRegistration == null) {
|
||||
return Mono.error(new McpError("Tool registration must not be null"));
|
||||
}
|
||||
if (toolRegistration.tool() == null) {
|
||||
return Mono.error(new McpError("Tool must not be null"));
|
||||
}
|
||||
if (toolRegistration.call() == null) {
|
||||
return Mono.error(new McpError("Tool call handler must not be null"));
|
||||
}
|
||||
if (this.serverCapabilities.tools() == null) {
|
||||
return Mono.error(new McpError("Server must be configured with tool capabilities"));
|
||||
}
|
||||
|
||||
// Check for duplicate tool names
|
||||
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
|
||||
return Mono.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
|
||||
}
|
||||
|
||||
this.tools.add(McpServer.mapDeprecatedTool(toolRegistration));
|
||||
logger.info("Added tool handler: {}", toolRegistration.tool().name());
|
||||
if (this.serverCapabilities.tools().listChanged()) {
|
||||
return notifyToolsListChanged();
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tool handler at runtime.
|
||||
* @param toolName The name of the tool handler to remove
|
||||
|
|
@ -595,35 +419,6 @@ public class McpAsyncServer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new resource handler at runtime.
|
||||
* @param resourceHandler The resource handler to add
|
||||
* @return Mono that completes when clients have been notified of the change
|
||||
* @deprecated Use {@link #addResource(McpServerFeatures.AsyncResourceRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public Mono<Void> addResource(ResourceRegistration resourceHandler) {
|
||||
if (resourceHandler == null || resourceHandler.resource() == null) {
|
||||
return Mono.error(new McpError("Resource must not be null"));
|
||||
}
|
||||
|
||||
if (this.serverCapabilities.resources() == null) {
|
||||
return Mono.error(new McpError("Server must be configured with resource capabilities"));
|
||||
}
|
||||
|
||||
if (this.resources.containsKey(resourceHandler.resource().uri())) {
|
||||
return Mono
|
||||
.error(new McpError("Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
|
||||
}
|
||||
|
||||
this.resources.put(resourceHandler.resource().uri(), McpServer.mapDeprecatedResource(resourceHandler));
|
||||
logger.info("Added resource handler: {}", resourceHandler.resource().uri());
|
||||
if (this.serverCapabilities.resources().listChanged()) {
|
||||
return notifyResourcesListChanged();
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a resource handler at runtime.
|
||||
* @param resourceUri The URI of the resource handler to remove
|
||||
|
|
@ -724,38 +519,6 @@ public class McpAsyncServer {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new prompt handler at runtime.
|
||||
* @param promptRegistration The prompt handler to add
|
||||
* @return Mono that completes when clients have been notified of the change
|
||||
* @deprecated Use {@link #addPrompt(McpServerFeatures.AsyncPromptRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public Mono<Void> addPrompt(PromptRegistration promptRegistration) {
|
||||
if (promptRegistration == null) {
|
||||
return Mono.error(new McpError("Prompt registration must not be null"));
|
||||
}
|
||||
if (this.serverCapabilities.prompts() == null) {
|
||||
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
|
||||
}
|
||||
|
||||
if (this.prompts.containsKey(promptRegistration.prompt().name())) {
|
||||
return Mono
|
||||
.error(new McpError("Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
|
||||
}
|
||||
|
||||
this.prompts.put(promptRegistration.prompt().name(), McpServer.mapDeprecatedPrompt(promptRegistration));
|
||||
|
||||
logger.info("Added prompt handler: {}", promptRegistration.prompt().name());
|
||||
|
||||
// Servers that declared the listChanged capability SHOULD send a notification,
|
||||
// when the list of available prompts changes
|
||||
if (this.serverCapabilities.prompts().listChanged()) {
|
||||
return notifyPromptsListChanged();
|
||||
}
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a prompt handler at runtime.
|
||||
* @param promptName The name of the prompt handler to remove
|
||||
|
|
|
|||
|
|
@ -10,16 +10,14 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.modelcontextprotocol.spec.McpSchema;
|
||||
import org.modelcontextprotocol.spec.McpTransport;
|
||||
import org.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import org.modelcontextprotocol.spec.McpSchema.CallToolResult;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
|
||||
import org.modelcontextprotocol.spec.McpTransport;
|
||||
import org.modelcontextprotocol.spec.ServerMcpTransport;
|
||||
import org.modelcontextprotocol.util.Assert;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
|
||||
/**
|
||||
* Factory class for creating Model Context Protocol (MCP) servers. MCP servers expose
|
||||
|
|
@ -138,18 +136,6 @@ public interface McpServer {
|
|||
return new AsyncSpec(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start building an MCP server with the specified transport.
|
||||
* @param transport The transport layer implementation for MCP communication
|
||||
* @return A new builder instance
|
||||
* @deprecated Use {@link #sync(ServerMcpTransport)} or
|
||||
* {@link #async(ServerMcpTransport)} to create a server instance.
|
||||
*/
|
||||
@Deprecated
|
||||
public static Builder using(ServerMcpTransport transport) {
|
||||
return new Builder(transport);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronous server specification.
|
||||
*/
|
||||
|
|
@ -908,547 +894,4 @@ public interface McpServer {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder class for creating MCP servers with custom configuration.
|
||||
*
|
||||
* @deprecated Use {@link #sync(ServerMcpTransport)} or
|
||||
* {@link #async(ServerMcpTransport)} to create a server.
|
||||
*/
|
||||
@Deprecated
|
||||
public static class Builder {
|
||||
|
||||
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
|
||||
"1.0.0");
|
||||
|
||||
private final ServerMcpTransport transport;
|
||||
|
||||
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
|
||||
|
||||
private McpSchema.ServerCapabilities serverCapabilities;
|
||||
|
||||
/**
|
||||
* The Model Context Protocol (MCP) allows servers to expose tools that can be
|
||||
* invoked by language models. Tools enable models to interact with external
|
||||
* systems, such as querying databases, calling APIs, or performing computations.
|
||||
* Each tool is uniquely identified by a name and includes metadata describing its
|
||||
* schema.
|
||||
*/
|
||||
private final List<ToolRegistration> tools = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The Model Context Protocol (MCP) provides a standardized way for servers to
|
||||
* expose resources to clients. Resources allow servers to share data that
|
||||
* provides context to language models, such as files, database schemas, or
|
||||
* application-specific information. Each resource is uniquely identified by a
|
||||
* URI.
|
||||
*/
|
||||
private Map<String, ResourceRegistration> resources = new HashMap<>();
|
||||
|
||||
private List<ResourceTemplate> resourceTemplates = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* The Model Context Protocol (MCP) provides a standardized way for servers to
|
||||
* expose prompt templates to clients. Prompts allow servers to provide structured
|
||||
* messages and instructions for interacting with language models. Clients can
|
||||
* discover available prompts, retrieve their contents, and provide arguments to
|
||||
* customize them.
|
||||
*/
|
||||
private Map<String, PromptRegistration> prompts = new HashMap<>();
|
||||
|
||||
private List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
|
||||
|
||||
private Builder(ServerMcpTransport transport) {
|
||||
Assert.notNull(transport, "Transport must not be null");
|
||||
this.transport = transport;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server implementation information that will be shared with clients
|
||||
* during connection initialization. This helps with version compatibility,
|
||||
* debugging, and server identification.
|
||||
* @param serverInfo The server implementation details including name and version.
|
||||
* Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if serverInfo is null
|
||||
*/
|
||||
public Builder serverInfo(McpSchema.Implementation serverInfo) {
|
||||
Assert.notNull(serverInfo, "Server info must not be null");
|
||||
this.serverInfo = serverInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server implementation information using name and version strings. This
|
||||
* is a convenience method alternative to
|
||||
* {@link #serverInfo(McpSchema.Implementation)}.
|
||||
* @param name The server name. Must not be null or empty.
|
||||
* @param version The server version. Must not be null or empty.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if name or version is null or empty
|
||||
* @see #serverInfo(McpSchema.Implementation)
|
||||
*/
|
||||
public Builder serverInfo(String name, String version) {
|
||||
Assert.hasText(name, "Name must not be null or empty");
|
||||
Assert.hasText(version, "Version must not be null or empty");
|
||||
this.serverInfo = new McpSchema.Implementation(name, version);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the server capabilities that will be advertised to clients during
|
||||
* connection initialization. Capabilities define what features the server
|
||||
* supports, such as:
|
||||
* <ul>
|
||||
* <li>Tool execution
|
||||
* <li>Resource access
|
||||
* <li>Prompt handling
|
||||
* <li>Streaming responses
|
||||
* <li>Batch operations
|
||||
* </ul>
|
||||
* @param serverCapabilities The server capabilities configuration. Must not be
|
||||
* null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if serverCapabilities is null
|
||||
*/
|
||||
public Builder capabilities(McpSchema.ServerCapabilities serverCapabilities) {
|
||||
this.serverCapabilities = serverCapabilities;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a single tool with its implementation handler to the server. This is a
|
||||
* convenience method for registering individual tools without creating a
|
||||
* {@link ToolRegistration} explicitly.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* .tool(
|
||||
* new Tool("calculator", "Performs calculations", schema),
|
||||
* args -> new CallToolResult("Result: " + calculate(args))
|
||||
* )
|
||||
* }</pre>
|
||||
* @param tool The tool definition including name, description, and schema. Must
|
||||
* not be null.
|
||||
* @param handler The function that implements the tool's logic. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if tool or handler is null
|
||||
*/
|
||||
public Builder tool(McpSchema.Tool tool, Function<Map<String, Object>, McpSchema.CallToolResult> handler) {
|
||||
Assert.notNull(tool, "Tool must not be null");
|
||||
Assert.notNull(handler, "Handler must not be null");
|
||||
|
||||
this.tools.add(new ToolRegistration(tool, handler));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple tools with their handlers to the server using a List. This method
|
||||
* is useful when tools are dynamically generated or loaded from a configuration
|
||||
* source.
|
||||
* @param toolRegistrations The list of tool registrations to add. Must not be
|
||||
* null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if toolRegistrations is null
|
||||
* @see #tools(ToolRegistration...)
|
||||
*/
|
||||
public Builder tools(List<ToolRegistration> toolRegistrations) {
|
||||
Assert.notNull(toolRegistrations, "Tool handlers list must not be null");
|
||||
this.tools.addAll(toolRegistrations);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds multiple tools with their handlers to the server using varargs. This
|
||||
* method provides a convenient way to register multiple tools inline.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* .tools(
|
||||
* new ToolRegistration(calculatorTool, calculatorHandler),
|
||||
* new ToolRegistration(weatherTool, weatherHandler),
|
||||
* new ToolRegistration(fileManagerTool, fileManagerHandler)
|
||||
* )
|
||||
* }</pre>
|
||||
* @param toolRegistrations The tool registrations to add. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if toolRegistrations is null
|
||||
* @see #tools(List)
|
||||
*/
|
||||
public Builder tools(ToolRegistration... toolRegistrations) {
|
||||
for (ToolRegistration tool : toolRegistrations) {
|
||||
this.tools.add(tool);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple resources with their handlers using a Map. This method is
|
||||
* useful when resources are dynamically generated or loaded from a configuration
|
||||
* source.
|
||||
* @param resourceRegsitrations Map of resource name to registration. Must not be
|
||||
* null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if resourceRegsitrations is null
|
||||
* @see #resources(ResourceRegistration...)
|
||||
*/
|
||||
public Builder resources(Map<String, ResourceRegistration> resourceRegsitrations) {
|
||||
Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null");
|
||||
this.resources.putAll(resourceRegsitrations);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple resources with their handlers using a List. This method is
|
||||
* useful when resources need to be added in bulk from a collection.
|
||||
* @param resourceRegsitrations List of resource registrations. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if resourceRegsitrations is null
|
||||
* @see #resources(ResourceRegistration...)
|
||||
*/
|
||||
public Builder resources(List<ResourceRegistration> resourceRegsitrations) {
|
||||
Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null");
|
||||
for (ResourceRegistration resource : resourceRegsitrations) {
|
||||
this.resources.put(resource.resource().uri(), resource);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple resources with their handlers using varargs. This method
|
||||
* provides a convenient way to register multiple resources inline.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* .resources(
|
||||
* new ResourceRegistration(fileResource, fileHandler),
|
||||
* new ResourceRegistration(dbResource, dbHandler),
|
||||
* new ResourceRegistration(apiResource, apiHandler)
|
||||
* )
|
||||
* }</pre>
|
||||
* @param resourceRegistrations The resource registrations to add. Must not be
|
||||
* null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if resourceRegistrations is null
|
||||
*/
|
||||
public Builder resources(ResourceRegistration... resourceRegistrations) {
|
||||
Assert.notNull(resourceRegistrations, "Resource handlers list must not be null");
|
||||
for (ResourceRegistration resource : resourceRegistrations) {
|
||||
this.resources.put(resource.resource().uri(), resource);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource templates that define patterns for dynamic resource access.
|
||||
* Templates use URI patterns with placeholders that can be filled at runtime.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* .resourceTemplates(
|
||||
* new ResourceTemplate("file://{path}", "Access files by path"),
|
||||
* new ResourceTemplate("db://{table}/{id}", "Access database records")
|
||||
* )
|
||||
* }</pre>
|
||||
* @param resourceTemplates List of resource templates. If null, clears existing
|
||||
* templates.
|
||||
* @return This builder instance for method chaining
|
||||
* @see #resourceTemplates(ResourceTemplate...)
|
||||
*/
|
||||
public Builder resourceTemplates(List<ResourceTemplate> resourceTemplates) {
|
||||
this.resourceTemplates = resourceTemplates;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the resource templates using varargs for convenience. This is an
|
||||
* alternative to {@link #resourceTemplates(List)}.
|
||||
* @param resourceTemplates The resource templates to set.
|
||||
* @return This builder instance for method chaining
|
||||
* @see #resourceTemplates(List)
|
||||
*/
|
||||
public Builder resourceTemplates(ResourceTemplate... resourceTemplates) {
|
||||
for (ResourceTemplate resourceTemplate : resourceTemplates) {
|
||||
this.resourceTemplates.add(resourceTemplate);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple prompts with their handlers using a Map. This method is
|
||||
* useful when prompts are dynamically generated or loaded from a configuration
|
||||
* source.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* Map<String, PromptRegistration> prompts = new HashMap<>();
|
||||
* prompts.put("analysis", new PromptRegistration(
|
||||
* new Prompt("analysis", "Code analysis template"),
|
||||
* request -> new GetPromptResult(generateAnalysisPrompt(request))
|
||||
* ));
|
||||
* .prompts(prompts)
|
||||
* }</pre>
|
||||
* @param prompts Map of prompt name to registration. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if prompts is null
|
||||
*/
|
||||
public Builder prompts(Map<String, PromptRegistration> prompts) {
|
||||
this.prompts.putAll(prompts);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple prompts with their handlers using a List. This method is
|
||||
* useful when prompts need to be added in bulk from a collection.
|
||||
* @param prompts List of prompt registrations. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if prompts is null
|
||||
* @see #prompts(PromptRegistration...)
|
||||
*/
|
||||
public Builder prompts(List<PromptRegistration> prompts) {
|
||||
for (PromptRegistration prompt : prompts) {
|
||||
this.prompts.put(prompt.prompt().name(), prompt);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple prompts with their handlers using varargs. This method
|
||||
* provides a convenient way to register multiple prompts inline.
|
||||
*
|
||||
* <p>
|
||||
* Example usage: <pre>{@code
|
||||
* .prompts(
|
||||
* new PromptRegistration(analysisPrompt, analysisHandler),
|
||||
* new PromptRegistration(summaryPrompt, summaryHandler),
|
||||
* new PromptRegistration(reviewPrompt, reviewHandler)
|
||||
* )
|
||||
* }</pre>
|
||||
* @param prompts The prompt registrations to add. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if prompts is null
|
||||
*/
|
||||
public Builder prompts(PromptRegistration... prompts) {
|
||||
for (PromptRegistration prompt : prompts) {
|
||||
this.prompts.put(prompt.prompt().name(), prompt);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a consumer that will be notified when the list of roots changes. This
|
||||
* is useful for updating resource availability dynamically, such as when new
|
||||
* files are added or removed.
|
||||
* @param consumer The consumer to register. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if consumer is null
|
||||
*/
|
||||
public Builder rootsChangeConsumer(Consumer<List<McpSchema.Root>> consumer) {
|
||||
Assert.notNull(consumer, "Consumer must not be null");
|
||||
this.rootsChangeConsumers.add(consumer);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple consumers that will be notified when the list of roots
|
||||
* changes. This method is useful when multiple consumers need to be registered at
|
||||
* once.
|
||||
* @param consumers The list of consumers to register. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if consumers is null
|
||||
*/
|
||||
public Builder rootsChangeConsumers(List<Consumer<List<McpSchema.Root>>> consumers) {
|
||||
Assert.notNull(consumers, "Consumers list must not be null");
|
||||
this.rootsChangeConsumers.addAll(consumers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers multiple consumers that will be notified when the list of roots
|
||||
* changes using varargs. This method provides a convenient way to register
|
||||
* multiple consumers inline.
|
||||
* @param consumers The consumers to register. Must not be null.
|
||||
* @return This builder instance for method chaining
|
||||
* @throws IllegalArgumentException if consumers is null
|
||||
*/
|
||||
public Builder rootsChangeConsumers(Consumer<List<McpSchema.Root>>... consumers) {
|
||||
for (Consumer<List<McpSchema.Root>> consumer : consumers) {
|
||||
this.rootsChangeConsumers.add(consumer);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a synchronous MCP server that provides blocking operations. Synchronous
|
||||
* servers process each request to completion before handling the next one, making
|
||||
* them simpler to implement but potentially less performant for concurrent
|
||||
* operations.
|
||||
* @return A new instance of {@link McpSyncServer} configured with this builder's
|
||||
* settings
|
||||
* @deprecated Use {@link #sync(ServerMcpTransport)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public McpSyncServer sync() {
|
||||
return new McpSyncServer(async());
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an asynchronous MCP server that provides non-blocking operations.
|
||||
* Asynchronous servers can handle multiple requests concurrently using
|
||||
* CompletableFuture, making them more efficient for high-concurrency scenarios
|
||||
* but more complex to implement.
|
||||
* @return A new instance of {@link McpAsyncServer} configured with this builder's
|
||||
* settings
|
||||
* @deprecated Use {@link #async(ServerMcpTransport)}
|
||||
*/
|
||||
@Deprecated
|
||||
public McpAsyncServer async() {
|
||||
return new McpAsyncServer(transport, serverInfo, serverCapabilities, tools, resources, resourceTemplates,
|
||||
prompts, rootsChangeConsumers);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration of a tool with its handler function. Tools are the primary way for MCP
|
||||
* servers to expose functionality to AI models. Each tool represents a specific
|
||||
* capability, such as:
|
||||
* <ul>
|
||||
* <li>Performing calculations
|
||||
* <li>Accessing external APIs
|
||||
* <li>Querying databases
|
||||
* <li>Manipulating files
|
||||
* <li>Executing system commands
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Example tool registration: <pre>{@code
|
||||
* new ToolRegistration(
|
||||
* new Tool(
|
||||
* "calculator",
|
||||
* "Performs mathematical calculations",
|
||||
* new JsonSchemaObject()
|
||||
* .required("expression")
|
||||
* .property("expression", JsonSchemaType.STRING)
|
||||
* ),
|
||||
* args -> {
|
||||
* String expr = (String) args.get("expression");
|
||||
* return new CallToolResult("Result: " + evaluate(expr));
|
||||
* }
|
||||
* )
|
||||
* }</pre>
|
||||
*
|
||||
* @param tool The tool definition including name, description, and parameter schema
|
||||
* @param call The function that implements the tool's logic, receiving arguments and
|
||||
* returning results
|
||||
* @deprecated Use {@link McpServerFeatures.SyncToolRegistration} or
|
||||
* {@link McpServerFeatures.AsyncToolRegistration}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static record ToolRegistration(McpSchema.Tool tool,
|
||||
Function<Map<String, Object>, McpSchema.CallToolResult> call) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration of a resource with its handler function. Resources provide context to
|
||||
* AI models by exposing data such as:
|
||||
* <ul>
|
||||
* <li>File contents
|
||||
* <li>Database records
|
||||
* <li>API responses
|
||||
* <li>System information
|
||||
* <li>Application state
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Example resource registration: <pre>{@code
|
||||
* new ResourceRegistration(
|
||||
* new Resource("docs", "Documentation files", "text/markdown"),
|
||||
* request -> {
|
||||
* String content = readFile(request.getPath());
|
||||
* return new ReadResourceResult(content);
|
||||
* }
|
||||
* )
|
||||
* }</pre>
|
||||
*
|
||||
* @param resource The resource definition including name, description, and MIME type
|
||||
* @param readHandler The function that handles resource read requests
|
||||
* @deprecated Use {@link McpServerFeatures.SyncResourceRegistration} or
|
||||
* {@link McpServerFeatures.AsyncResourceRegistration}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static record ResourceRegistration(McpSchema.Resource resource,
|
||||
Function<McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> readHandler) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Registration of a prompt template with its handler function. Prompts provide
|
||||
* structured templates for AI model interactions, supporting:
|
||||
* <ul>
|
||||
* <li>Consistent message formatting
|
||||
* <li>Parameter substitution
|
||||
* <li>Context injection
|
||||
* <li>Response formatting
|
||||
* <li>Instruction templating
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Example prompt registration: <pre>{@code
|
||||
* new PromptRegistration(
|
||||
* new Prompt("analyze", "Code analysis template"),
|
||||
* request -> {
|
||||
* String code = request.getArguments().get("code");
|
||||
* return new GetPromptResult(
|
||||
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
|
||||
* );
|
||||
* }
|
||||
* )
|
||||
* }</pre>
|
||||
*
|
||||
* @param prompt The prompt definition including name and description
|
||||
* @param promptHandler The function that processes prompt requests and returns
|
||||
* formatted templates
|
||||
* @deprecated Use {@link McpServerFeatures.SyncPromptRegistration} or
|
||||
* {@link McpServerFeatures.AsyncPromptRegistration}.
|
||||
*/
|
||||
@Deprecated
|
||||
public static record PromptRegistration(McpSchema.Prompt prompt,
|
||||
Function<McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
|
||||
}
|
||||
|
||||
static McpServerFeatures.AsyncToolRegistration mapDeprecatedTool(ToolRegistration oldTool) {
|
||||
return new McpServerFeatures.AsyncToolRegistration(oldTool.tool(),
|
||||
map -> Mono.fromCallable(() -> oldTool.call().apply(map)).subscribeOn(Schedulers.boundedElastic()));
|
||||
}
|
||||
|
||||
static McpServerFeatures.AsyncResourceRegistration mapDeprecatedResource(ResourceRegistration oldResource) {
|
||||
return new McpServerFeatures.AsyncResourceRegistration(oldResource.resource(),
|
||||
req -> Mono.fromCallable(() -> oldResource.readHandler().apply(req))
|
||||
.subscribeOn(Schedulers.boundedElastic()));
|
||||
}
|
||||
|
||||
static McpServerFeatures.AsyncPromptRegistration mapDeprecatedPrompt(PromptRegistration oldPrompt) {
|
||||
return new McpServerFeatures.AsyncPromptRegistration(oldPrompt.prompt(),
|
||||
req -> Mono.fromCallable(() -> oldPrompt.promptHandler().apply(req))
|
||||
.subscribeOn(Schedulers.boundedElastic()));
|
||||
}
|
||||
|
||||
static List<McpServerFeatures.AsyncToolRegistration> mapDeprecatedTools(List<ToolRegistration> oldTools) {
|
||||
return oldTools.stream().map(McpServer::mapDeprecatedTool).toList();
|
||||
}
|
||||
|
||||
static Map<String, McpServerFeatures.AsyncResourceRegistration> mapDeprecatedResources(
|
||||
Map<String, ResourceRegistration> oldResources) {
|
||||
return oldResources.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> mapDeprecatedResource(e.getValue())));
|
||||
}
|
||||
|
||||
static Map<String, McpServerFeatures.AsyncPromptRegistration> mapDeprecatedPrompts(
|
||||
Map<String, PromptRegistration> oldPrompts) {
|
||||
return oldPrompts.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, e -> mapDeprecatedPrompt(e.getValue())));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,6 @@
|
|||
|
||||
package org.modelcontextprotocol.server;
|
||||
|
||||
import org.modelcontextprotocol.server.McpServer.PromptRegistration;
|
||||
import org.modelcontextprotocol.server.McpServer.ResourceRegistration;
|
||||
import org.modelcontextprotocol.server.McpServer.ToolRegistration;
|
||||
import org.modelcontextprotocol.spec.McpError;
|
||||
import org.modelcontextprotocol.spec.McpSchema;
|
||||
import org.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
|
||||
|
|
@ -93,16 +90,6 @@ public class McpSyncServer {
|
|||
this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new tool handler.
|
||||
* @param toolHandler The tool handler to add
|
||||
* @deprecated Use {@link #addTool(McpServerFeatures.SyncToolRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void addTool(ToolRegistration toolHandler) {
|
||||
this.asyncServer.addTool(toolHandler).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a tool handler.
|
||||
* @param toolName The name of the tool handler to remove
|
||||
|
|
@ -119,16 +106,6 @@ public class McpSyncServer {
|
|||
this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new resource handler.
|
||||
* @param resourceHandler The resource handler to add
|
||||
* @deprecated Use {@link #addResource(McpServerFeatures.SyncResourceRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void addResource(ResourceRegistration resourceHandler) {
|
||||
this.asyncServer.addResource(resourceHandler).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a resource handler.
|
||||
* @param resourceUri The URI of the resource handler to remove
|
||||
|
|
@ -145,16 +122,6 @@ public class McpSyncServer {
|
|||
this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new prompt handler.
|
||||
* @param promptRegistration The prompt registration to add
|
||||
* @deprecated Use {@link #addPrompt(McpServerFeatures.SyncPromptRegistration)}.
|
||||
*/
|
||||
@Deprecated
|
||||
public void addPrompt(PromptRegistration promptRegistration) {
|
||||
this.asyncServer.addPrompt(promptRegistration).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a prompt handler.
|
||||
* @param promptName The name of the prompt handler to remove
|
||||
|
|
|
|||
Loading…
Reference in a new issue