feat: add structured logging support with severity levels
- Add client and server-side logging capabilities with configurable severity levels
- Implement logging message notifications with DEBUG to EMERGENCY levels
- Add logging consumers for client-side log handling
- Update documentation with logging examples and configuration
- Add comprehensive tests for logging functionality
- Fix typo in README.md (notificaiotn -> notification)
- Centralize MCP method names as constants
- Move all JSON-RPC method names to McpSchema class as static constants
- Replace hardcoded method strings with constant references in McpAsyncClient
and McpAsyncServer
- Update test cases to use new method name constants
Resolves #35
This commit is contained in:
parent
a5394fffd2
commit
86659344fc
6 changed files with 861 additions and 228 deletions
|
|
@ -44,6 +44,8 @@ import org.springframework.ai.mcp.spec.McpSchema.GetPromptRequest;
|
|||
import org.springframework.ai.mcp.spec.McpSchema.GetPromptResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Implementation;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.ListPromptsResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.PaginatedRequest;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Root;
|
||||
import org.springframework.ai.mcp.spec.McpTransport;
|
||||
|
|
@ -114,6 +116,7 @@ public class McpAsyncClient {
|
|||
* @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.
|
||||
*/
|
||||
public McpAsyncClient(McpTransport transport, Duration requestTimeout, Implementation clientInfo,
|
||||
|
|
@ -121,6 +124,7 @@ public class McpAsyncClient {
|
|||
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");
|
||||
|
|
@ -142,13 +146,13 @@ public class McpAsyncClient {
|
|||
|
||||
// Roots List Request Handler
|
||||
if (!Utils.isEmpty(this.roots) && this.clientCapabilities.roots() != null) {
|
||||
requestHanlers.put("roots/list", rootsListRequestHandler());
|
||||
requestHanlers.put(McpSchema.METHOD_ROOTS_LIST, rootsListRequestHandler());
|
||||
}
|
||||
|
||||
// Sampling Handler
|
||||
if (samplingHandler != null && this.clientCapabilities.sampling() != null) {
|
||||
this.samplingHandler = samplingHandler;
|
||||
requestHanlers.put("sampling/createMessage", samplingCreateMessageHandler());
|
||||
requestHanlers.put(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, samplingCreateMessageHandler());
|
||||
}
|
||||
|
||||
// Notification Handlers
|
||||
|
|
@ -160,7 +164,7 @@ public class McpAsyncClient {
|
|||
if (!Utils.isEmpty(toolsChangeConsumers)) {
|
||||
toolsChangeConsumersFinal.addAll(toolsChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put("notifications/tools/list_changed",
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED,
|
||||
toolsChangeNotificationHandler(toolsChangeConsumersFinal));
|
||||
|
||||
// Resources Change Notification
|
||||
|
|
@ -169,7 +173,7 @@ public class McpAsyncClient {
|
|||
if (!Utils.isEmpty(resourcesChangeConsumers)) {
|
||||
resourcesChangeConsumersFinal.addAll(resourcesChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put("notifications/resources/list_changed",
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED,
|
||||
resourcesChangeNotificationHandler(resourcesChangeConsumersFinal));
|
||||
|
||||
// Prompts Change Notification
|
||||
|
|
@ -178,9 +182,18 @@ public class McpAsyncClient {
|
|||
if (!Utils.isEmpty(promptsChangeConsumers)) {
|
||||
promptsChangeConsumersFinal.addAll(promptsChangeConsumers);
|
||||
}
|
||||
notificationHandlers.put("notifications/prompts/list_changed",
|
||||
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, requestHanlers, notificationHandlers);
|
||||
|
||||
}
|
||||
|
|
@ -222,8 +235,8 @@ public class McpAsyncClient {
|
|||
this.clientCapabilities,
|
||||
this.clientInfo); // @formatter:on
|
||||
|
||||
Mono<McpSchema.InitializeResult> result = this.mcpSession.sendRequest("initialize", initializeRequest,
|
||||
new TypeReference<McpSchema.InitializeResult>() {
|
||||
Mono<McpSchema.InitializeResult> result = this.mcpSession.sendRequest(McpSchema.METHOD_INITIALIZE,
|
||||
initializeRequest, new TypeReference<McpSchema.InitializeResult>() {
|
||||
});
|
||||
|
||||
return result.flatMap(initializeResult -> {
|
||||
|
|
@ -237,7 +250,8 @@ public class McpAsyncClient {
|
|||
"Unsupported protocol version from the server: " + initializeResult.protocolVersion()));
|
||||
}
|
||||
else {
|
||||
return this.mcpSession.sendNotification("notifications/initialized", null).thenReturn(initializeResult);
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED, null)
|
||||
.thenReturn(initializeResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -266,7 +280,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the server's ping response
|
||||
*/
|
||||
public Mono<Object> ping() {
|
||||
return this.mcpSession.sendRequest("ping", null, new TypeReference<Object>() {
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_PING, null, new TypeReference<Object>() {
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -335,7 +349,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the notification is sent
|
||||
*/
|
||||
public Mono<Void> rootsListChangedNotification() {
|
||||
return this.mcpSession.sendNotification("notifications/roots/list_changed");
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED);
|
||||
}
|
||||
|
||||
private RequestHandler rootsListRequestHandler() {
|
||||
|
|
@ -387,7 +401,7 @@ public class McpAsyncClient {
|
|||
* (false/absent)
|
||||
*/
|
||||
public Mono<McpSchema.CallToolResult> callTool(McpSchema.CallToolRequest callToolRequest) {
|
||||
return this.mcpSession.sendRequest("tools/call", callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_CALL, callToolRequest, CALL_TOOL_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -408,7 +422,7 @@ public class McpAsyncClient {
|
|||
* Optional cursor for pagination if more tools are available
|
||||
*/
|
||||
public Mono<McpSchema.ListToolsResult> listTools(String cursor) {
|
||||
return this.mcpSession.sendRequest("tools/list", new McpSchema.PaginatedRequest(cursor),
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_TOOLS_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_TOOLS_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
|
|
@ -427,20 +441,14 @@ public class McpAsyncClient {
|
|||
private NotificationHandler toolsChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Tool>>> toolsChangeConsumers) {
|
||||
|
||||
return new NotificationHandler() {
|
||||
@Override
|
||||
public Mono<Void> handle(Object params) {
|
||||
// TODO: add support for cursor/pagination
|
||||
return 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>
|
||||
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>
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
|
|
@ -470,7 +478,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the list of resources result
|
||||
*/
|
||||
public Mono<McpSchema.ListResourcesResult> listResources(String cursor) {
|
||||
return this.mcpSession.sendRequest("resources/list", new McpSchema.PaginatedRequest(cursor),
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_LIST, new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_RESOURCES_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
|
|
@ -489,7 +497,8 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the resource content
|
||||
*/
|
||||
public Mono<McpSchema.ReadResourceResult> readResource(McpSchema.ReadResourceRequest readResourceRequest) {
|
||||
return this.mcpSession.sendRequest("resources/read", readResourceRequest, READ_RESOURCE_RESULT_TYPE_REF);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_READ, readResourceRequest,
|
||||
READ_RESOURCE_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -512,8 +521,8 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the list of resource templates result
|
||||
*/
|
||||
public Mono<McpSchema.ListResourceTemplatesResult> listResourceTemplates(String cursor) {
|
||||
return this.mcpSession.sendRequest("resources/templates/list", new McpSchema.PaginatedRequest(cursor),
|
||||
LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST,
|
||||
new McpSchema.PaginatedRequest(cursor), LIST_RESOURCE_TEMPLATES_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -522,7 +531,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the notification is sent
|
||||
*/
|
||||
public Mono<Void> sendResourcesListChanged() {
|
||||
return this.mcpSession.sendNotification("notifications/resources/list_changed");
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -536,7 +545,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the subscription is complete
|
||||
*/
|
||||
public Mono<Void> subscribeResource(McpSchema.SubscribeRequest subscribeRequest) {
|
||||
return this.mcpSession.sendRequest("resources/subscribe", subscribeRequest, VOID_TYPE_REFERENCE);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_SUBSCRIBE, subscribeRequest, VOID_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -546,26 +555,21 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the unsubscription is complete
|
||||
*/
|
||||
public Mono<Void> unsubscribeResource(McpSchema.UnsubscribeRequest unsubscribeRequest) {
|
||||
return this.mcpSession.sendRequest("resources/unsubscribe", unsubscribeRequest, VOID_TYPE_REFERENCE);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_RESOURCES_UNSUBSCRIBE, unsubscribeRequest,
|
||||
VOID_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
private NotificationHandler resourcesChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Resource>>> resourcesChangeConsumers) {
|
||||
|
||||
return new NotificationHandler() {
|
||||
@Override
|
||||
public Mono<Void> handle(Object params) {
|
||||
// TODO: add support for cursor/pagination
|
||||
return 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(); // Convert to Mono<Void>
|
||||
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();
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
|
|
@ -591,7 +595,8 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the list of prompts result
|
||||
*/
|
||||
public Mono<ListPromptsResult> listPrompts(String cursor) {
|
||||
return this.mcpSession.sendRequest("prompts/list", new PaginatedRequest(cursor), LIST_PROMPTS_RESULT_TYPE_REF);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_LIST, new PaginatedRequest(cursor),
|
||||
LIST_PROMPTS_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -600,7 +605,7 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes with the get prompt result
|
||||
*/
|
||||
public Mono<GetPromptResult> getPrompt(GetPromptRequest getPromptRequest) {
|
||||
return this.mcpSession.sendRequest("prompts/get", getPromptRequest, GET_PROMPT_RESULT_TYPE_REF);
|
||||
return this.mcpSession.sendRequest(McpSchema.METHOD_PROMPT_GET, getPromptRequest, GET_PROMPT_RESULT_TYPE_REF);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -610,26 +615,66 @@ public class McpAsyncClient {
|
|||
* @return A Mono that completes when the notification is sent
|
||||
*/
|
||||
public Mono<Void> promptListChangedNotification() {
|
||||
return this.mcpSession.sendNotification("notifications/prompts/list_changed");
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED);
|
||||
}
|
||||
|
||||
private NotificationHandler promptsChangeNotificationHandler(
|
||||
List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers) {
|
||||
|
||||
return new NotificationHandler() {
|
||||
@Override
|
||||
public Mono<Void> handle(Object params) {
|
||||
// TODO: add support for cursor/pagination
|
||||
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>
|
||||
}
|
||||
};
|
||||
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
|
||||
};
|
||||
|
||||
// --------------------------
|
||||
// Logging
|
||||
// --------------------------
|
||||
/**
|
||||
* Create a notification handler for logging notifications from the server. This
|
||||
* handler automatically distributes logging messages to all registered consumers.
|
||||
* @param loggingConsumers List of consumers that will be notified when a logging
|
||||
* message is received. Each consumer receives the logging message notification.
|
||||
* @return A NotificationHandler that processes log notifications by distributing the
|
||||
* message to all registered consumers
|
||||
*/
|
||||
private NotificationHandler loggingNotificationHandler(
|
||||
List<Consumer<LoggingMessageNotification>> loggingConsumers) {
|
||||
|
||||
return params -> {
|
||||
|
||||
McpSchema.LoggingMessageNotification loggingMessageNotification = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.LoggingMessageNotification>() {
|
||||
});
|
||||
|
||||
return Mono.fromRunnable(() -> {
|
||||
for (Consumer<LoggingMessageNotification> loggingConsumer : loggingConsumers) {
|
||||
loggingConsumer.accept(loggingMessageNotification);
|
||||
}
|
||||
}).subscribeOn(Schedulers.boundedElastic()).then();
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Client can set the minimum logging level it wants to receive from the server.
|
||||
* @param loggingLevel the min logging level
|
||||
*/
|
||||
public Mono<Void> setLoggingLevel(LoggingLevel loggingLevel) {
|
||||
Assert.notNull(loggingLevel, "Logging level must not be null");
|
||||
|
||||
String levelName = this.transport.unmarshalFrom(loggingLevel, new TypeReference<String>() {
|
||||
});
|
||||
|
||||
Map<String, Object> params = Map.of("level", levelName);
|
||||
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_LOGGING_SET_LEVEL, params);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,6 +71,9 @@ import org.springframework.ai.mcp.util.Assert;
|
|||
* .clientInfo(new Implementation("My Client", "1.0.0"))
|
||||
* .roots(new Root("file://workspace", "Workspace Files"))
|
||||
* .toolsChangeConsumer(tools -> System.out.println("Tools updated: " + tools))
|
||||
* .resourcesChangeConsumer(resources -> System.out.println("Resources updated: " + resources))
|
||||
* .promptsChangeConsumer(prompts -> System.out.println("Prompts updated: " + prompts))
|
||||
* .loggingConsumer(message -> System.out.println("Log message: " + message))
|
||||
* .async();
|
||||
* }</pre>
|
||||
*
|
||||
|
|
@ -82,6 +85,16 @@ import org.springframework.ai.mcp.util.Assert;
|
|||
* <li>Prompt template handling
|
||||
* <li>Real-time updates through change consumers
|
||||
* <li>Custom sampling strategies
|
||||
* <li>Structured logging with severity levels
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* The client supports structured logging through the MCP logging utility:
|
||||
* <ul>
|
||||
* <li>Eight severity levels from DEBUG to EMERGENCY
|
||||
* <li>Optional logger name categorization
|
||||
* <li>Configurable logging consumers
|
||||
* <li>Server-controlled minimum log level
|
||||
* </ul>
|
||||
*
|
||||
* @author Christian Tzolov
|
||||
|
|
@ -141,6 +154,8 @@ public interface McpClient {
|
|||
|
||||
private List<Consumer<List<McpSchema.Prompt>>> promptsChangeConsumers = new ArrayList<>();
|
||||
|
||||
private List<Consumer<McpSchema.LoggingMessageNotification>> loggingConsumers = new ArrayList<>();
|
||||
|
||||
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
|
||||
|
||||
private Builder(McpTransport transport) {
|
||||
|
|
@ -284,6 +299,34 @@ public interface McpClient {
|
|||
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
|
||||
|
|
@ -304,7 +347,7 @@ public interface McpClient {
|
|||
*/
|
||||
public McpAsyncClient async() {
|
||||
return new McpAsyncClient(transport, requestTimeout, clientInfo, capabilities, roots, toolsChangeConsumers,
|
||||
resourcesChangeConsumers, promptsChangeConsumers, samplingHandler);
|
||||
resourcesChangeConsumers, promptsChangeConsumers, loggingConsumers, samplingHandler);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import org.springframework.ai.mcp.util.Assert;
|
|||
|
||||
/**
|
||||
* A synchronous client for the Model Context Protocol (MCP).
|
||||
*
|
||||
*
|
||||
* @author Dariusz Jędrzejczyk
|
||||
* @author Christian Tzolov
|
||||
*/
|
||||
|
|
@ -280,4 +280,12 @@ public class McpSyncClient implements AutoCloseable {
|
|||
this.delegate.promptListChangedNotification().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Client can set the minimum logging level it wants to receive from the server.
|
||||
* @param loggingLevel the min logging level
|
||||
*/
|
||||
public void setLoggingLevel(McpSchema.LoggingLevel loggingLevel) {
|
||||
this.delegate.setLoggingLevel(loggingLevel).block();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,19 @@
|
|||
/*
|
||||
* Copyright 2024-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ai.mcp.server;
|
||||
|
||||
import java.time.Duration;
|
||||
|
|
@ -22,6 +38,8 @@ import org.springframework.ai.mcp.spec.DefaultMcpSession.NotificationHandler;
|
|||
import org.springframework.ai.mcp.spec.McpError;
|
||||
import org.springframework.ai.mcp.spec.McpSchema;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.CallToolResult;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingLevel;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.Tool;
|
||||
import org.springframework.ai.mcp.spec.McpTransport;
|
||||
import org.springframework.ai.mcp.util.Utils;
|
||||
|
|
@ -59,6 +77,9 @@ public class McpAsyncServer {
|
|||
|
||||
private final ConcurrentHashMap<String, PromptRegistration> prompts;
|
||||
|
||||
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
|
||||
|
||||
// TODO: Add support for roots list changed notification
|
||||
/**
|
||||
* Create a new McpAsyncServer with the given transport and capabilities.
|
||||
* @param mcpTransport The transport layer implementation for MCP communication
|
||||
|
|
@ -83,7 +104,8 @@ public class McpAsyncServer {
|
|||
|
||||
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities : new McpSchema.ServerCapabilities(
|
||||
null, // experimental
|
||||
null, // logging
|
||||
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,
|
||||
|
|
@ -92,37 +114,42 @@ public class McpAsyncServer {
|
|||
Map<String, DefaultMcpSession.RequestHandler> requestHandlers = new HashMap<>();
|
||||
|
||||
// Initialize request handlers for standard MCP methods
|
||||
requestHandlers.put("initialize", initializeRequestHandler());
|
||||
requestHandlers.put(McpSchema.METHOD_INITIALIZE, initializeRequestHandler());
|
||||
|
||||
// Ping MUST respond with an empty data, but not NULL response.
|
||||
requestHandlers.put("ping", (params) -> Mono.<Object>just(""));
|
||||
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.<Object>just(""));
|
||||
|
||||
// Add tools API handlers if the tool capability is enabled
|
||||
if (this.serverCapabilities.tools() != null) {
|
||||
requestHandlers.put("tools/list", toolsListRequestHandler());
|
||||
requestHandlers.put("tools/call", toolsCallRequestHandler());
|
||||
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("resources/list", resourcesListRequestHandler());
|
||||
requestHandlers.put("resources/read", resourcesReadRequestHandler());
|
||||
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("resources/templates/list", resourceTemplateListRequestHandler());
|
||||
requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
|
||||
}
|
||||
|
||||
// Add prompts API handlers if provider exists
|
||||
if (!Utils.isEmpty(this.prompts)) {
|
||||
requestHandlers.put("prompts/list", promptsListRequestHandler());
|
||||
requestHandlers.put("prompts/get", promptsGetRequestHandler());
|
||||
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("notifications/initialized", (params) -> Mono.empty());
|
||||
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
|
||||
|
||||
this.transport = mcpTransport;
|
||||
this.mcpSession = new DefaultMcpSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
|
||||
|
|
@ -236,20 +263,16 @@ public class McpAsyncServer {
|
|||
* @return A Mono that completes when all clients have been notified
|
||||
*/
|
||||
public Mono<Void> notifyToolsListChanged() {
|
||||
return this.mcpSession.sendNotification("notifications/tools/list_changed", null);
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
|
||||
}
|
||||
|
||||
private DefaultMcpSession.RequestHandler toolsListRequestHandler() {
|
||||
return params -> {
|
||||
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||
});
|
||||
|
||||
List<Tool> tools = this.tools.stream().map(toolRegistration -> {
|
||||
return toolRegistration.tool();
|
||||
}).toList();
|
||||
|
||||
logger.info("Client tools list request - Cursor: {}", request.cursor());
|
||||
return Mono.just(new McpSchema.ListToolsResult(tools, null));
|
||||
};
|
||||
}
|
||||
|
|
@ -334,7 +357,7 @@ public class McpAsyncServer {
|
|||
* @return A Mono that completes when all clients have been notified
|
||||
*/
|
||||
public Mono<Void> notifyResourcesListChanged() {
|
||||
return this.mcpSession.sendNotification("notifications/resources/list_changed", null);
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
|
||||
}
|
||||
|
||||
private DefaultMcpSession.RequestHandler resourcesListRequestHandler() {
|
||||
|
|
@ -428,14 +451,15 @@ public class McpAsyncServer {
|
|||
* @return A Mono that completes when all clients have been notified
|
||||
*/
|
||||
public Mono<Void> notifyPromptsListChanged() {
|
||||
return this.mcpSession.sendNotification("notifications/prompts/list_changed", null);
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
|
||||
}
|
||||
|
||||
private DefaultMcpSession.RequestHandler promptsListRequestHandler() {
|
||||
return params -> {
|
||||
McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.PaginatedRequest>() {
|
||||
});
|
||||
// TODO: Implement pagination
|
||||
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
|
||||
// new TypeReference<McpSchema.PaginatedRequest>() {
|
||||
// });
|
||||
|
||||
var promptList = this.prompts.values().stream().map(PromptRegistration::propmpt).toList();
|
||||
|
||||
|
|
@ -458,4 +482,48 @@ public class McpAsyncServer {
|
|||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
// Logging Management
|
||||
// ---------------------------------------
|
||||
|
||||
/**
|
||||
* Send a logging message notification to all connected clients. Messages below the
|
||||
* current minimum logging level will be filtered out.
|
||||
* @param loggingMessageNotification The logging message to send
|
||||
* @return A Mono that completes when the notification has been sent
|
||||
*/
|
||||
public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||
|
||||
if (loggingMessageNotification == null) {
|
||||
return Mono.error(new McpError("Logging message must not be null"));
|
||||
}
|
||||
|
||||
Map<String, Object> params = this.transport.unmarshalFrom(loggingMessageNotification,
|
||||
new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
|
||||
if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to set the minimum logging level. Messages below this level will
|
||||
* not be sent.
|
||||
* @return A handler that processes logging level change requests
|
||||
*/
|
||||
private DefaultMcpSession.RequestHandler setLoggerRequestHandler() {
|
||||
return params -> {
|
||||
McpSchema.LoggingLevel setLoggerRequest = transport.unmarshalFrom(params,
|
||||
new TypeReference<McpSchema.LoggingLevel>() {
|
||||
});
|
||||
|
||||
this.minLoggingLevel = setLoggerRequest;
|
||||
|
||||
return Mono.empty();
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,9 +16,12 @@
|
|||
|
||||
package org.springframework.ai.mcp.server;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.ai.mcp.server.McpServer.PromptRegistration;
|
||||
import org.springframework.ai.mcp.server.McpServer.ResourceRegistration;
|
||||
import org.springframework.ai.mcp.server.McpServer.ToolRegistration;
|
||||
import org.springframework.ai.mcp.spec.McpSchema.LoggingMessageNotification;
|
||||
import org.springframework.ai.mcp.util.Assert;
|
||||
|
||||
/**
|
||||
|
|
@ -113,6 +116,14 @@ public class McpSyncServer {
|
|||
this.asyncServer.notifyPromptsListChanged().block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a logging message notification to all clients.
|
||||
* @param loggingMessageNotification The logging message notification to send
|
||||
*/
|
||||
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
|
||||
this.asyncServer.loggingNotification(loggingMessageNotification).block();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the server gracefully.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -44,6 +44,57 @@ public class McpSchema {
|
|||
|
||||
public static final String JSONRPC_VERSION = "2.0";
|
||||
|
||||
// ---------------------------
|
||||
// Method Names
|
||||
// ---------------------------
|
||||
|
||||
// Lifecycle Methods
|
||||
public static final String METHOD_INITIALIZE = "initialize";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_INITIALIZED = "notifications/initialized";
|
||||
|
||||
public static final String METHOD_PING = "ping";
|
||||
|
||||
// Tool Methods
|
||||
public static final String METHOD_TOOLS_LIST = "tools/list";
|
||||
|
||||
public static final String METHOD_TOOLS_CALL = "tools/call";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_TOOLS_LIST_CHANGED = "notifications/tools/list_changed";
|
||||
|
||||
// Resources Methods
|
||||
public static final String METHOD_RESOURCES_LIST = "resources/list";
|
||||
|
||||
public static final String METHOD_RESOURCES_READ = "resources/read";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED = "notifications/resources/list_changed";
|
||||
|
||||
public static final String METHOD_RESOURCES_TEMPLATES_LIST = "resources/templates/list";
|
||||
|
||||
public static final String METHOD_RESOURCES_SUBSCRIBE = "resources/subscribe";
|
||||
|
||||
public static final String METHOD_RESOURCES_UNSUBSCRIBE = "resources/unsubscribe";
|
||||
|
||||
// Prompt Methods
|
||||
public static final String METHOD_PROMPT_LIST = "prompts/list";
|
||||
|
||||
public static final String METHOD_PROMPT_GET = "prompts/get";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED = "notifications/prompts/list_changed";
|
||||
|
||||
// Logging Methods
|
||||
public static final String METHOD_LOGGING_SET_LEVEL = "logging/setLevel";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_MESSAGE = "notifications/message";
|
||||
|
||||
// Roots Methods
|
||||
public static final String METHOD_ROOTS_LIST = "roots/list";
|
||||
|
||||
public static final String METHOD_NOTIFICATION_ROOTS_LIST_CHANGED = "notifications/roots/list_changed";
|
||||
|
||||
// Sampling Methods
|
||||
public static final String METHOD_SAMPLING_CREATE_MESSAGE = "sampling/createMessage";
|
||||
|
||||
// ---------------------------
|
||||
// JSON-RPC Error Codes
|
||||
// ---------------------------
|
||||
|
|
@ -254,11 +305,16 @@ public class McpSchema {
|
|||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record ServerCapabilities( // @formatter:off
|
||||
@JsonProperty("experimental") Map<String, Object> experimental,
|
||||
@JsonProperty("logging") Object logging,
|
||||
@JsonProperty("logging") LoggingCapabilities logging,
|
||||
@JsonProperty("prompts") PromptCapabilities prompts,
|
||||
@JsonProperty("resources") ResourceCapabilities resources,
|
||||
@JsonProperty("tools") ToolCapabilities tools) {
|
||||
|
||||
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record LoggingCapabilities() {
|
||||
}
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||
public record PromptCapabilities(
|
||||
@JsonProperty("listChanged") Boolean listChanged) {
|
||||
|
|
@ -282,7 +338,7 @@ public class McpSchema {
|
|||
public static class Builder {
|
||||
|
||||
private Map<String, Object> experimental;
|
||||
private Object logging;
|
||||
private LoggingCapabilities logging = new LoggingCapabilities();
|
||||
private PromptCapabilities prompts;
|
||||
private ResourceCapabilities resources;
|
||||
private ToolCapabilities tools;
|
||||
|
|
@ -292,8 +348,8 @@ public class McpSchema {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder logging(Object logging) {
|
||||
this.logging = logging;
|
||||
public Builder logging() {
|
||||
this.logging = new LoggingCapabilities();
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
@ -331,19 +387,6 @@ public class McpSchema {
|
|||
@JsonProperty("assistant") ASSISTANT
|
||||
}// @formatter:on
|
||||
|
||||
public enum LoggingLevel {// @formatter:off
|
||||
|
||||
@JsonProperty("debug") DEBUG,
|
||||
@JsonProperty("info") INFO,
|
||||
@JsonProperty("notice") NOTICE,
|
||||
@JsonProperty("warning") WARNING,
|
||||
@JsonProperty("error") ERROR,
|
||||
@JsonProperty("critical") CRITICAL,
|
||||
@JsonProperty("alert") ALERT,
|
||||
@JsonProperty("emergency") EMERGENCY
|
||||
|
||||
} // @formatter:on
|
||||
|
||||
// ---------------------------
|
||||
// Resource Interfaces
|
||||
// ---------------------------
|
||||
|
|
@ -752,12 +795,73 @@ public class McpSchema {
|
|||
@JsonProperty("total") Double total) {
|
||||
}// @formatter:on
|
||||
|
||||
/**
|
||||
* The Model Context Protocol (MCP) provides a standardized way for servers to send
|
||||
* structured log messages to clients. Clients can control logging verbosity by
|
||||
* setting minimum log levels, with servers sending notifications containing severity
|
||||
* levels, optional logger names, and arbitrary JSON-serializable data.
|
||||
*
|
||||
* @param level The severity levels. The mimimum log level is set by the client.
|
||||
* @param logger The logger that generated the message.
|
||||
* @param data JSON-serializable logging data.
|
||||
*/
|
||||
public record LoggingMessageNotification(// @formatter:off
|
||||
@JsonProperty("level") LoggingLevel level,
|
||||
@JsonProperty("logger") String logger,
|
||||
@JsonProperty("data") Object data) {
|
||||
@JsonProperty("data") String data) {
|
||||
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
private LoggingLevel level = LoggingLevel.INFO;
|
||||
private String logger = "server";
|
||||
private String data;
|
||||
|
||||
public Builder level(LoggingLevel level) {
|
||||
this.level = level;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder logger(String logger) {
|
||||
this.logger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder data(String data) {
|
||||
this.data = data;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoggingMessageNotification build() {
|
||||
return new LoggingMessageNotification(level, logger, data);
|
||||
}
|
||||
}
|
||||
}// @formatter:on
|
||||
|
||||
public enum LoggingLevel {// @formatter:off
|
||||
@JsonProperty("debug") DEBUG(0),
|
||||
@JsonProperty("info") INFO(1),
|
||||
@JsonProperty("notice") NOTICE(2),
|
||||
@JsonProperty("warning") WARNING(3),
|
||||
@JsonProperty("error") ERROR(4),
|
||||
@JsonProperty("critical") CRITICAL(5),
|
||||
@JsonProperty("alert") ALERT(6),
|
||||
@JsonProperty("emergency") EMERGENCY(7);
|
||||
|
||||
private final int level;
|
||||
|
||||
LoggingLevel(int level) {
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
public int level() {
|
||||
return level;
|
||||
}
|
||||
|
||||
} // @formatter:on
|
||||
|
||||
// ---------------------------
|
||||
// Autocomplete
|
||||
// ---------------------------
|
||||
|
|
|
|||
Loading…
Reference in a new issue