diff --git a/framework/fel/java/plugins/pom.xml b/framework/fel/java/plugins/pom.xml index 4de9bd7e..95322044 100644 --- a/framework/fel/java/plugins/pom.xml +++ b/framework/fel/java/plugins/pom.xml @@ -16,6 +16,7 @@ tool-discoverer tool-executor tool-factory-repository + tool-mcp-server tool-repository-simple \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-server/pom.xml b/framework/fel/java/plugins/tool-mcp-server/pom.xml new file mode 100644 index 00000000..597ceeaf --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + + + org.fitframework.fel + fel-plugin-parent + 3.5.0-SNAPSHOT + + + fel-tool-mcp-server + + + + + org.fitframework + fit-api + + + org.fitframework + fit-util + + + org.fitframework + fit-reactor + + + + org.fitframework.fel + tool-service + + + + + org.assertj + assertj-core + + + + + + + org.fitframework + fit-build-maven-plugin + ${fit.version} + + system + 4 + + + + build-plugin + + build-plugin + + + + package-plugin + + package-plugin + + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven.antrun.version} + + + package + + + + + + + run + + + + + + + \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java new file mode 100644 index 00000000..dd588f05 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java @@ -0,0 +1,173 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server; + +import static modelengine.fitframework.inspection.Validation.notBlank; +import static modelengine.fitframework.inspection.Validation.notNull; + +import modelengine.fel.tool.mcp.server.entity.JsonRpcEntity; +import modelengine.fel.tool.mcp.server.handler.InitializeHandler; +import modelengine.fel.tool.mcp.server.handler.ToolCallHandler; +import modelengine.fel.tool.mcp.server.handler.ToolListHandler; +import modelengine.fel.tool.mcp.server.handler.UnsupportedMethodHandler; +import modelengine.fit.http.annotation.GetMapping; +import modelengine.fit.http.annotation.PostMapping; +import modelengine.fit.http.annotation.RequestBody; +import modelengine.fit.http.annotation.RequestQuery; +import modelengine.fit.http.entity.TextEvent; +import modelengine.fit.http.server.HttpClassicServerResponse; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.annotation.Fit; +import modelengine.fitframework.annotation.Value; +import modelengine.fitframework.flowable.Choir; +import modelengine.fitframework.flowable.Emitter; +import modelengine.fitframework.log.Logger; +import modelengine.fitframework.schedule.ExecutePolicy; +import modelengine.fitframework.schedule.Task; +import modelengine.fitframework.schedule.ThreadPoolScheduler; +import modelengine.fitframework.serialization.ObjectSerializer; +import modelengine.fitframework.util.CollectionUtils; +import modelengine.fitframework.util.MapUtils; +import modelengine.fitframework.util.StringUtils; +import modelengine.fitframework.util.UuidUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * FIT MCP Server controller. + * + * @author 季聿阶 + * @since 2025-05-13 + */ +@Component +public class McpController { + private static final Logger log = Logger.get(McpController.class); + private static final String MESSAGE_PATH = "/mcp/message"; + private static final String EVENT_ENDPOINT = "endpoint"; + private static final String EVENT_MESSAGE = "message"; + private static final String METHOD_INITIALIZE = "initialize"; + private static final String METHOD_TOOLS_LIST = "tools/list"; + private static final String METHOD_TOOLS_CALL = "tools/call"; + private static final String RESPONSE_OK = StringUtils.EMPTY; + + private final Map> emitters = new ConcurrentHashMap<>(); + private final Map responses = new ConcurrentHashMap<>(); + private final Map methodHandlers = new HashMap<>(); + private final MessageHandler unsupportedMethodHandler = new UnsupportedMethodHandler(); + private final String baseUrl; + private final ObjectSerializer serializer; + + /** + * Constructs a new instance of the McpController class. + * + * @param baseUrl The base URL for the MCP server as a {@link String}, used to construct message endpoints. + * @param serializer The JSON serializer used to serialize and deserialize RPC messages, as an + * {@link ObjectSerializer}. + * @param mcpServer The MCP server instance used to handle tool operations such as initialization, + * listing tools, and calling tools, as a {@link McpServer}. + */ + public McpController(@Value("${base-url}") String baseUrl, @Fit(alias = "json") ObjectSerializer serializer, + McpServer mcpServer) { + this.baseUrl = notBlank(baseUrl, "The base URL for MCP server cannot be blank."); + this.serializer = notNull(serializer, "The json serializer cannot be null."); + notNull(mcpServer, "The MCP server cannot be null."); + + this.methodHandlers.put(METHOD_INITIALIZE, new InitializeHandler(mcpServer)); + this.methodHandlers.put(METHOD_TOOLS_LIST, new ToolListHandler(mcpServer)); + this.methodHandlers.put(METHOD_TOOLS_CALL, new ToolCallHandler(mcpServer, this.serializer)); + + ThreadPoolScheduler channelDetectorScheduler = ThreadPoolScheduler.custom() + .corePoolSize(1) + .isDaemonThread(true) + .threadPoolName("mcp-server-channel-detector") + .build(); + channelDetectorScheduler.schedule(Task.builder().policy(ExecutePolicy.fixedDelay(10000)).runnable(() -> { + if (MapUtils.isEmpty(this.responses)) { + return; + } + List toRemoved = new ArrayList<>(); + for (Map.Entry entry : this.responses.entrySet()) { + if (entry.getValue().isActive()) { + continue; + } + toRemoved.add(entry.getKey()); + } + if (CollectionUtils.isEmpty(toRemoved)) { + return; + } + toRemoved.forEach(this.responses::remove); + toRemoved.forEach(this.emitters::remove); + log.info("Channels are inactive, remove emitters and responses. [sessionIds={}]", toRemoved); + }).build()); + } + + /** + * Creates a Server-Sent Events (SSE) channel for real-time communication with the client. + * + *

This method generates a unique session ID and registers an emitter to send events.

+ * + * @param response The HTTP server response object used to manage the SSE connection as a + * {@link HttpClassicServerResponse}. + * @return A {@link Choir}{@code <}{@link TextEvent}{@code >} object that emits text events to the connected client. + */ + @GetMapping(path = "/sse") + public Choir createSse(HttpClassicServerResponse response) { + String sessionId = UuidUtils.randomUuidString(); + this.responses.put(sessionId, response); + log.info("New SSE channel for MCP server created. [sessionId={}]", sessionId); + return Choir.create(emitter -> { + emitters.put(sessionId, emitter); + TextEvent textEvent = TextEvent.custom() + .id(sessionId) + .event(EVENT_ENDPOINT) + .data(this.baseUrl + MESSAGE_PATH + "?sessionId=" + sessionId) + .build(); + emitter.emit(textEvent); + }); + } + + /** + * Receives and processes an MCP message via HTTP POST request. + * + *

This method handles incoming JSON-RPC requests, routes them to the appropriate handler, + * and returns a response via the associated event emitter.

+ * + * @param sessionId The session ID used to identify the current client session. + * @param request The JSON-RPC request entity containing the method name and parameters. + * @return Always returns an empty string ({@value #RESPONSE_OK}) to indicate success. + */ + @PostMapping(path = MESSAGE_PATH) + public Object receiveMcpMessage(@RequestQuery(name = "sessionId") String sessionId, + @RequestBody JsonRpcEntity request) { + log.info("Receive MCP message. [sessionId={}, request={}]", sessionId, request); + Object id = request.getId(); + if (id == null) { + // Request without an ID indicates a notification message, ignore. + return RESPONSE_OK; + } + MessageHandler handler = this.methodHandlers.getOrDefault(request.getMethod(), this.unsupportedMethodHandler); + JsonRpcEntity response = new JsonRpcEntity(); + response.setId(id); + try { + Object result = handler.handle(request.getParams()); + response.setResult(result); + } catch (Exception e) { + log.error("Failed to handle MCP message.", e); + response.setError(e.getMessage()); + } + String serialized = this.serializer.serialize(response); + TextEvent textEvent = TextEvent.custom().id(sessionId).event(EVENT_MESSAGE).data(serialized).build(); + Emitter emitter = this.emitters.get(sessionId); + emitter.emit(textEvent); + log.info("Send MCP message. [response={}]", serialized); + return RESPONSE_OK; + } +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServer.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServer.java new file mode 100644 index 00000000..8d6fb3d1 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServer.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server; + +import modelengine.fel.tool.mcp.server.entity.ToolEntity; + +import java.util.List; +import java.util.Map; + +/** + * Represents the MCP Server. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public interface McpServer { + /** + * Gets MCP Server Info. + * + * @return The MCP Server Info as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + Map getInfo(); + + /** + * Gets MCP Server Tools. + * + * @return The MCP Server Tools as a {@link List}{@code <}{@link ToolEntity}{@code >}. + */ + List getTools(); + + /** + * Calls MCP Server Tool. + * + * @param name The tool name as a {@link String}. + * @param arguments The tool arguments as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + * @return The tool result as a {@link Object}. + */ + Object callTool(String name, Map arguments); +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageHandler.java new file mode 100644 index 00000000..458a17ef --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageHandler.java @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server; + +import java.util.Map; + +/** + * A functional interface for handling messages in the MCP server. + * Implementations of this interface are responsible for processing incoming message requests + * and returning an appropriate response object. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public interface MessageHandler { + /** + * Handles the given message request. + * + * @param request A map containing the request parameters and data as a + * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + * @return The result of processing the request as an {@link Object}, which can be any type of object. + */ + Object handle(Map request); +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageRequest.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageRequest.java new file mode 100644 index 00000000..b850f672 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageRequest.java @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server; + +/** + * A base class for all message request types in the MCP server. + * This class serves as a common ancestor for specific message request classes, + * providing a shared structure and type for message handling in the system. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class MessageRequest {} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageResponse.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageResponse.java new file mode 100644 index 00000000..c32634c0 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/MessageResponse.java @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server; + +/** + * A base class for all message response types in the MCP server. + * This class serves as a common ancestor for specific message response classes, + * providing a shared structure and type for returning results after message processing. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class MessageResponse {} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java new file mode 100644 index 00000000..25019a2d --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.entity; + +import java.util.Map; + +/** + * Represents a JSON RPC request entity, encapsulating information related to JSON RPC requests. + * This class follows the JSON RPC specification, supporting the construction and parsing of JSON RPC request objects. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class JsonRpcEntity { + private String jsonrpc = "2.0"; + private String method; + private Object id; + private Map params; + private Object result; + private Object error; + + /** + * Gets the JSON RPC version used in the request or response. + * + * @return The JSON RPC version as a {@link String}, typically "2.0". + */ + public String getJsonrpc() { + return this.jsonrpc; + } + + /** + * Sets the JSON RPC version used in the request or response. + * + * @param jsonrpc The JSON RPC version as a {@link String}, typically "2.0". + */ + public void setJsonrpc(String jsonrpc) { + this.jsonrpc = jsonrpc; + } + + /** + * Gets the method name to be invoked in the JSON RPC request. + * + * @return The method name as a {@link String}. + */ + public String getMethod() { + return this.method; + } + + /** + * Sets the method name to be invoked in the JSON RPC request. + * + * @param method The method name as a {@link String}. + */ + public void setMethod(String method) { + this.method = method; + } + + /** + * Gets the identifier used to correlate the request with its corresponding response. + * + * @return The identifier as an {@link Object}, which can be of type {@link String}, {@link Number}, or null. + */ + public Object getId() { + return this.id; + } + + /** + * Sets the identifier used to correlate the request with its corresponding response. + * + * @param id The identifier as an {@link Object}, which can be of type {@link String}, {@link Number}, or null. + */ + public void setId(Object id) { + this.id = id; + } + + /** + * Gets the parameters associated with the JSON RPC method call. + * + * @return A map containing the parameters as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + public Map getParams() { + return this.params; + } + + /** + * Sets the parameters associated with the JSON RPC method call. + * + * @param params A map containing the parameters as a + * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + public void setParams(Map params) { + this.params = params; + } + + /** + * Gets the result returned by the JSON RPC method execution. + * + * @return The result as an {@link Object}, which can be of any type depending on the method implementation. + */ + public Object getResult() { + return this.result; + } + + /** + * Sets the result returned by the JSON RPC method execution. + * + * @param result The result as an {@link Object}, which can be of any type depending on the method implementation. + */ + public void setResult(Object result) { + this.result = result; + } + + /** + * Gets the error information if the JSON RPC method execution resulted in an error. + * + * @return The error information as an {@link Object}, or null if no error occurred. + */ + public Object getError() { + return this.error; + } + + /** + * Sets the error information indicating that the JSON RPC method execution resulted in an error. + * + * @param error The error information as an {@link Object}, or null if no error occurred. + */ + public void setError(Object error) { + this.error = error; + } + + @Override + public String toString() { + return "JsonRpcEntity{" + "jsonrpc='" + this.jsonrpc + '\'' + ", method='" + this.method + '\'' + ", id=" + + this.id + ", params=" + this.params + ", result=" + this.result + ", error=" + this.error + '}'; + } +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java new file mode 100644 index 00000000..7ffe45b9 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java @@ -0,0 +1,91 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.entity; + +import java.util.Map; + +/** + * Represents a tool entity with name, description, and schema. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class ToolEntity { + /** + * The name of the tool. + * This serves as a unique identifier for the tool within the system. + */ + private String name; + + /** + * A brief description of the tool. + * Provides human-readable information about what the tool does. + */ + private String description; + + /** + * The input schema that defines the expected parameters when invoking the tool. + * Typically represented using a map structure, e.g., JSON schema format. + */ + private Map inputSchema; + + /** + * Gets the name of the tool. + * + * @return The tool's name as a {@link String}. + */ + public String getName() { + return this.name; + } + + /** + * Sets the name of the tool. + * + * @param name The new name for the tool, as a {@link String}. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the description of the tool. + * + * @return The tool's description as a {@link String}. + */ + public String getDescription() { + return this.description; + } + + /** + * Sets the description of the tool. + * + * @param description The new description for the tool, as a {@link String}. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Gets the input schema of the tool. + * This defines the required and optional parameters for invoking the tool. + * + * @return The tool's input schema as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + public Map getInputSchema() { + return this.inputSchema; + } + + /** + * Sets the input schema of the tool. + * + * @param inputSchema The new input schema for the tool, as a + * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + public void setInputSchema(Map inputSchema) { + this.inputSchema = inputSchema; + } +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/AbstractMessageHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/AbstractMessageHandler.java new file mode 100644 index 00000000..770489c1 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/AbstractMessageHandler.java @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import static modelengine.fitframework.inspection.Validation.notNull; + +import modelengine.fel.tool.mcp.server.MessageHandler; +import modelengine.fel.tool.mcp.server.MessageRequest; +import modelengine.fitframework.util.ObjectUtils; + +import java.util.Map; + +/** + * The abstract parent class of {@link MessageHandler}. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public abstract class AbstractMessageHandler implements MessageHandler { + private final Class requestClass; + + AbstractMessageHandler(Class requestClass) { + this.requestClass = notNull(requestClass, "The request class cannot be null."); + } + + @Override + public Object handle(Map request) { + Req req = ObjectUtils.toCustomObject(request, this.requestClass); + return this.handle(req); + } + + /** + * Handles the request. + * + * @param request The request as a {@link Req}. + * @return The response as a {@link Object}. + */ + abstract Object handle(Req request); +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/InitializeHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/InitializeHandler.java new file mode 100644 index 00000000..8d86f170 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/InitializeHandler.java @@ -0,0 +1,49 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import static modelengine.fitframework.inspection.Validation.notNull; + +import modelengine.fel.tool.mcp.server.McpServer; +import modelengine.fel.tool.mcp.server.MessageRequest; + +/** + * A handler for processing initialization requests in the MCP server. + * This class extends {@link AbstractMessageHandler} and is responsible for handling + * {@link InitializeRequest} messages by retrieving server information via the associated {@link McpServer}. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class InitializeHandler extends AbstractMessageHandler { + private final McpServer mcpServer; + + /** + * Constructs a new instance of the InitializeHandler class. + * + * @param mcpServer The MCP server instance used to retrieve server information during request handling. + * @throws IllegalStateException If {@code mcpServer} is null. + */ + public InitializeHandler(McpServer mcpServer) { + super(InitializeRequest.class); + this.mcpServer = notNull(mcpServer, "The MCP server cannot be null."); + } + + @Override + protected Object handle(InitializeRequest request) { + return this.mcpServer.getInfo(); + } + + /** + * Represents an initialization request in the MCP server. + * This request is handled by {@link InitializeHandler} to retrieve server information. + * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class InitializeRequest extends MessageRequest {} +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolCallHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolCallHandler.java new file mode 100644 index 00000000..8ab123ba --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolCallHandler.java @@ -0,0 +1,232 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import static modelengine.fitframework.inspection.Validation.notNull; +import static modelengine.fitframework.util.ObjectUtils.cast; + +import modelengine.fel.tool.mcp.server.McpServer; +import modelengine.fel.tool.mcp.server.MessageRequest; +import modelengine.fel.tool.mcp.server.MessageResponse; +import modelengine.fitframework.annotation.Property; +import modelengine.fitframework.serialization.ObjectSerializer; +import modelengine.fitframework.util.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * A handler for processing tool call requests in the MCP server. + * This class extends {@link AbstractMessageHandler} and is responsible for handling + * {@link ToolCallRequest} messages by invoking the specified tool via the associated {@link McpServer}. + * It serializes the result using the provided {@link ObjectSerializer} and returns a structured + * response through the {@link ToolCallResponse} class. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class ToolCallHandler extends AbstractMessageHandler { + private final McpServer mcpServer; + private final ObjectSerializer jsonSerializer; + + /** + * Constructs a new instance of the ToolCallHandler class. + * + * @param mcpServer The MCP server instance used to invoke tools during request handling. + * @param jsonSerializer The serializer used to convert non-string results into JSON strings. + * @throws IllegalStateException If {@code mcpServer} or {@code jsonSerializer} is null. + */ + public ToolCallHandler(McpServer mcpServer, ObjectSerializer jsonSerializer) { + super(ToolCallRequest.class); + this.mcpServer = notNull(mcpServer, "The MCP server cannot be null."); + this.jsonSerializer = notNull(jsonSerializer, "The json serializer cannot be null."); + } + + @Override + protected Object handle(ToolCallRequest request) { + if (request == null) { + throw new IllegalStateException("No tool call request."); + } + if (StringUtils.isBlank(request.getName())) { + throw new IllegalStateException("No tool name to call."); + } + ToolCallResponse response = new ToolCallResponse(); + ToolCallResponse.Content content = new ToolCallResponse.Content(); + response.setContents(List.of(content)); + content.setType("text"); + try { + Object result = this.mcpServer.callTool(request.getName(), request.getArguments()); + if (result instanceof String) { + content.setText(cast(result)); + } else { + content.setText(this.jsonSerializer.serialize(result)); + } + response.setError(false); + } catch (Exception e) { + content.setText(e.getMessage()); + response.setError(true); + } + return response; + } + + /** + * Represents a tool call request in the MCP server. + * This request contains the name of the tool to be invoked and a map of arguments + * to be passed to the tool. It is handled by {@link ToolCallHandler} to execute the tool + * and return the result. + * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class ToolCallRequest extends MessageRequest { + private String name; + private Map arguments; + + /** + * Gets the name of the tool to be called. + * + * @return The name of the tool as a {@link String}. + */ + public String getName() { + return this.name; + } + + /** + * Sets the name of the tool to be called. + * + * @param name The name of the tool as a {@link String}. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Gets the arguments to be passed to the tool. + * + * @return A map containing the arguments as a {@link Map}{@code <}{@link String}{@code , + * }{@link Object}{@code >}. + */ + public Map getArguments() { + return this.arguments; + } + + /** + * Sets the arguments to be passed to the tool. + * + * @param arguments A map containing the arguments as a + * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + public void setArguments(Map arguments) { + this.arguments = arguments; + } + } + + /** + * Represents the structured response returned after executing a tool call. + * This class includes a list of content items and an error flag indicating + * whether the execution was successful. + * + *

Each content item has a type and text value, which can be used to represent + * the result or error message from the tool execution.

+ * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class ToolCallResponse extends MessageResponse { + @Property(name = "content") + private List contents; + private boolean isError; + + /** + * Gets the list of content items included in the response. + * + * @return A list of content items as a {@link List}{@code <}{@link Content}{@code >}. + */ + public List getContents() { + return this.contents; + } + + /** + * Sets the list of content items included in the response. + * + * @param contents A list of content items as a {@link List}{@code <}{@link Content}{@code >}. + */ + public void setContents(List contents) { + this.contents = contents; + } + + /** + * Checks whether the tool execution resulted in an error. + * + * @return true if an error occurred; false otherwise. + */ + public boolean isError() { + return this.isError; + } + + /** + * Sets the error flag indicating whether the tool execution resulted in an error. + * + * @param error true if an error occurred; false otherwise. + */ + public void setError(boolean error) { + this.isError = error; + } + + /** + * Represents a single content item within the tool call response. + * Each content item has a type (e.g., "text", "json") and a text value, + * typically used to describe the result or error message from the tool execution. + * + *

This class supports multiple content formats, allowing flexible representation + * of the tool's output.

+ * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class Content { + private String type; + private String text; + + /** + * Gets the type of the content item. + * + * @return The type of the content as a {@link String}. + */ + public String getType() { + return this.type; + } + + /** + * Sets the type of the content item. + * + * @param type The type of the content as a {@link String}. + */ + public void setType(String type) { + this.type = type; + } + + /** + * Gets the text value of the content item. + * + * @return The text value as a {@link String}. + */ + public String getText() { + return this.text; + } + + /** + * Sets the text value of the content item. + * + * @param text The text value as a {@link String}. + */ + public void setText(String text) { + this.text = text; + } + } + } +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolListHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolListHandler.java new file mode 100644 index 00000000..e21fa574 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/ToolListHandler.java @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import static modelengine.fitframework.inspection.Validation.notNull; + +import modelengine.fel.tool.mcp.server.McpServer; +import modelengine.fel.tool.mcp.server.MessageRequest; +import modelengine.fitframework.util.MapBuilder; + +/** + * A handler for processing tool list requests in the MCP server. + * This class extends {@link AbstractMessageHandler} and is responsible for handling + * {@link ToolListRequest} messages by retrieving the list of tools from the associated {@link McpServer} + * and returning them in a structured map format. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class ToolListHandler extends AbstractMessageHandler { + private final McpServer mcpServer; + + /** + * Constructs a new instance of the ToolListHandler class. + * + * @param mcpServer The MCP server instance used to retrieve the list of tools during request handling. + * @throws IllegalStateException If {@code mcpServer} is null. + */ + public ToolListHandler(McpServer mcpServer) { + super(ToolListRequest.class); + this.mcpServer = notNull(mcpServer, "The MCP server cannot be null."); + } + + @Override + public Object handle(ToolListRequest request) { + return MapBuilder.get().put("tools", this.mcpServer.getTools()).build(); + } + + /** + * Represents a tool list request in the MCP server. + * This request is handled by {@link ToolListHandler} to retrieve the list of available tools + * from the server and return them in a structured format. + * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class ToolListRequest extends MessageRequest {} +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/UnsupportedMethodHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/UnsupportedMethodHandler.java new file mode 100644 index 00000000..a83dd2c0 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/UnsupportedMethodHandler.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import modelengine.fel.tool.mcp.server.MessageRequest; +import modelengine.fel.tool.mcp.server.MessageResponse; + +/** + * Represents a request for an unsupported method in the MCP server. + * This request is handled by {@link UnsupportedMethodHandler} to indicate that the + * corresponding operation is not implemented or supported. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class UnsupportedMethodHandler + extends AbstractMessageHandler { + /** + * Constructs a new instance of the UnsupportedMethodHandler class. + * + *

This handler is used to handle requests for methods that are not supported or implemented.

+ */ + public UnsupportedMethodHandler() { + super(UnsupportedMethodRequest.class); + } + + @Override + public MessageResponse handle(UnsupportedMethodRequest request) { + throw new UnsupportedOperationException("Not supported request method."); + } + + /** + * Represents a request for an operation that is not supported by the current handler. + * This class is used in conjunction with {@link UnsupportedMethodHandler} to signal + * that the requested method has no implementation. + * + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class UnsupportedMethodRequest extends MessageRequest {} +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java new file mode 100644 index 00000000..76f765b2 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.support; + +import static modelengine.fitframework.inspection.Validation.notNull; + +import modelengine.fel.tool.mcp.server.McpServer; +import modelengine.fel.tool.mcp.server.entity.ToolEntity; +import modelengine.fel.tool.service.ToolChangedObserver; +import modelengine.fel.tool.service.ToolExecuteService; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.log.Logger; +import modelengine.fitframework.util.MapBuilder; +import modelengine.fitframework.util.MapUtils; +import modelengine.fitframework.util.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * The default implementation of {@link McpServer}. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +@Component +public class DefaultMcpServer implements McpServer, ToolChangedObserver { + private static final Logger log = Logger.get(DefaultMcpServer.class); + + private final ToolExecuteService toolExecuteService; + private final Map tools = new ConcurrentHashMap<>(); + + /** + * Constructs a new instance of the DefaultMcpServer class. + * + * @param toolExecuteService The service used to execute tools when handling tool call requests. + * @throws IllegalStateException If {@code toolExecuteService} is null. + */ + public DefaultMcpServer(ToolExecuteService toolExecuteService) { + this.toolExecuteService = notNull(toolExecuteService, "The tool execute service cannot be null."); + } + + @Override + public Map getInfo() { + return MapBuilder.get() + .put("protocolVersion", "2025-03-26") + .put("capabilities", + MapBuilder.get() + .put("logging", MapBuilder.get().build()) + .put("tools", MapBuilder.get().put("listChanged", true).build()) + .build()) + .put("serverInfo", + MapBuilder.get().put("name", "FIT Store MCP Server").put("version", "3.5.0-SNAPSHOT").build()) + .build(); + } + + @Override + public List getTools() { + return List.copyOf(this.tools.values()); + } + + @Override + public Object callTool(String name, Map arguments) { + log.info("Calling tool. [toolName={}, arguments={}]", name, arguments); + String result = this.toolExecuteService.execute(name, arguments); + log.info("Tool called. [result={}]", result); + return result; + } + + @Override + public void onToolAdded(String name, String description, Map schema) { + if (StringUtils.isBlank(name)) { + log.warn("Tool addition is ignored: tool name is blank."); + return; + } + if (StringUtils.isBlank(description)) { + log.warn("Tool addition is ignored: tool description is blank. [toolName={}]", name); + return; + } + if (MapUtils.isEmpty(schema)) { + log.warn("Tool addition is ignored: tool schema is null or empty. [toolName={}]", name); + return; + } + ToolEntity tool = new ToolEntity(); + tool.setName(name); + tool.setDescription(description); + tool.setInputSchema(schema); + this.tools.put(name, tool); + log.info("Tool added to MCP server. [toolName={}, description={}, schema={}]", name, description, schema); + } + + @Override + public void onToolRemoved(String name) { + if (StringUtils.isBlank(name)) { + log.warn("Tool removal is ignored: tool name is blank."); + return; + } + this.tools.remove(name); + log.info("Tool removed from MCP server. [toolName={}]", name); + } +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/resources/application.yml b/framework/fel/java/plugins/tool-mcp-server/src/main/resources/application.yml new file mode 100644 index 00000000..64ea1035 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/resources/application.yml @@ -0,0 +1,4 @@ +fit: + beans: + packages: + - 'modelengine.fel.tool.mcp.server' \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-repository-simple/src/main/java/modelengine/fel/tool/support/SimpleToolRepository.java b/framework/fel/java/plugins/tool-repository-simple/src/main/java/modelengine/fel/tool/support/SimpleToolRepository.java index 3086da83..5da4f4fe 100644 --- a/framework/fel/java/plugins/tool-repository-simple/src/main/java/modelengine/fel/tool/support/SimpleToolRepository.java +++ b/framework/fel/java/plugins/tool-repository-simple/src/main/java/modelengine/fel/tool/support/SimpleToolRepository.java @@ -7,9 +7,11 @@ package modelengine.fel.tool.support; import static modelengine.fitframework.inspection.Validation.notBlank; +import static modelengine.fitframework.inspection.Validation.notNull; import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.tool.ToolInfoEntity; +import modelengine.fel.tool.service.ToolChangedObserver; import modelengine.fel.tool.service.ToolRepository; import modelengine.fitframework.annotation.Component; import modelengine.fitframework.log.Logger; @@ -21,7 +23,7 @@ import java.util.stream.Collectors; /** - * 表示 {@link ToolRepository} 的简单实现。 + * A simple implementation of the {@link ToolRepository} interface. * * @author 易文渊 * @author 杭潇 @@ -31,16 +33,29 @@ public class SimpleToolRepository implements ToolRepository { private static final Logger log = Logger.get(SimpleToolRepository.class); + private final ToolChangedObserver toolChangedObserver; private final Map toolCache = new ConcurrentHashMap<>(); + /** + * Constructs a new instance of the SimpleToolRepository class. + * + * @param toolChangedObserver The observer to be notified when tools are added or removed, as a + * {@link ToolChangedObserver}. + * @throws IllegalStateException If {@code toolChangedObserver} is null. + */ + public SimpleToolRepository(ToolChangedObserver toolChangedObserver) { + this.toolChangedObserver = notNull(toolChangedObserver, "The tool changed observer cannot be null."); + } + @Override public void addTool(ToolInfoEntity tool) { if (tool == null) { return; } String uniqueName = ToolInfo.identify(tool); - toolCache.put(uniqueName, tool); + this.toolCache.put(uniqueName, tool); log.info("Register tool[uniqueName={}] success.", uniqueName); + this.toolChangedObserver.onToolAdded(uniqueName, tool.description(), tool.schema()); } @Override @@ -49,8 +64,9 @@ public void deleteTool(String namespace, String toolName) { return; } String uniqueName = ToolInfo.identify(namespace, toolName); - toolCache.remove(uniqueName); + this.toolCache.remove(uniqueName); log.info("Unregister tool[uniqueName={}] success.", uniqueName); + this.toolChangedObserver.onToolRemoved(uniqueName); } @Override diff --git a/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/service/ToolChangedObserver.java b/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/service/ToolChangedObserver.java new file mode 100644 index 00000000..a9a71616 --- /dev/null +++ b/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/service/ToolChangedObserver.java @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2024 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.service; + +import java.util.Map; + +/** + * Represents an observer for tool change events. + * + * @author 季聿阶 + * @since 2025-05-11 + */ +public interface ToolChangedObserver { + /** + * Method called when a tool has been added. + * + * @param name The name of the added tool, as a {@link String}. + * @param description A description of the added tool, as a {@link String}. + * @param schema The schema associated with the added tool, as a + * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + */ + void onToolAdded(String name, String description, Map schema); + + /** + * Method called when a tool has been removed. + * + * @param name The name of the removed tool, as a {@link String}. + */ + void onToolRemoved(String name); +} diff --git a/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerRequest.java b/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerRequest.java index 39623dea..52e292c4 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerRequest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerRequest.java @@ -131,6 +131,11 @@ public InputStream getBodyInputStream() { return this.body; } + @Override + public boolean isActive() { + return this.ctx.channel().isActive(); + } + private void checkIfClosed() throws IOException { if (this.isClosed) { throw new IOException("The netty http server request has already been closed."); diff --git a/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerResponse.java b/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerResponse.java index 114a42dc..12e163bc 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerResponse.java +++ b/framework/fit/java/fit-builtin/plugins/fit-http-server-netty/src/main/java/modelengine/fit/http/server/netty/NettyHttpServerResponse.java @@ -121,6 +121,11 @@ public OutputStream getBodyOutputStream() { return this.body; } + @Override + public boolean isActive() { + return this.ctx.channel().isActive(); + } + @Override public void close() throws IOException { this.isClosed = true; diff --git a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java index c9c4fa7c..e5efcf4c 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java +++ b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java @@ -9,6 +9,7 @@ import static modelengine.fitframework.inspection.Validation.notNull; import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonFactoryBuilder; import com.fasterxml.jackson.core.JsonGenerator; @@ -83,7 +84,8 @@ public JacksonObjectSerializer(@Value("${date-time-format}") String dateTimeForm .maxStringLength(Integer.MAX_VALUE) .build()).build()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .setAnnotationIntrospector(new FitAnnotationIntrospector()) - .setVisibility(visibilityChecker); + .setVisibility(visibilityChecker) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormat)); module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormat)); diff --git a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/test/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializerTest.java b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/test/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializerTest.java index a3f1c155..74032c3b 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/test/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializerTest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/test/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializerTest.java @@ -212,7 +212,7 @@ void givenAliasJsonThenDeserializeOk() { @Test @DisplayName("当存在别名时,序列化成功") void givenAliasObjectThenSerializeOk() { - String expect = "{\"lastName\":null,\"first_name\":\"foo\",\"person_name\":null}"; + String expect = "{\"first_name\":\"foo\"}"; PersonAlias personAlias = new PersonAlias(); personAlias.firstName("foo"); assertThat(this.jsonSerializer.serialize(personAlias)).isEqualTo(expect); @@ -229,7 +229,7 @@ void givenJsonPropertyThenDeserializeOriginOk() { @Test @DisplayName("当使用原生注解,存在别名时,序列化成功") void givenJsonPropertyThenSerializeOriginOk() { - String expect = "{\"lastName\":\"bar\",\"first_name\":null,\"person_name\":null}"; + String expect = "{\"lastName\":\"bar\"}"; PersonAlias personAlias = new PersonAlias(); personAlias.lastName("bar"); assertThat(this.jsonSerializer.serialize(personAlias)).isEqualTo(expect); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerRequest.java index f22616f1..0ab8cf08 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerRequest.java @@ -16,53 +16,62 @@ import java.io.Closeable; /** - * 表示经典的服务端的 Http 请求。 + * Represents a classic HTTP server request. * * @author 季聿阶 * @since 2022-07-07 */ public interface HttpClassicServerRequest extends HttpClassicRequest, Closeable { /** - * 获取 Http 请求的所有属性集合。 + * Gets the collection of all attributes associated with this HTTP request. * - * @return 表示 Http 请求的所有属性集合的 {@link AttributeCollection}。 + * @return The {@link AttributeCollection} containing all request attributes. */ AttributeCollection attributes(); /** - * 表示 Http 请求的本地地址。 + * Gets the local address associated with this HTTP request. * - * @return 表示本地地址的 {@link Address}。 + * @return The {@link Address} representing the local endpoint. */ Address localAddress(); /** - * 表示 Http 请求的远端地址。 + * Gets the remote address associated with this HTTP request. * - * @return 表示远端地址的 {@link Address}。 + * @return The {@link Address} representing the remote endpoint. */ Address remoteAddress(); /** - * 获取 Http 请求是否为安全的的标记。 + * Checks whether this HTTP request is secure (e.g., using HTTPS). * - * @return 如果 Http 请求安全,则返回 {@code true},否则,返回 {@code false}。 + * @return true if the request is secure; false otherwise. */ boolean isSecure(); /** - * 获取 Http 消息的消息体的结构化数据的二进制内容。 + * Gets the binary content of the structured data in the message body of the HTTP request. * - * @return 表示消息体的结构化数据的二进制内容的 {@code byte[]}。 + * @return A byte array containing the entity body data. */ byte[] entityBytes(); /** - * 创建经典的服务端的 Http 请求对象。 + * Checks whether the current request is active. * - * @param httpResource 表示 Http 的资源的 {@link HttpResource}。 - * @param serverRequest 表示服务端的 Http 请求的 {@link ServerRequest}。 - * @return 表示创建出来的经典的服务端的 Http 请求对象的 {@link HttpClassicServerRequest}。 + *

An inactive request typically indicates that the underlying connection has been closed or reset.

+ * + * @return true if the request is active; false otherwise. + */ + boolean isActive(); + + /** + * Creates an instance of a classic HTTP server request. + * + * @param httpResource The HTTP resource associated with the request, as a {@link HttpResource}. + * @param serverRequest The underlying server request, as a {@link ServerRequest}. + * @return A newly created instance of {@link HttpClassicServerRequest}. */ static HttpClassicServerRequest create(HttpResource httpResource, ServerRequest serverRequest) { return new DefaultHttpClassicServerRequest(httpResource, serverRequest); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerResponse.java index 6c531f86..f74edc45 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/HttpClassicServerResponse.java @@ -19,71 +19,85 @@ import java.io.IOException; /** - * 表示经典的服务端的 Http 响应。 + * Represents a classic HTTP server response. * * @author 季聿阶 * @since 2022-11-25 */ public interface HttpClassicServerResponse extends HttpClassicResponse, Closeable { /** - * 设置 Http 响应的状态码。 + * Sets the status code for the HTTP response. * - * @param statusCode 表示 Http 响应的状态码的 {@code int}。 + * @param statusCode The HTTP status code as an {@code int}. */ void statusCode(int statusCode); /** - * 设置 Http 响应的状态信息。 + * Sets the reason phrase for the HTTP response. * - * @param reasonPhrase 表示 Http 响应的状态信息的 {@link String}。 + * @param reasonPhrase The reason phrase describing the status, as a {@link String}. */ void reasonPhrase(String reasonPhrase); /** - * 获取 Http 响应的消息头集合。 - *

注意:如果要修改 Cookie 相关的信息,请不要直接在当前对象中操作,请使用 {@link #cookies()} 进行修改。

+ * Gets the collection of message headers for the HTTP response. * - * @return 表示 Http 响应的消息头集合的 {@link ConfigurableMessageHeaders}。 + *

Note: To modify cookie-related information, do not operate directly on this object. + * Use {@link #cookies()} instead.

+ * + * @return The configurable message headers of the HTTP response as a {@link ConfigurableMessageHeaders}. */ @Override ConfigurableMessageHeaders headers(); /** - * 获取 Http 响应的 Cookie 集合。 + * Gets the collection of cookies included in the HTTP response. * - * @return 表示 Http 响应的 Cookie 集合的 {@link ConfigurableCookieCollection}。 + * @return The configurable cookie collection of the HTTP response as a {@link ConfigurableCookieCollection}. */ @Override ConfigurableCookieCollection cookies(); /** - * 设置 Http 响应的消息体的结构化数据。 - *

注意:该方法与 {@link #writableBinaryEntity()} 不能同时使用。

+ * Sets the structured data of the HTTP response body. + * + *

Note: This method cannot be used together with {@link #writableBinaryEntity()}.

* - * @param entity 表示待设置的 Http 响应的消息体的结构化数据的 {@link Entity}。 + * @param entity The structured data to be set as the HTTP response body, as an {@link Entity}. */ void entity(Entity entity); /** - * 获取 Http 响应的返回输出流。 - *

注意:该方法与 {@link #entity(Entity)} 不能同时使用,调用该方法时,会立即将 Http 消息头发送。

+ * Gets the output stream for writing binary data to the HTTP response body. * - * @return 表示 Http 响应的返回输出流的 {@link WritableBinaryEntity}。 - * @throws IOException 写入过程发生 IO 异常。 + *

Note: This method cannot be used together with {@link #entity(Entity)}. + * Invoking this method will immediately send the HTTP headers.

+ * + * @return A {@link WritableBinaryEntity} representing the output stream for the response body. + * @throws IOException If an I/O error occurs during writing. */ WritableBinaryEntity writableBinaryEntity() throws IOException; /** - * 发送当前 Http 响应。 + * Sends the current HTTP response to the client. */ void send(); /** - * 创建经典的服务端的 Http 响应对象。 + * Checks whether the current response is active. + * + *

An inactive response typically indicates that the underlying connection has been closed or reset.

+ * + * @return true if the response is active; false otherwise. + */ + boolean isActive(); + + /** + * Creates an instance of a classic HTTP server response. * - * @param httpResource 表示 Http 的资源的 {@link HttpResource}。 - * @param serverResponse 表示服务端的 Http 响应的 {@link ServerResponse}。 - * @return 表示创建的经典的服务端的 Http 响应对象的 {@link HttpClassicServerResponse}。 + * @param httpResource The HTTP resource associated with the response, as a {@link HttpResource}. + * @param serverResponse The underlying server response, as a {@link ServerResponse}. + * @return A newly created instance of {@link HttpClassicServerResponse}. */ static HttpClassicServerResponse create(HttpResource httpResource, ServerResponse serverResponse) { return new DefaultHttpClassicServerResponse(httpResource, serverResponse); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerRequest.java index aa4c251e..4804c61b 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerRequest.java @@ -86,6 +86,11 @@ public byte[] entityBytes() { return this.entityBytesLoader.get(); } + @Override + public boolean isActive() { + return this.serverRequest.isActive(); + } + private Optional actualEntity() { Charset charset = this.contentType().flatMap(ContentType::charset).orElse(StandardCharsets.UTF_8); try { diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java index 6bc319bd..9fd34b41 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/server/support/DefaultHttpClassicServerResponse.java @@ -182,6 +182,11 @@ public void send() { } } + @Override + public boolean isActive() { + return this.serverResponse.isActive(); + } + private void sendTextEventStream(TextEventStreamEntity eventStreamEntity) throws IOException { ObjectSerializer objectSerializer = this.jsonSerializer() .orElseThrow(() -> new IllegalStateException("The json serializer cannot be null.")); diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerRequest.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerRequest.java index 10f3621d..9a75d0e0 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerRequest.java @@ -12,71 +12,83 @@ import java.io.InputStream; /** - * 表示服务端的 Http 请求。 + * Represents an HTTP request on the server side. * * @author 季聿阶 * @since 2022-07-05 */ public interface ServerRequest extends Message { /** - * 获取 Http 请求的本地地址。 + * Gets the local address of the HTTP request. * - * @return 表示本地地址的 {@link Address}。 + * @return The {@link Address} representing the local address. */ Address localAddress(); /** - * 获取 Http 请求的远端地址。 + * Gets the remote address of the HTTP request. * - * @return 表示远端地址的 {@link Address}。 + * @return The {@link Address} representing the remote address. */ Address remoteAddress(); /** - * 获取 Http 请求是否为安全的的标记。 + * Checks whether the HTTP request is secure. * - * @return 如果 Http 请求安全,则返回 {@code true},否则,返回 {@code false}。 + * @return true if the HTTP request is secure; otherwise false. */ boolean isSecure(); /** - * 从 Http 消息体中读取下一个字节,如果没有任何可读的数据,返回 {@code -1}。 + * Reads the next byte from the HTTP message body. + * Returns -1 if there is no more data available to read. * - * @return 表示读取到的字节的 {@code int}。正常范围为 {@code 0 - 255},没有数据则为 {@code -1}。 - * @throws IOException 当发生 I/O 异常时。 + * @return The byte read, represented as an int in the range 0 to 255, + * or -1 if there is no more data. + * @throws IOException If an I/O error occurs. */ int readBody() throws IOException; /** - * 从 Http 消息体中读取最多 {@code bytes.length} 个字节,存放到 {@code bytes} 数组中。 + * Reads up to {@code bytes.length} bytes from the HTTP message body into the specified buffer. * - * @param bytes 表示读取数据后存放的数组的 {@code byte[]}。 - * @return 表示读取到的数据的字节数的 {@code int},如果没有任何可读的数据,返回 {@code -1}。 - * @throws IOException 当发生 I/O 异常时。 - * @throws IllegalArgumentException 当 {@code bytes} 为 {@code null} 时。 + * @param bytes The byte array into which the data is read. + * @return The total number of bytes read into the buffer, or -1 if there is no more data. + * @throws IOException If an I/O error occurs. + * @throws IllegalArgumentException If {@code bytes} is null. */ default int readBody(byte[] bytes) throws IOException { return this.readBody(notNull(bytes, "The bytes to read cannot be null."), 0, bytes.length); } /** - * 从 Http 消息体中读取最多 {@code len} 个字节,存放到 {@code bytes} 数组中。 + * Reads up to {@code len} bytes from the HTTP message body into the specified buffer, + * starting at offset {@code off}. * - * @param bytes 表示读取数据后存放的数组的 {@code byte[]}。 - * @param off 表示存放数据的偏移量的 {@code int}。 - * @param len 表示读取数据的最大数量的 {@code int}。 - * @return 表示读取到的数据的字节数的 {@code int},如果没有可读的任何数据,返回 {@code -1}。 - * @throws IOException 当发生 I/O 异常时。 - * @throws IllegalArgumentException 当 {@code bytes} 为 {@code null} 时。 - * @throws IndexOutOfBoundsException 当 {@code off} 或 {@code len} 为负数时,或 {@code off + len} - * 超过了 {@code bytes} 的长度时。 + * @param bytes The byte array into which the data is read. + * @param off The start offset in the destination array {@code bytes}. + * @param len The maximum number of bytes to read. + * @return The total number of bytes read into the buffer, or -1 if there is no more data. + * @throws IOException If an I/O error occurs. + * @throws IllegalArgumentException If {@code bytes} is null. + * @throws IndexOutOfBoundsException If {@code off} or {@code len} is negative, + * or if {@code off + len} exceeds the length of {@code bytes}. */ int readBody(byte[] bytes, int off, int len) throws IOException; /** - * 获取 Http 消息体的输入流。 + * Gets the input stream for reading the body of the HTTP message. * - * @return 表示 Http 消息体的输入流的 {@link InputStream}。 + * @return An {@link InputStream} representing the body content of the HTTP request. */ InputStream getBodyInputStream(); + + /** + * Checks whether the current request is active. + * + *

An inactive request typically indicates that the underlying connection has been closed or reset.

+ * + * @return true if the request is active; false otherwise. + */ + boolean isActive(); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerResponse.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerResponse.java index 6d3ba00d..c8e886b4 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerResponse.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/ServerResponse.java @@ -12,7 +12,7 @@ import java.io.OutputStream; /** - * 表示服务端的 Http 响应。 + * Represents an HTTP response on the server side. * * @author 季聿阶 * @since 2022-07-05 @@ -20,27 +20,26 @@ public interface ServerResponse extends Message { /** - * 向 Http 消息起始行和消息头中写入数据。 + * Writes the start line and headers of the HTTP message. * - * @throws IOException 当发生 I/O 异常时。 + * @throws IOException If an I/O error occurs. */ void writeStartLineAndHeaders() throws IOException; /** - * 向 Http 消息体中写入数据。 + * Writes a single byte to the HTTP message body. * - * @param b 表示待写入数据的 {@code int}。 - *

实际上,这里的数据 {@code b} 一定是一个 {@code byte}。

- * @throws IOException 当发生 I/O 异常时。 + * @param b The data to be written, represented as an int. Only the least significant byte is used. + * @throws IOException If an I/O error occurs. */ void writeBody(int b) throws IOException; /** - * 向 Http 消息体中写入数据。 + * Writes the entire contents of the specified byte array to the HTTP message body. * - * @param bytes 表示待写入数据的 {@code byte[]}。 - * @throws IOException 当发生 I/O 异常时。 - * @throws IllegalArgumentException 当 {@code bytes} 为 {@code null} 时。 + * @param bytes The byte array containing the data to be written. + * @throws IOException If an I/O error occurs. + * @throws IllegalArgumentException If {@code bytes} is null. * @see #writeBody(byte[], int, int) */ default void writeBody(byte[] bytes) throws IOException { @@ -48,29 +47,39 @@ default void writeBody(byte[] bytes) throws IOException { } /** - * 向 Http 消息体中写入数据。 + * Writes up to {@code len} bytes from the specified byte array, + * starting at offset {@code off}, to the HTTP message body. * - * @param bytes 表示待写入数据所在数组的 {@code byte[]}。 - * @param off 表示待写入数据的偏移量的 {@code int}。 - * @param len 表示待写入数据的数量的 {@code int}。 - * @throws IOException 当发生 I/O 异常时。 - * @throws IllegalArgumentException 当 {@code bytes} 为 {@code null} 时。 - * @throws IndexOutOfBoundsException 当 {@code off} 或 {@code len} 为负数时,或 {@code off + len} - * 超过了 {@code bytes} 的长度时。 + * @param bytes The byte array containing the data to be written. + * @param off The start offset in the source array {@code bytes}. + * @param len The number of bytes to write. + * @throws IOException If an I/O error occurs. + * @throws IllegalArgumentException If {@code bytes} is null. + * @throws IndexOutOfBoundsException If {@code off} or {@code len} is negative, + * or if {@code off + len} exceeds the length of {@code bytes}. */ void writeBody(byte[] bytes, int off, int len) throws IOException; /** - * 强制已经写入的数据执行写出,也就是说将之前写入到缓冲区的数据全部对外输出;同时发送响应结束标识符。 + * Forces any buffered data to be written out immediately and sends the response end marker. * - * @throws IOException 当发生 I/O 异常时。 + * @throws IOException If an I/O error occurs. */ void flush() throws IOException; /** - * 获取消息体的输出流。 + * Gets the output stream for writing the body of the HTTP message. * - * @return 表示消息体的输出流的 {@link OutputStream}。 + * @return An {@link OutputStream} representing the body content of the HTTP response. */ OutputStream getBodyOutputStream(); + + /** + * Checks whether the current response is active. + * + *

An inactive response typically indicates that the underlying connection has been closed or reset.

+ * + * @return true if the response is active; false otherwise. + */ + boolean isActive(); }