From 7953968266437cff5ab88f95e2db618fff1a4cee Mon Sep 17 00:00:00 2001 From: CodeCaster Date: Tue, 27 May 2025 16:18:19 +0800 Subject: [PATCH] [fel] Enhanced MCP Client and Server Interaction with Tool Management Improvements This commit introduces several enhancements to the MCP (Model Communication Protocol) client and server interaction, focusing on improving tool management and communication capabilities. Key changes include: 1. ToolInfo Interface Expansion: - Added a new static method parseIdentifier to parse tool identifiers into namespace and tool name components, enhancing tool identification and management. 2. MCP Client Enhancements: - Updated the DefaultMcpClient class to support more robust initialization and communication with the MCP server. - Introduced new methods for handling Server-Sent Events (SSE) and improved error handling for tool calls. - Enhanced the callTool method to return results directly and manage pending requests more efficiently. 3. MCP Server Adjustments: - Modified the McpServer interface and its implementations to better support tool addition and retrieval. - Updated the DefaultMcpServer to provide a more structured server schema and improved tool registration logic. 4. Weather Service Example: - Introduced a new weather service example (WeatherService and WeatherServiceImpl) to demonstrate tool implementation and usage within the MCP framework. 5. Code Structure and Cleanup: - Reorganized and cleaned up code in various classes to improve readability and maintainability. - Removed deprecated or unused code related to the MCP server entity. 6. Dependency and Plugin Updates: - Added new dependencies and updated plugins in the pom.xml file to support the enhanced MCP functionality. 7. Test Code Updates: - Expanded test cases for the MCP client and server to ensure compatibility and functionality of the new features. These changes collectively enhance the MCP framework's ability to manage and interact with tools, providing a more robust and flexible platform for model communication and execution. --- .../modelengine/fel/core/tool/ToolInfo.java | 17 ++ .../fel/tool/support/DefaultToolExecutor.java | 9 +- .../mcp/client/support/DefaultMcpClient.java | 127 +++++++++-- .../support/DefaultMcpClientFactory.java | 4 +- .../fel/tool/mcp/server/McpServer.java | 22 +- .../tool/mcp/server/McpServerController.java | 12 +- .../mcp/server/handler/InitializeHandler.java | 2 +- .../mcp/server/support/DefaultMcpServer.java | 30 +-- .../mcp/server/McpServerControllerTest.java | 20 +- .../server/support/DefaultMcpServerTest.java | 18 +- .../fel/java/plugins/tool-mcp-test/pom.xml | 21 +- .../fel/tool/mcp/test/TestController.java | 43 +++- .../tool/mcp/test/tool/WeatherService.java | 44 ++++ .../mcp/test/tool/WeatherServiceImpl.java | 46 ++++ .../tool/support/SimpleToolRepository.java | 4 +- .../fel/tool/mcp/client/McpClientFactory.java | 5 +- .../fel/tool/mcp/entity/ClientSchema.java | 26 +++ .../fel/tool/mcp/entity/JsonRpc.java | 4 +- .../fel/tool/mcp/entity/Server.java | 213 ------------------ .../fel/tool/mcp/entity/ServerSchema.java | 61 +++++ .../fel/tool/service/ToolChangedObserver.java | 4 +- .../client/okhttp/OkHttpClientRequest.java | 6 +- .../DefaultReflectibleHttpHandlerTest.java | 6 +- .../DefaultHttpExceptionHandlerTest.java | 6 +- .../server/netty/NettyHttpServerRequest.java | 13 +- .../fit/http/HttpClassicRequest.java | 1 + .../support/AbstractHttpClassicRequest.java | 26 +-- .../modelengine/fit/http/util/HttpUtils.java | 39 +--- .../http/client/HttpClassicClientTest.java | 7 +- .../DefaultHttpClassicClientRequestTest.java | 29 +-- .../handler/MockHttpClassicServerRequest.java | 4 +- .../support/DefaultQueryCollectionTest.java | 3 +- .../fit/http/protocol}/QueryCollection.java | 4 +- .../fit/http/protocol/RequestLine.java | 42 ++-- .../support/DefaultQueryCollection.java | 8 +- .../protocol/support/DefaultRequestLine.java | 11 +- .../fit/http/protocol/util/QueryUtils.java | 80 +++++++ .../support/DefaultRequestLineTest.java | 11 +- 38 files changed, 593 insertions(+), 435 deletions(-) create mode 100644 framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherService.java create mode 100644 framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherServiceImpl.java create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ClientSchema.java delete mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ServerSchema.java rename framework/fit/java/fit-builtin/services/{fit-http-classic/definition/src/main/java/modelengine/fit/http => fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol}/QueryCollection.java (96%) rename framework/fit/java/fit-builtin/services/{fit-http-classic/definition/src/main/java/modelengine/fit/http => fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol}/support/DefaultQueryCollection.java (91%) create mode 100644 framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/util/QueryUtils.java diff --git a/framework/fel/java/fel-core/src/main/java/modelengine/fel/core/tool/ToolInfo.java b/framework/fel/java/fel-core/src/main/java/modelengine/fel/core/tool/ToolInfo.java index 62ebd87e..749d9061 100644 --- a/framework/fel/java/fel-core/src/main/java/modelengine/fel/core/tool/ToolInfo.java +++ b/framework/fel/java/fel-core/src/main/java/modelengine/fel/core/tool/ToolInfo.java @@ -141,4 +141,21 @@ static String identify(ToolInfo toolInfo) { static String identify(String namespace, String toolName) { return StringUtils.format("{0}:{1}", namespace, toolName); } + + /** + * Parses the tool identifier. + * + * @param identifier The identifier to be parsed. + * @return An array containing the namespace and tool name. + */ + static String[] parseIdentifier(String identifier) { + if (identifier == null || identifier.isEmpty()) { + throw new IllegalArgumentException("Identifier cannot be null or empty."); + } + String[] parts = identifier.split(":", 2); + if (parts.length != 2) { + throw new IllegalArgumentException("Invalid identifier format. Expected 'namespace:name'."); + } + return parts; + } } \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-executor/src/main/java/modelengine/fel/tool/support/DefaultToolExecutor.java b/framework/fel/java/plugins/tool-executor/src/main/java/modelengine/fel/tool/support/DefaultToolExecutor.java index 9abd914d..a1674b34 100644 --- a/framework/fel/java/plugins/tool-executor/src/main/java/modelengine/fel/tool/support/DefaultToolExecutor.java +++ b/framework/fel/java/plugins/tool-executor/src/main/java/modelengine/fel/tool/support/DefaultToolExecutor.java @@ -8,10 +8,11 @@ import static modelengine.fitframework.inspection.Validation.notNull; -import modelengine.fel.tool.ToolInfoEntity; +import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.tool.Tool; import modelengine.fel.tool.ToolFactory; import modelengine.fel.tool.ToolFactoryRepository; +import modelengine.fel.tool.ToolInfoEntity; import modelengine.fel.tool.service.ToolExecuteService; import modelengine.fel.tool.service.ToolRepository; import modelengine.fitframework.annotation.Component; @@ -70,13 +71,15 @@ public String execute(String group, String toolName, Map jsonObj @Override @Fitable(id = "standard") public String execute(String uniqueName, String jsonArgs) { - return this.execute("Common", uniqueName, jsonArgs); + String[] strings = ToolInfo.parseIdentifier(uniqueName); + return this.execute(strings[0], strings[1], jsonArgs); } @Override @Fitable(id = "standard") public String execute(String uniqueName, Map jsonObject) { - return this.execute("Common", uniqueName, jsonObject); + String[] strings = ToolInfo.parseIdentifier(uniqueName); + return this.execute(strings[0], strings[1], jsonObject); } private Tool getTool(String group, String toolName) { diff --git a/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java index 74c08c12..f22ef103 100644 --- a/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java +++ b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java @@ -9,9 +9,10 @@ import static modelengine.fitframework.util.ObjectUtils.cast; import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.entity.ClientSchema; import modelengine.fel.tool.mcp.entity.JsonRpc; import modelengine.fel.tool.mcp.entity.Method; -import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.ServerSchema; import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fit.http.client.HttpClassicClient; import modelengine.fit.http.client.HttpClassicClientRequest; @@ -26,8 +27,11 @@ import modelengine.fitframework.schedule.ThreadPoolExecutor; import modelengine.fitframework.schedule.ThreadPoolScheduler; import modelengine.fitframework.serialization.ObjectSerializer; +import modelengine.fitframework.util.CollectionUtils; import modelengine.fitframework.util.LockUtils; +import modelengine.fitframework.util.MapBuilder; import modelengine.fitframework.util.ObjectUtils; +import modelengine.fitframework.util.StringUtils; import modelengine.fitframework.util.ThreadUtils; import modelengine.fitframework.util.UuidUtils; @@ -54,37 +58,43 @@ public class DefaultMcpClient implements McpClient { private final ObjectSerializer jsonSerializer; private final HttpClassicClient client; - private final String connectionString; + private final String baseUri; + private final String sseEndpoint; private final String name; private final AtomicLong id = new AtomicLong(0); - private volatile String messageUrl; + private volatile String messageEndpoint; private volatile String sessionId; - private volatile Server server; + private volatile ServerSchema serverSchema; private volatile boolean initialized = false; private final List tools = new ArrayList<>(); private final Object initializedLock = LockUtils.newSynchronizedLock(); private final Object toolsLock = LockUtils.newSynchronizedLock(); private final Map>> responseConsumers = new ConcurrentHashMap<>(); private final Map pendingRequests = new ConcurrentHashMap<>(); + private final Map pendingResults = new ConcurrentHashMap<>(); /** * Constructs a new instance of the DefaultMcpClient. * * @param jsonSerializer The serializer used for JSON serialization and deserialization. * @param client The HTTP client used for communication with the MCP server. - * @param connectionString The connection string used to establish the initial connection. + * @param baseUri The base URI of the MCP server. + * @param sseEndpoint The endpoint for the Server-Sent Events (SSE) connection. */ - public DefaultMcpClient(ObjectSerializer jsonSerializer, HttpClassicClient client, String connectionString) { + public DefaultMcpClient(ObjectSerializer jsonSerializer, HttpClassicClient client, String baseUri, + String sseEndpoint) { this.jsonSerializer = jsonSerializer; this.client = client; - this.connectionString = connectionString; + this.baseUri = baseUri; + this.sseEndpoint = sseEndpoint; this.name = UuidUtils.randomUuidString(); } @Override public void initialize() { - HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.GET, connectionString); + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.GET, this.baseUri + this.sseEndpoint); Choir messages = this.client.exchangeStream(request, TextEvent.class); ThreadPoolExecutor threadPool = ThreadPoolExecutor.custom() .threadPoolName("mcp-client-" + this.name) @@ -125,7 +135,13 @@ public void initialize() { } private void consumeTextEvent(TextEvent textEvent) { - log.info("Receive message from MCP server. [message={}]", textEvent.data()); + log.info("Receive message from MCP server. [id={}, event={}, message={}]", + textEvent.id(), + textEvent.event(), + textEvent.data()); + if (StringUtils.isBlank(textEvent.event()) || StringUtils.isBlank((String) textEvent.data())) { + return; + } if (Objects.equals(textEvent.event(), "endpoint")) { this.initializeMcpServer(textEvent); return; @@ -157,7 +173,8 @@ private void pingServer() { log.info("MCP client is not initialized and {} method will be delayed.", Method.PING.code()); return; } - HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.POST, this.messageUrl); + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.POST, this.baseUri + this.messageEndpoint); long currentId = this.getNextId(); JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.PING.code()); request.entity(Entity.createObject(request, rpcRequest)); @@ -183,12 +200,17 @@ private void pingServer() { } private void initializeMcpServer(TextEvent textEvent) { - this.messageUrl = textEvent.data().toString(); - this.sessionId = textEvent.id(); - HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.POST, this.messageUrl); + this.messageEndpoint = textEvent.data().toString(); + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.POST, this.baseUri + this.messageEndpoint); + this.sessionId = + request.queries().first("session_id").orElseThrow(() -> new IllegalStateException("no session_id")); long currentId = this.getNextId(); this.responseConsumers.put(currentId, this::initializedMcpServer); - JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.INITIALIZE.code()); + ClientSchema schema = new ClientSchema("2024-11-05", + new ClientSchema.Capabilities(), + new ClientSchema.Info("FIT MCP Client", "3.5.0-SNAPSHOT")); + JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.INITIALIZE.code(), schema); request.entity(Entity.createObject(request, rpcRequest)); log.info("Send {} method to MCP server. [sessionId={}, request={}]", Method.INITIALIZE.code(), @@ -223,9 +245,9 @@ private void initializedMcpServer(JsonRpc.Response response) { this.initialized = true; this.initializedLock.notifyAll(); } - this.server = ObjectUtils.toCustomObject(response.result(), Server.class); - log.info("MCP server has initialized. [server={}]", this.server); - HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.POST, this.messageUrl); + this.recordServerSchema(response); + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.POST, this.baseUri + this.messageEndpoint); JsonRpc.Notification notification = JsonRpc.createNotification(Method.NOTIFICATION_INITIALIZED.code()); request.entity(Entity.createObject(request, notification)); log.info("Send {} method to MCP server. [sessionId={}, notification={}]", @@ -249,12 +271,19 @@ private void initializedMcpServer(JsonRpc.Response response) { } } + private void recordServerSchema(JsonRpc.Response response) { + Map mapResult = cast(response.result()); + this.serverSchema = ServerSchema.create(mapResult); + log.info("MCP server has initialized. [server={}]", this.serverSchema); + } + @Override public List getTools() { if (this.isNotInitialized()) { throw new IllegalStateException("MCP client is not initialized. Please wait a moment."); } - HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.POST, this.messageUrl); + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.POST, this.baseUri + this.messageEndpoint); long currentId = this.getNextId(); this.responseConsumers.put(currentId, this::getTools0); this.pendingRequests.put(currentId, true); @@ -292,6 +321,7 @@ private void getTools0(JsonRpc.Response response) { log.error("Failed to get tools list from MCP server. [sessionId={}, response={}]", this.sessionId, response); + this.pendingRequests.put(response.id(), false); return; } Map result = cast(response.result()); @@ -301,8 +331,8 @@ private void getTools0(JsonRpc.Response response) { this.tools.addAll(rawTools.stream() .map(rawTool -> ObjectUtils.toCustomObject(rawTool, Tool.class)) .toList()); - this.pendingRequests.put(response.id(), false); } + this.pendingRequests.put(response.id(), false); } @Override @@ -310,7 +340,64 @@ public Object callTool(String name, Map arguments) { if (this.isNotInitialized()) { throw new IllegalStateException("MCP client is not initialized. Please wait a moment."); } - return null; + HttpClassicClientRequest request = + this.client.createRequest(HttpRequestMethod.POST, this.baseUri + this.messageEndpoint); + long currentId = this.getNextId(); + this.responseConsumers.put(currentId, this::callTools0); + this.pendingRequests.put(currentId, true); + JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, + Method.TOOLS_CALL.code(), + MapBuilder.get().put("name", name).put("arguments", arguments).build()); + request.entity(Entity.createObject(request, rpcRequest)); + log.info("Send {} method to MCP server. [sessionId={}, request={}]", + Method.TOOLS_CALL.code(), + this.sessionId, + rpcRequest); + try (HttpClassicClientResponse exchange = request.exchange(Object.class)) { + if (exchange.statusCode() >= 200 && exchange.statusCode() < 300) { + log.info("Send {} method to MCP server successfully. [sessionId={}, statusCode={}]", + Method.TOOLS_CALL.code(), + this.sessionId, + exchange.statusCode()); + } else { + log.error("Failed to {} MCP server. [sessionId={}, statusCode={}]", + Method.TOOLS_CALL.code(), + this.sessionId, + exchange.statusCode()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + while (this.pendingRequests.get(currentId)) { + ThreadUtils.sleep(100); + } + return this.pendingResults.get(currentId); + } + + private void callTools0(JsonRpc.Response response) { + if (response.error() != null) { + log.error("Failed to call tool from MCP server. [sessionId={}, response={}]", this.sessionId, response); + this.pendingRequests.put(response.id(), false); + return; + } + Map result = cast(response.result()); + boolean isError = cast(result.get("isError")); + if (isError) { + log.error("Failed to call tool from MCP server. [sessionId={}, result={}]", this.sessionId, result); + this.pendingRequests.put(response.id(), false); + return; + } + List> rawContents = cast(result.get("content")); + if (CollectionUtils.isEmpty(rawContents)) { + log.error("Failed to call tool from MCP server: no result returned. [sessionId={}, result={}]", + this.sessionId, + result); + this.pendingRequests.put(response.id(), false); + return; + } + Map rawContent = rawContents.get(0); + this.pendingResults.put(response.id(), rawContent.get("text")); + this.pendingRequests.put(response.id(), false); } private long getNextId() { diff --git a/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java index 61412104..f35bb082 100644 --- a/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java +++ b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java @@ -44,7 +44,7 @@ public DefaultMcpClientFactory(HttpClassicClientFactory clientFactory, } @Override - public McpClient create(String connectionString) { - return new DefaultMcpClient(this.jsonSerializer, this.client, connectionString); + public McpClient create(String baseUri, String sseEndpoint) { + return new DefaultMcpClient(this.jsonSerializer, this.client, baseUri, sseEndpoint); } } 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 index 8da9c35e..d62c13b8 100644 --- 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 @@ -6,7 +6,7 @@ package modelengine.fel.tool.mcp.server; -import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.ServerSchema; import modelengine.fel.tool.mcp.entity.Tool; import java.util.List; @@ -20,21 +20,21 @@ */ public interface McpServer { /** - * Gets MCP Server Info. + * Gets MCP server schema. * - * @return The MCP Server Info as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + * @return The MCP server schema as a {@link ServerSchema}. */ - Server getInfo(); + ServerSchema getSchema(); /** - * Gets MCP Server Tools. + * Gets MCP server tools. * - * @return The MCP Server Tools as a {@link List}{@code <}{@link Tool}{@code >}. + * @return The MCP server tools as a {@link List}{@code <}{@link Tool}{@code >}. */ List getTools(); /** - * Calls MCP Server Tool. + * 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 >}. @@ -43,18 +43,18 @@ public interface McpServer { Object callTool(String name, Map arguments); /** - * Registers MCP Server Tools Changed Observer. + * Registers MCP server tools changed observer. * - * @param observer The MCP Server Tools Changed Observer as a {@link ToolsChangedObserver}. + * @param observer The MCP server tools changed observer as a {@link ToolsChangedObserver}. */ void registerToolsChangedObserver(ToolsChangedObserver observer); /** - * Represents the MCP Server Tools Changed Observer. + * Represents the MCP server tools changed observer. */ interface ToolsChangedObserver { /** - * Called when MCP Server Tools changed. + * Called when MCP server tools changed. */ void onToolsChanged(); } diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java index e53838dd..e2b1d5c5 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java @@ -6,7 +6,6 @@ package modelengine.fel.tool.mcp.server; -import static modelengine.fitframework.inspection.Validation.notBlank; import static modelengine.fitframework.inspection.Validation.notNull; import static modelengine.fitframework.util.ObjectUtils.cast; @@ -26,7 +25,6 @@ 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; @@ -61,21 +59,17 @@ public class McpServerController implements McpServer.ToolsChangedObserver { 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 McpServerController(@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."); + public McpServerController(@Fit(alias = "json") ObjectSerializer serializer, McpServer mcpServer) { this.serializer = notNull(serializer, "The json serializer cannot be null."); notNull(mcpServer, "The MCP server cannot be null."); mcpServer.registerToolsChangedObserver(this); @@ -126,7 +120,7 @@ public Choir createSse(HttpClassicServerResponse response) { log.info("New SSE channel for MCP server created. [sessionId={}]", sessionId); return Choir.create(emitter -> { emitters.put(sessionId, emitter); - String data = this.baseUrl + MESSAGE_PATH + "?sessionId=" + sessionId; + String data = MESSAGE_PATH + "?session_id=" + sessionId; TextEvent textEvent = TextEvent.custom().id(sessionId).event(Event.ENDPOINT.code()).data(data).build(); emitter.emit(textEvent); log.info("Send MCP endpoint. [endpoint={}]", data); @@ -144,7 +138,7 @@ public Choir createSse(HttpClassicServerResponse response) { * @return Always returns an empty string ({@value #RESPONSE_OK}) to indicate success. */ @PostMapping(path = MESSAGE_PATH) - public Object receiveMcpMessage(@RequestQuery(name = "sessionId") String sessionId, + public Object receiveMcpMessage(@RequestQuery(name = "session_id") String sessionId, @RequestBody Map request) { log.info("Receive MCP message. [sessionId={}, message={}]", sessionId, request); Object id = request.get("id"); 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 index 0ce01532..8cd9ecd8 100644 --- 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 @@ -35,7 +35,7 @@ public InitializeHandler(McpServer mcpServer) { @Override protected Object handle(InitializeRequest request) { - return this.mcpServer.getInfo(); + return this.mcpServer.getSchema(); } /** 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 index b0b95e9a..4ac08761 100644 --- 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 @@ -8,7 +8,7 @@ import static modelengine.fitframework.inspection.Validation.notNull; -import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.ServerSchema; import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fel.tool.mcp.server.McpServer; import modelengine.fel.tool.service.ToolChangedObserver; @@ -48,20 +48,12 @@ public DefaultMcpServer(ToolExecuteService toolExecuteService) { } @Override - public Server getInfo() { - Server server = new Server(); - server.setProtocolVersion("2025-03-26"); - Server.Capabilities capabilities = new Server.Capabilities(); - server.setCapabilities(capabilities); - Server.Capabilities.Tools tools1 = new Server.Capabilities.Tools(); - capabilities.setTools(tools1); - tools1.setListChanged(true); - capabilities.setLogging(new Server.Capabilities.Logging()); - Server.Info fitStoreMcpServer = new Server.Info(); - server.setServerInfo(fitStoreMcpServer); - fitStoreMcpServer.setName("FIT Store MCP Server"); - fitStoreMcpServer.setVersion("3.5.0-SNAPSHOT"); - return server; + public ServerSchema getSchema() { + ServerSchema.Info info = new ServerSchema.Info("FIT Store MCP Server", "3.5.0-SNAPSHOT"); + ServerSchema.Capabilities.Logging logging = new ServerSchema.Capabilities.Logging(); + ServerSchema.Capabilities.Tools tools = new ServerSchema.Capabilities.Tools(true); + ServerSchema.Capabilities capabilities = new ServerSchema.Capabilities(logging, tools); + return new ServerSchema("2024-11-05", capabilities, info); } @Override @@ -85,7 +77,7 @@ public void registerToolsChangedObserver(ToolsChangedObserver observer) { } @Override - public void onToolAdded(String name, String description, Map schema) { + public void onToolAdded(String name, String description, Map parameters) { if (StringUtils.isBlank(name)) { log.warn("Tool addition is ignored: tool name is blank."); return; @@ -94,16 +86,16 @@ public void onToolAdded(String name, String description, Map sch log.warn("Tool addition is ignored: tool description is blank. [toolName={}]", name); return; } - if (MapUtils.isEmpty(schema)) { + if (MapUtils.isEmpty(parameters)) { log.warn("Tool addition is ignored: tool schema is null or empty. [toolName={}]", name); return; } Tool tool = new Tool(); tool.setName(name); tool.setDescription(description); - tool.setInputSchema(schema); + tool.setInputSchema(parameters); this.tools.put(name, tool); - log.info("Tool added to MCP server. [toolName={}, description={}, schema={}]", name, description, schema); + log.info("Tool added to MCP server. [toolName={}, description={}, schema={}]", name, description, parameters); this.toolsChangedObservers.forEach(ToolsChangedObserver::onToolsChanged); } diff --git a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java index fd75a33d..068e4671 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java +++ b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java @@ -27,37 +27,21 @@ public class McpServerControllerTest { private ObjectSerializer objectSerializer; private McpServer mcpServer; - private String baseUrl; @BeforeEach void setup() { this.objectSerializer = mock(ObjectSerializer.class); this.mcpServer = mock(McpServer.class); - this.baseUrl = "http://localhost:8080"; } @Nested @DisplayName("Constructor Tests") class GivenConstructor { - @Test - @DisplayName("Should throw exception when base URL is null or blank") - void shouldThrowExceptionWhenBaseUrlIsNullOrEmpty() { - // Null - var exception1 = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpServerController(null, objectSerializer, mcpServer)); - assertThat(exception1).hasMessage("The base URL for MCP server cannot be blank."); - - // Blank - var exception2 = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpServerController("", objectSerializer, mcpServer)); - assertThat(exception2).hasMessage("The base URL for MCP server cannot be blank."); - } - @Test @DisplayName("Should throw exception when serializer is null") void shouldThrowExceptionWhenSerializerIsNull() { var exception = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpServerController(baseUrl, null, mcpServer)); + () -> new McpServerController(null, mcpServer)); assertThat(exception).hasMessage("The json serializer cannot be null."); } @@ -65,7 +49,7 @@ void shouldThrowExceptionWhenSerializerIsNull() { @DisplayName("Should throw exception when mcpServer is null") void shouldThrowExceptionWhenMcpServerIsNull() { var exception = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpServerController(baseUrl, objectSerializer, null)); + () -> new McpServerController(objectSerializer, null)); assertThat(exception).hasMessage("The MCP server cannot be null."); } } diff --git a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServerTest.java b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServerTest.java index e4c9af1e..f23e187e 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServerTest.java +++ b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServerTest.java @@ -16,7 +16,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.ServerSchema; import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fel.tool.mcp.server.McpServer; import modelengine.fel.tool.service.ToolExecuteService; @@ -64,19 +64,19 @@ class GivenGetInfo { @DisplayName("Should return expected server information") void returnExpectedServerInfo() { McpServer server = new DefaultMcpServer(toolExecuteService); - Server info = server.getInfo(); + ServerSchema info = server.getSchema(); - assertThat(info).returns("2025-03-26", Server::getProtocolVersion); + assertThat(info).returns("2024-11-05", ServerSchema::protocolVersion); - Server.Capabilities capabilities = info.getCapabilities(); + ServerSchema.Capabilities capabilities = info.capabilities(); assertThat(capabilities).isNotNull(); - Server.Capabilities.Tools toolsCapability = capabilities.getTools(); - assertThat(toolsCapability).returns(true, Server.Capabilities.Tools::isListChanged); + ServerSchema.Capabilities.Tools toolsCapability = capabilities.tools(); + assertThat(toolsCapability).returns(true, ServerSchema.Capabilities.Tools::listChanged); - Server.Info serverInfo = info.getServerInfo(); - assertThat(serverInfo).returns("FIT Store MCP Server", Server.Info::getName) - .returns("3.5.0-SNAPSHOT", Server.Info::getVersion); + ServerSchema.Info serverInfo = info.serverInfo(); + assertThat(serverInfo).returns("FIT Store MCP Server", ServerSchema.Info::name) + .returns("3.5.0-SNAPSHOT", ServerSchema.Info::version); } } diff --git a/framework/fel/java/plugins/tool-mcp-test/pom.xml b/framework/fel/java/plugins/tool-mcp-test/pom.xml index dfdf0864..c2824385 100644 --- a/framework/fel/java/plugins/tool-mcp-test/pom.xml +++ b/framework/fel/java/plugins/tool-mcp-test/pom.xml @@ -35,6 +35,10 @@ org.fitframework.fel tool-mcp-client-service + + org.fitframework.fel + tool-service + @@ -56,14 +60,23 @@ + + org.fitframework.fel + tool-maven-plugin + ${fit.version} + + + build-tool + + build-tool + + + + org.fitframework fit-build-maven-plugin ${fit.version} - - system - 5 - build-plugin diff --git a/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java index 26d8aab5..a43a186f 100644 --- a/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java +++ b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java @@ -10,9 +10,14 @@ import modelengine.fel.tool.mcp.client.McpClientFactory; import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fit.http.annotation.GetMapping; +import modelengine.fit.http.annotation.PostMapping; +import modelengine.fit.http.annotation.RequestBody; +import modelengine.fit.http.annotation.RequestMapping; +import modelengine.fit.http.annotation.RequestQuery; import modelengine.fitframework.annotation.Component; import java.util.List; +import java.util.Map; /** * Represents a test controller for interacting with the MCP (Model Communication Protocol) client. @@ -22,6 +27,7 @@ * @since 2025-05-21 */ @Component +@RequestMapping(path = "/mcp-test") public class TestController { private final McpClientFactory mcpClientFactory; private McpClient client; @@ -36,15 +42,40 @@ public TestController(McpClientFactory mcpClientFactory) { } /** - * Handles the HTTP GET request to "/test/mcp". - * Initializes the MCP client and retrieves a list of available tools from the server. + * Initializes the MCP client by creating an instance using the provided factory and initializing it. + * This method sets up the connection to the MCP server and prepares it for further interactions. * - * @return A list of tools retrieved from the MCP server. + * @return A string indicating that the initialization was successful. */ - @GetMapping(path = "/test/mcp") - public List testMcp() { - this.client = this.mcpClientFactory.create("http://localhost:8080/sse"); + @PostMapping(path = "/initialize") + public String initialize(@RequestQuery(name = "baseUri") String baseUri, + @RequestQuery(name = "sseEndpoint") String sseEndpoint) { + this.client = this.mcpClientFactory.create(baseUri, sseEndpoint); this.client.initialize(); + return "Initialized"; + } + + /** + * Retrieves a list of available tools from the MCP server. + * This method calls the MCP client to fetch the list of tools and returns it to the caller. + * + * @return A list of {@link Tool} objects representing the available tools. + */ + @GetMapping(path = "/tools/list") + public List toolsList() { return this.client.getTools(); } + + /** + * Calls a specific tool with the given name and JSON arguments. + * This method invokes the specified tool on the MCP server and returns the result. + * + * @param name The name of the tool to be called. + * @param jsonArgs The JSON arguments to be passed to the tool. + * @return The result of the tool execution. + */ + @PostMapping(path = "/tools/call") + public Object toolsCall(@RequestQuery(name = "name") String name, @RequestBody Map jsonArgs) { + return this.client.callTool(name, jsonArgs); + } } \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherService.java b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherService.java new file mode 100644 index 00000000..4074bd9c --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherService.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024-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.test.tool; + +import modelengine.fel.tool.annotation.Group; +import modelengine.fel.tool.annotation.ToolMethod; +import modelengine.fitframework.annotation.Genericable; +import modelengine.fitframework.annotation.Property; + +/** + * 表示天气服务的接口定义。 + * + * @author 易文渊 + * @author 杭潇 + * @since 2024-09-02 + */ +@Group(name = "weather_service") +public interface WeatherService { + /** + * 获取指定地点的当前温度。 + * + * @param location 表示地点名称的 {@link String}。 + * @param unit 表示温度单位的 {@link String}。 + * @return 表示当前温度的 {@link String}。 + */ + @ToolMethod(namespace = "example", name = "current_temperature", description = "获取指定地点的当前温度") + @Genericable("modelengine.example.weather.temperature") + String getCurrentTemperature(@Property(description = "城市名称", required = true) String location, + @Property(description = "使用的温度单位,可选:Celsius,Fahrenheit", defaultValue = "Celsius") String unit); + + /** + * 获取指定地点的降雨概率。 + * + * @param location 表示地点名称的 {@link String}。 + * @return 表示降雨概率的 {@link String}。 + */ + @ToolMethod(namespace = "example", name = "rain_probability", description = "获取指定地点的降雨概率") + @Genericable("modelengine.example.weather.rain") + String getRainProbability(@Property(description = "城市名称", required = true) String location); +} \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherServiceImpl.java b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherServiceImpl.java new file mode 100644 index 00000000..353562a3 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/tool/WeatherServiceImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024-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.test.tool; + +import modelengine.fel.tool.annotation.Attribute; +import modelengine.fel.tool.annotation.Group; +import modelengine.fel.tool.annotation.ToolMethod; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.annotation.Fitable; +import modelengine.fitframework.annotation.Property; + +/** + * 表示 {@link WeatherService} 的默认实现。 + * + * @author 易文渊 + * @author 杭潇 + * @since 2024-09-02 + */ +@Component +@Group(name = "default_weather_service") +public class WeatherServiceImpl implements WeatherService { + @Override + @Fitable("default") + @ToolMethod(name = "get_current_temperature", description = "获取指定城市的当前温度", + extensions = { + @Attribute(key = "tags", value = "FIT"), @Attribute(key = "tags", value = "TEST"), + @Attribute(key = "attribute", value = "nothing"), + @Attribute(key = "attribute", value = "nothing two") + }) + @Property(description = "当前温度的结果") + public String getCurrentTemperature(String location, String unit) { + return "26"; + } + + @Override + @Fitable("default") + @ToolMethod(name = "get_rain_probability", description = "获取指定城市下雨的概率") + @Property(description = "下雨的概率") + public String getRainProbability(String location) { + return "0.06"; + } +} \ 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 5da4f4fe..ae9bd773 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 @@ -8,6 +8,7 @@ import static modelengine.fitframework.inspection.Validation.notBlank; import static modelengine.fitframework.inspection.Validation.notNull; +import static modelengine.fitframework.util.ObjectUtils.cast; import modelengine.fel.core.tool.ToolInfo; import modelengine.fel.tool.ToolInfoEntity; @@ -55,7 +56,8 @@ public void addTool(ToolInfoEntity tool) { String uniqueName = ToolInfo.identify(tool); this.toolCache.put(uniqueName, tool); log.info("Register tool[uniqueName={}] success.", uniqueName); - this.toolChangedObserver.onToolAdded(uniqueName, tool.description(), tool.schema()); + Map parameters = cast(tool.schema().get("parameters")); + this.toolChangedObserver.onToolAdded(uniqueName, tool.description(), parameters); } @Override diff --git a/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java b/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java index 1956b909..d52c639d 100644 --- a/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java +++ b/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java @@ -18,8 +18,9 @@ public interface McpClientFactory { /** * Creates a {@link McpClient} instance. * - * @param connectionString The connection {@link String}. + * @param baseUri The base URI of the MCP server. + * @param sseEndpoint The SSE endpoint of the MCP server. * @return The connected {@link McpClient} instance. */ - McpClient create(String connectionString); + McpClient create(String baseUri, String sseEndpoint); } \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ClientSchema.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ClientSchema.java new file mode 100644 index 00000000..692c8369 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ClientSchema.java @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * 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.entity; + +/** + * Represents a client entity in the MCP framework, encapsulating information about the client's protocol version, + * capabilities, and additional client details. + * + * @author 季聿阶 + * @since 2025-05-22 + */ +public record ClientSchema(String protocolVersion, Capabilities capabilities, Info clientInfo) { + /** + * Represents the capabilities supported by the client. + */ + public record Capabilities() {} + + /** + * Represents additional information about the client, such as its name and version. + */ + public record Info(String name, String version) {} +} \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java index 1e0e84d5..61c27163 100644 --- a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java @@ -37,7 +37,7 @@ public static Request createRequest(T id, String method) { * @param The type of the request ID. * @return A JSON RPC request object. */ - public static Request createRequest(T id, String method, Map params) { + public static Request createRequest(T id, String method, Object params) { return new Request<>("2.0", id, method, params); } @@ -91,7 +91,7 @@ public static Notification createNotification(String method, Map * * @param The type of the request ID. */ - public record Request(String jsonrpc, T id, String method, Map params) {} + public record Request(String jsonrpc, T id, String method, Object params) {} /** * Represents a JSON RPC response. diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java deleted file mode 100644 index 9700c823..00000000 --- a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java +++ /dev/null @@ -1,213 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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.entity; - -/** - * Represents a server entity in the MCP framework, encapsulating information about the server's protocol version, - * capabilities, and additional server details. - * - * @author 季聿阶 - * @since 2025-05-22 - */ -public class Server { - private String protocolVersion; - private Capabilities capabilities; - private Info serverInfo; - - /** - * Returns the protocol version used by the server. - * - * @return The protocol version. - */ - public String getProtocolVersion() { - return this.protocolVersion; - } - - /** - * Sets the protocol version used by the server. - * - * @param protocolVersion The protocol version to set. - */ - public void setProtocolVersion(String protocolVersion) { - this.protocolVersion = protocolVersion; - } - - /** - * Returns the capabilities supported by the server. - * - * @return The server capabilities. - */ - public Capabilities getCapabilities() { - return this.capabilities; - } - - /** - * Sets the capabilities supported by the server. - * - * @param capabilities The server capabilities to set. - */ - public void setCapabilities(Capabilities capabilities) { - this.capabilities = capabilities; - } - - /** - * Returns additional information about the server. - * - * @return The server information. - */ - public Info getServerInfo() { - return this.serverInfo; - } - - /** - * Sets additional information about the server. - * - * @param serverInfo The server information to set. - */ - public void setServerInfo(Info serverInfo) { - this.serverInfo = serverInfo; - } - - @Override - public String toString() { - return "Server{" + "protocolVersion='" + protocolVersion + '\'' + ", capabilities=" + capabilities - + ", serverInfo=" + serverInfo + '}'; - } - - /** - * Represents the capabilities supported by the server, including logging and tool-related functionalities. - */ - public static class Capabilities { - private Logging logging; - private Tools tools; - - /** - * Returns the logging capabilities of the server. - * - * @return The logging capabilities. - */ - public Logging getLogging() { - return this.logging; - } - - /** - * Sets the logging capabilities of the server. - * - * @param logging The logging capabilities to set. - */ - public void setLogging(Logging logging) { - this.logging = logging; - } - - /** - * Returns the tool-related capabilities of the server. - * - * @return The tool-related capabilities. - */ - public Tools getTools() { - return this.tools; - } - - /** - * Sets the tool-related capabilities of the server. - * - * @param tools The tool-related capabilities to set. - */ - public void setTools(Tools tools) { - this.tools = tools; - } - - @Override - public String toString() { - return "Capabilities{" + "logging=" + logging + ", tools=" + tools + '}'; - } - - /** - * Represents the logging capabilities of the server. - */ - public static class Logging {} - - /** - * Represents the tool-related capabilities of the server, including whether the tool list has changed. - */ - public static class Tools { - private boolean listChanged; - - /** - * Returns whether the tool list has changed. - * - * @return True if the tool list has changed, false otherwise. - */ - public boolean isListChanged() { - return this.listChanged; - } - - /** - * Sets whether the tool list has changed. - * - * @param listChanged The change status of the tool list. - */ - public void setListChanged(boolean listChanged) { - this.listChanged = listChanged; - } - - @Override - public String toString() { - return "Tools{" + "listChanged=" + listChanged + '}'; - } - } - } - - /** - * Represents additional information about the server, such as its name and version. - */ - public static class Info { - private String name; - private String version; - - /** - * Returns the name of the server. - * - * @return The server name. - */ - public String getName() { - return this.name; - } - - /** - * Sets the name of the server. - * - * @param name The server name to set. - */ - public void setName(String name) { - this.name = name; - } - - /** - * Returns the version of the server. - * - * @return The server version. - */ - public String getVersion() { - return this.version; - } - - /** - * Sets the version of the server. - * - * @param version The server version to set. - */ - public void setVersion(String version) { - this.version = version; - } - - @Override - public String toString() { - return "Info{" + "name='" + name + '\'' + ", version='" + version + '\'' + '}'; - } - } -} \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ServerSchema.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ServerSchema.java new file mode 100644 index 00000000..7125c560 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/ServerSchema.java @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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.entity; + +import static modelengine.fitframework.util.ObjectUtils.cast; + +import java.util.Map; + +/** + * Represents a server entity in the MCP framework, encapsulating information about the server's protocol version, + * capabilities, and additional server details. + * + * @author 季聿阶 + * @since 2025-05-22 + */ +public record ServerSchema(String protocolVersion, Capabilities capabilities, Info serverInfo) { + /** + * Creates a new {@link ServerSchema} instance based on the provided map of server information. + * + * @param map The map containing server information. + * @return A new {@link ServerSchema} instance. + */ + public static ServerSchema create(Map map) { + String protocolVersion = cast(map.get("protocolVersion")); + Map capabilitiesMap = cast(map.get("capabilities")); + Capabilities.Logging logging = new Capabilities.Logging(); + Map toolsMap = cast(capabilitiesMap.get("tools")); + boolean toolsListChanged = cast(toolsMap.getOrDefault("listChanged", false)); + Capabilities.Tools tools = new Capabilities.Tools(toolsListChanged); + Capabilities capabilities = new Capabilities(logging, tools); + Map infoMap = cast(map.get("serverInfo")); + String name = cast(infoMap.get("name")); + String version = cast(infoMap.get("version")); + Info serverInfo = new Info(name, version); + return new ServerSchema(protocolVersion, capabilities, serverInfo); + } + + /** + * Represents the capabilities supported by the server, including logging and tool-related functionalities. + */ + public record Capabilities(Logging logging, Tools tools) { + /** + * Represents the logging capabilities of the server. + */ + public record Logging() {} + + /** + * Represents the tool-related capabilities of the server, including whether the tool list has changed. + */ + public record Tools(boolean listChanged) {} + } + + /** + * Represents additional information about the server, such as its name and version. + */ + public record Info(String name, String version) {} +} \ No newline at end of file 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 index a9a71616..64aa8891 100644 --- 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 @@ -20,10 +20,10 @@ public interface ToolChangedObserver { * * @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 + * @param parameters The parameters associated with the added tool, as a * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. */ - void onToolAdded(String name, String description, Map schema); + void onToolAdded(String name, String description, Map parameters); /** * Method called when a tool has been removed. diff --git a/framework/fit/java/fit-builtin/plugins/fit-http-client-okhttp/src/main/java/modelengine/fit/http/client/okhttp/OkHttpClientRequest.java b/framework/fit/java/fit-builtin/plugins/fit-http-client-okhttp/src/main/java/modelengine/fit/http/client/okhttp/OkHttpClientRequest.java index 44e33288..52716ddc 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-http-client-okhttp/src/main/java/modelengine/fit/http/client/okhttp/OkHttpClientRequest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-http-client-okhttp/src/main/java/modelengine/fit/http/client/okhttp/OkHttpClientRequest.java @@ -13,6 +13,7 @@ import modelengine.fit.http.protocol.ConfigurableMessageHeaders; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.WritableMessageBody; import modelengine.fit.http.protocol.support.ClientRequestBody; @@ -115,7 +116,10 @@ public ClientResponse readResponse() throws IOException { @Override public RequestLine startLine() { - return RequestLine.create(HttpVersion.HTTP_1_1, this.method, this.url.getPath()); + return RequestLine.create(HttpVersion.HTTP_1_1, + this.method, + this.url.getPath(), + QueryCollection.create(this.url.getQuery())); } @Override diff --git a/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/DefaultReflectibleHttpHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/DefaultReflectibleHttpHandlerTest.java index d8f67e5c..3e8e854b 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/DefaultReflectibleHttpHandlerTest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/DefaultReflectibleHttpHandlerTest.java @@ -23,6 +23,7 @@ import modelengine.fit.http.protocol.HttpResponseStatus; import modelengine.fit.http.protocol.HttpVersion; import modelengine.fit.http.protocol.MessageHeaders; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.ServerRequest; import modelengine.fit.http.protocol.ServerResponse; @@ -98,7 +99,10 @@ void teardown() throws IOException { private void initializeRequest() { HttpResource httpResource = mock(HttpResource.class); this.serverRequest = mock(ServerRequest.class); - RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, HttpRequestMethod.CONNECT, "testUri"); + RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, + HttpRequestMethod.CONNECT, + "testUri", + QueryCollection.create()); MessageHeaders headers = new DefaultMessageHeaders(); when(this.serverRequest.startLine()).thenReturn(startLine); when(this.serverRequest.headers()).thenReturn(headers); diff --git a/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/support/DefaultHttpExceptionHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/support/DefaultHttpExceptionHandlerTest.java index 90a05ca3..aee56526 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/support/DefaultHttpExceptionHandlerTest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-http-handler-registry/src/test/java/modelengine/fit/http/server/handler/support/DefaultHttpExceptionHandlerTest.java @@ -17,6 +17,7 @@ import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; import modelengine.fit.http.protocol.MessageHeaders; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.ServerRequest; import modelengine.fit.http.protocol.ServerResponse; @@ -82,7 +83,10 @@ void teardown() throws IOException { private void initializeRequest() { HttpResource httpResource = mock(HttpResource.class); this.serverRequest = mock(ServerRequest.class); - RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, HttpRequestMethod.CONNECT, "testUri"); + RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, + HttpRequestMethod.CONNECT, + "testUri", + QueryCollection.create()); MessageHeaders headers = new DefaultMessageHeaders(); when(this.serverRequest.startLine()).thenReturn(startLine); when(this.serverRequest.headers()).thenReturn(headers); 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 52e292c4..05904611 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 @@ -18,6 +18,7 @@ import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; import modelengine.fit.http.protocol.MessageHeaders; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.ReadableMessageBody; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.ServerRequest; @@ -38,6 +39,8 @@ * @since 2022-07-08 */ public class NettyHttpServerRequest implements ServerRequest, OnHttpContentReceived { + private static final char QUERY_SEPARATOR = '?'; + private final HttpRequest request; private final ChannelHandlerContext ctx; private final boolean isSecure; @@ -73,7 +76,15 @@ private RequestLine initStartLine() { HttpVersion httpVersion = notNull(HttpVersion.from(this.request.protocolVersion().toString()), "The http version is unsupported. [version={0}]", this.request.protocolVersion()); - return RequestLine.create(httpVersion, method, this.request.uri()); + int index = this.request.uri().indexOf(QUERY_SEPARATOR); + if (index < 0) { + return RequestLine.create(httpVersion, method, this.request.uri(), QueryCollection.create()); + } else { + return RequestLine.create(httpVersion, + method, + this.request.uri().substring(0, index), + QueryCollection.create(this.request.uri().substring(index + 1))); + } } private MessageHeaders initHeaders() { diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/HttpClassicRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/HttpClassicRequest.java index 92a64cb4..49da7c60 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/HttpClassicRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/HttpClassicRequest.java @@ -7,6 +7,7 @@ package modelengine.fit.http; import modelengine.fit.http.protocol.HttpRequestMethod; +import modelengine.fit.http.protocol.QueryCollection; /** * 表示经典的 Http 请求。 diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java index 0dd09b22..a7d0c0fc 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/AbstractHttpClassicRequest.java @@ -11,10 +11,10 @@ import modelengine.fit.http.HttpClassicRequest; import modelengine.fit.http.HttpResource; -import modelengine.fit.http.QueryCollection; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.MessageHeaderNames; import modelengine.fit.http.protocol.MessageHeaders; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; /** @@ -24,10 +24,7 @@ * @since 2022-11-23 */ public abstract class AbstractHttpClassicRequest extends AbstractHttpMessage implements HttpClassicRequest { - private static final char QUERY_SEPARATOR = '?'; - private final RequestLine startLine; - private final QueryCollection queries; private final MessageHeaders headers; /** @@ -40,20 +37,9 @@ public abstract class AbstractHttpClassicRequest extends AbstractHttpMessage imp public AbstractHttpClassicRequest(HttpResource httpResource, RequestLine startLine, MessageHeaders headers) { super(httpResource, startLine, headers); this.startLine = notNull(startLine, "The request line cannot be null."); - this.queries = this.initQueries(); this.headers = notNull(headers, "The message headers cannot be null."); } - private QueryCollection initQueries() { - String requestUri = this.startLine.requestUri(); - int index = requestUri.indexOf(QUERY_SEPARATOR); - if (index < 0) { - return QueryCollection.create(); - } else { - return QueryCollection.create(requestUri.substring(index + 1)); - } - } - @Override public HttpRequestMethod method() { return this.headers.first(MessageHeaderNames.X_HTTP_METHOD_OVERRIDE) @@ -73,17 +59,11 @@ public String host() { @Override public String path() { - String requestUri = this.startLine.requestUri(); - int index = requestUri.indexOf(QUERY_SEPARATOR); - if (index < 0) { - return requestUri; - } else { - return requestUri.substring(0, index); - } + return this.startLine.requestUri(); } @Override public QueryCollection queries() { - return this.queries; + return this.startLine.queries(); } } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java index 47c02a22..43fd215d 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/HttpUtils.java @@ -12,6 +12,7 @@ import modelengine.fit.http.header.ParameterCollection; import modelengine.fit.http.header.support.DefaultHeaderValue; import modelengine.fit.http.header.support.DefaultParameterCollection; +import modelengine.fit.http.protocol.util.QueryUtils; import modelengine.fitframework.model.MultiValueMap; import modelengine.fitframework.resource.UrlUtils; import modelengine.fitframework.util.StringUtils; @@ -23,7 +24,6 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; -import java.util.function.Function; /** * Http 协议相关的工具类。 @@ -33,8 +33,7 @@ * @since 2022-07-22 */ public class HttpUtils { - private static final char KEY_VALUE_PAIR_SEPARATOR = '&'; - private static final char KEY_VALUE_SEPARATOR = '='; + private static final char STRING_VALUE_SURROUNDED = '\"'; /** @@ -79,18 +78,6 @@ private static ParameterCollection parseParameters(List parameterStrings return parameterCollection; } - /** - * 将 Http 查询参数的内容解析成为一个键和多值的映射。 - *

该映射的实现默认为 {@link LinkedHashMap},即键是有序的。查询参数的样式为 {@code k1=v1&k2=v2}。

- * - * @param keyValues 表示待解析的查询参数或表单参数的 {@link String}。 - * @return 表示解析后的键与多值的映射的 {@link MultiValueMap}{@code <}{@link String}{@code , - * }{@link String}{@code >}。 - */ - public static MultiValueMap parseQuery(String keyValues) { - return HttpUtils.parseQueryOrForm(keyValues, UrlUtils::decodePath); - } - /** * 将 Http 表单参数的内容解析成为一个键和多值的映射。 *

该映射的实现默认为 {@link LinkedHashMap},即键是有序的。表单参数的样式为 {@code k1=v1&k2=v2}。

@@ -100,27 +87,7 @@ public static MultiValueMap parseQuery(String keyValues) { * }{@link String}{@code >}。 */ public static MultiValueMap parseForm(String keyValues) { - return HttpUtils.parseQueryOrForm(keyValues, UrlUtils::decodeForm); - } - - private static MultiValueMap parseQueryOrForm(String keyValues, - Function decodeMethod) { - MultiValueMap map = MultiValueMap.create(LinkedHashMap::new); - if (StringUtils.isBlank(keyValues)) { - return map; - } - List keyValuePairs = - StringUtils.split(keyValues, KEY_VALUE_PAIR_SEPARATOR, ArrayList::new, StringUtils::isNotBlank); - for (String keyValuePair : keyValuePairs) { - int index = keyValuePair.indexOf(KEY_VALUE_SEPARATOR); - if (index <= 0) { - continue; - } - String key = decodeMethod.apply(keyValuePair.substring(0, index)); - String value = decodeMethod.apply(keyValuePair.substring(index + 1)); - map.add(key, value); - } - return map; + return QueryUtils.parseQuery(keyValues, UrlUtils::decodeForm); } /** diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/HttpClassicClientTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/HttpClassicClientTest.java index f75ebef3..b3f5ae90 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/HttpClassicClientTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/HttpClassicClientTest.java @@ -19,6 +19,7 @@ import modelengine.fit.http.protocol.ConfigurableMessageHeaders; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.support.DefaultClientResponse; import modelengine.fit.http.protocol.support.DefaultMessageHeaders; @@ -64,8 +65,10 @@ void setup() { this.headers.add("Content-Length", "23"); ConfigurableMessageHeaders defaultMessageHeaders = new DefaultMessageHeaders(); when(this.clientRequest.headers()).thenReturn(defaultMessageHeaders); - RequestLine startLine = - new DefaultRequestLine(HttpVersion.HTTP_1_0, HttpRequestMethod.CONNECT, "requestUri"); + RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, + HttpRequestMethod.CONNECT, + "requestUri", + QueryCollection.create()); when(this.clientRequest.startLine()).thenReturn(startLine); } diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequestTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequestTest.java index d6bc7cd2..f0546719 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequestTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/client/support/DefaultHttpClassicClientRequestTest.java @@ -12,7 +12,6 @@ import modelengine.fit.http.HttpMessage; import modelengine.fit.http.HttpResource; -import modelengine.fit.http.QueryCollection; import modelengine.fit.http.client.HttpClassicClientResponse; import modelengine.fit.http.entity.Entity; import modelengine.fit.http.entity.FileEntity; @@ -22,6 +21,7 @@ import modelengine.fit.http.protocol.ConfigurableMessageHeaders; import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.support.DefaultClientResponse; import modelengine.fit.http.protocol.support.DefaultMessageHeaders; @@ -61,13 +61,16 @@ void setup() throws IOException { this.headers.add("Content-Length", "30"); ConfigurableMessageHeaders defaultMessageHeaders = new DefaultMessageHeaders(); when(this.clientRequest.headers()).thenReturn(defaultMessageHeaders); - RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, HttpRequestMethod.CONNECT, "requestUri"); + RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, + HttpRequestMethod.CONNECT, + "requestUri", + QueryCollection.create()); when(this.clientRequest.startLine()).thenReturn(startLine); String reasonPhrase = "testHttpClientErrorException"; ClientResponse clientResponse = new DefaultClientResponse(200, reasonPhrase, this.headers, this.responseStream); when(this.clientRequest.readResponse()).thenReturn(clientResponse); - this.defaultHttpClassicClientRequest = new DefaultHttpClassicClientRequest(this.httpResource, - this.clientRequest); + this.defaultHttpClassicClientRequest = + new DefaultHttpClassicClientRequest(this.httpResource, this.clientRequest); } @AfterEach @@ -222,24 +225,6 @@ void theQueriesIsEmpty() { assertThat(queries.queryString()).isEmpty(); } - @Nested - @DisplayName("Uri 值中包含查询分隔符") - class UriContainsTheQuerySeparator { - @Test - @DisplayName("获取的路径值与给定值相等") - void thePathIsEqualsToTheGivenValue() { - RequestLine startLine = new DefaultRequestLine(HttpVersion.HTTP_1_0, - HttpRequestMethod.CONNECT, - "testRequestUri?elseRequestUri"); - when(DefaultHttpClassicClientRequestTest.this.clientRequest.startLine()).thenReturn(startLine); - DefaultHttpClassicClientRequestTest.this.defaultHttpClassicClientRequest = - new DefaultHttpClassicClientRequest(DefaultHttpClassicClientRequestTest.this.httpResource, - DefaultHttpClassicClientRequestTest.this.clientRequest); - String path = DefaultHttpClassicClientRequestTest.this.defaultHttpClassicClientRequest.path(); - assertThat(path).isEqualTo("testRequestUri"); - } - } - @Test @DisplayName("获取的 Http 版本与给定的值相等") void theHttpVersionIsEqualsToTheGivenVersion() { diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/server/handler/MockHttpClassicServerRequest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/server/handler/MockHttpClassicServerRequest.java index c46cf5e8..d029e506 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/server/handler/MockHttpClassicServerRequest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/server/handler/MockHttpClassicServerRequest.java @@ -17,6 +17,7 @@ import modelengine.fit.http.entity.EntitySerializer; import modelengine.fit.http.entity.support.DefaultNamedEntity; import modelengine.fit.http.protocol.MessageHeaderNames; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.ReadableMessageBody; import modelengine.fit.http.protocol.RequestLine; import modelengine.fit.http.protocol.ServerRequest; @@ -72,7 +73,8 @@ public MockHttpClassicServerRequest() { when(serverRequest.headers()).thenReturn(headers); ReadableMessageBody body = mock(ReadableMessageBody.class); when(serverRequest.body()).thenReturn(body); - when(startLine.requestUri()).thenReturn("?" + URI_KEY + "=" + URI_VALUE); + when(startLine.requestUri()).thenReturn(""); + when(startLine.queries()).thenReturn(QueryCollection.create(URI_KEY + "=" + URI_VALUE)); HttpResource httpResource = mock(HttpResource.class); final Serializers serializers = mock(Serializers.class); final EntitySerializer entitySerializer = mock(EntitySerializer.class); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/support/DefaultQueryCollectionTest.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/support/DefaultQueryCollectionTest.java index 9a9b720b..cf8819ee 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/support/DefaultQueryCollectionTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/test/java/modelengine/fit/http/support/DefaultQueryCollectionTest.java @@ -8,8 +8,9 @@ import static org.assertj.core.api.Assertions.assertThat; -import modelengine.fit.http.QueryCollection; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.header.ConfigurableCookieCollection; +import modelengine.fit.http.protocol.support.DefaultQueryCollection; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/QueryCollection.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/QueryCollection.java similarity index 96% rename from framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/QueryCollection.java rename to framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/QueryCollection.java index ba1eb5d7..f02821ab 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/QueryCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/QueryCollection.java @@ -4,9 +4,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -package modelengine.fit.http; +package modelengine.fit.http.protocol; -import modelengine.fit.http.support.DefaultQueryCollection; +import modelengine.fit.http.protocol.support.DefaultQueryCollection; import java.util.List; import java.util.Optional; diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/RequestLine.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/RequestLine.java index 30901483..0061085a 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/RequestLine.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/RequestLine.java @@ -9,7 +9,7 @@ import modelengine.fit.http.protocol.support.DefaultRequestLine; /** - * 表示 Http 请求的起始行。 + * Represents the start line of an HTTP request. * * @author 季聿阶 * @see RFC 2616 @@ -17,33 +17,43 @@ */ public interface RequestLine extends StartLine { /** - * 获取 Http 请求的方法。 + * Retrieves the HTTP request method. * - * @return 表示 Http 请求方法的 {@link HttpRequestMethod}。 + * @return The HTTP request method as a {@link HttpRequestMethod}. * @see RFC 2616 */ HttpRequestMethod method(); /** - * 获取 Http 请求的 URI。 + * Retrieves the URI of the HTTP request. * - * @return 表示 Http 请求 URI 的 {@link String}。 + * @return The URI of the HTTP request as a {@link String}. * @see RFC 2616 */ String requestUri(); /** - * 根据 Http 版本、请求方法和请求地址,创建一个新的请求起始行。 + * Retrieves the query parameters of the HTTP request. * - * @param httpVersion 表示 Http 版本的 {@link HttpVersion}。 - * @param method 表示请求方法的 {@link HttpRequestMethod}。 - * @param requestUri 表示请求地址的 {@link String}。 - * @return 表示创建出来的新的请求起始行的 {@link RequestLine}。 - * @throws IllegalArgumentException 当 {@code httpVersion} 为 {@code null} 时。 - * @throws IllegalArgumentException 当 {@code method} 为 {@code null} 时。 - * @throws IllegalArgumentException 当 {@code requestUri} 为 {@code null} 或空白字符串时。 + * @return The query parameters of the HTTP request as a {@link QueryCollection}. + * @see RFC 2616 + */ + QueryCollection queries(); + + /** + * Creates a new request line with the specified HTTP version, method, request URI, and query parameters. + * + * @param httpVersion The HTTP version as a {@link HttpVersion}. + * @param method The request method as a {@link HttpRequestMethod}. + * @param requestUri The request URI as a {@link String}. + * @param queries The query parameters as a {@link QueryCollection}. + * @return A new instance of {@link RequestLine}. + * @throws IllegalArgumentException If {@code httpVersion} is {@code null}. + * @throws IllegalArgumentException If {@code method} is {@code null}. + * @throws IllegalArgumentException If {@code requestUri} is {@code null} or a blank string. */ - static RequestLine create(HttpVersion httpVersion, HttpRequestMethod method, String requestUri) { - return new DefaultRequestLine(httpVersion, method, requestUri); + static RequestLine create(HttpVersion httpVersion, HttpRequestMethod method, String requestUri, + QueryCollection queries) { + return new DefaultRequestLine(httpVersion, method, requestUri, queries); } -} +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultQueryCollection.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultQueryCollection.java similarity index 91% rename from framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultQueryCollection.java rename to framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultQueryCollection.java index 8a974687..b6db3084 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/support/DefaultQueryCollection.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultQueryCollection.java @@ -4,10 +4,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -package modelengine.fit.http.support; +package modelengine.fit.http.protocol.support; -import modelengine.fit.http.QueryCollection; -import modelengine.fit.http.util.HttpUtils; +import modelengine.fit.http.protocol.QueryCollection; +import modelengine.fit.http.protocol.util.QueryUtils; import modelengine.fitframework.model.MultiValueMap; import modelengine.fitframework.util.StringUtils; @@ -39,7 +39,7 @@ public DefaultQueryCollection() { * @param queryString 表示整个查询参数的 {@link String}。 */ public DefaultQueryCollection(String queryString) { - this.queries = HttpUtils.parseQuery(queryString); + this.queries = QueryUtils.parseQuery(queryString); } @Override diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultRequestLine.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultRequestLine.java index c9584c09..271d12f8 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultRequestLine.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/support/DefaultRequestLine.java @@ -10,6 +10,7 @@ import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; /** @@ -22,11 +23,14 @@ public class DefaultRequestLine implements RequestLine { private final HttpVersion httpVersion; private final HttpRequestMethod method; private final String requestUri; + private final QueryCollection queries; - public DefaultRequestLine(HttpVersion httpVersion, HttpRequestMethod method, String requestUri) { + public DefaultRequestLine(HttpVersion httpVersion, HttpRequestMethod method, String requestUri, + QueryCollection queries) { this.httpVersion = notNull(httpVersion, "The http version cannot be null."); this.method = notNull(method, "The request method cannot be null."); this.requestUri = notNull(requestUri, "The request uri cannot be null."); + this.queries = notNull(queries, "The query collection cannot be null."); } @Override @@ -39,6 +43,11 @@ public String requestUri() { return this.requestUri; } + @Override + public QueryCollection queries() { + return this.queries; + } + @Override public HttpVersion httpVersion() { return this.httpVersion; diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/util/QueryUtils.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/util/QueryUtils.java new file mode 100644 index 00000000..fa849e0c --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/main/java/modelengine/fit/http/protocol/util/QueryUtils.java @@ -0,0 +1,80 @@ +/*--------------------------------------------------------------------------------------------- + * 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.fit.http.protocol.util; + +import modelengine.fitframework.model.MultiValueMap; +import modelengine.fitframework.resource.UrlUtils; +import modelengine.fitframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.function.Function; + +/** + * Utility class for parsing HTTP query parameters into a key-value map. + *

+ * This class provides methods to parse query strings into a {@link MultiValueMap}, + * where keys are ordered and values can be multi-valued. The query string format is + * typically {@code k1=v1&k2=v2}. + *

+ * + * @author 季聿阶 + * @since 2025-05-26 + */ +public class QueryUtils { + private static final char KEY_VALUE_PAIR_SEPARATOR = '&'; + private static final char KEY_VALUE_SEPARATOR = '='; + + /** + * Parses the given query string into a key-value map. + *

+ * The query string is split into key-value pairs using the {@code &} separator. + * Each key-value pair is further split using the {@code =} separator. + * The resulting map is ordered and supports multi-valued keys. + *

+ * + * @param keyValues The query string to be parsed. + * @return A {@link MultiValueMap} containing the parsed key-value pairs. + * If the input string is blank, an empty map is returned. + */ + public static MultiValueMap parseQuery(String keyValues) { + return parseQuery(keyValues, UrlUtils::decodePath); + } + + /** + * Parses the given query string into a key-value map using a custom decoding method. + *

+ * The query string is split into key-value pairs using the {@code &} separator. + * Each key-value pair is further split using the {@code =} separator. + * The resulting map is ordered and supports multi-valued keys. + *

+ * + * @param keyValues The query string to be parsed. + * @param decodeMethod A function to decode the keys and values (e.g., URL decoding). + * @return A {@link MultiValueMap} containing the parsed and decoded key-value pairs. + * If the input string is blank, an empty map is returned. + */ + public static MultiValueMap parseQuery(String keyValues, Function decodeMethod) { + MultiValueMap map = MultiValueMap.create(LinkedHashMap::new); + if (StringUtils.isBlank(keyValues)) { + return map; + } + List keyValuePairs = + StringUtils.split(keyValues, KEY_VALUE_PAIR_SEPARATOR, ArrayList::new, StringUtils::isNotBlank); + for (String keyValuePair : keyValuePairs) { + int index = keyValuePair.indexOf(KEY_VALUE_SEPARATOR); + if (index <= 0) { + continue; + } + String key = decodeMethod.apply(keyValuePair.substring(0, index)); + String value = decodeMethod.apply(keyValuePair.substring(index + 1)); + map.add(key, value); + } + return map; + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/test/java/modelengine/fit/http/protocol/support/DefaultRequestLineTest.java b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/test/java/modelengine/fit/http/protocol/support/DefaultRequestLineTest.java index 49e9b346..3304d7d5 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/test/java/modelengine/fit/http/protocol/support/DefaultRequestLineTest.java +++ b/framework/fit/java/fit-builtin/services/fit-http-protocol/definition/src/test/java/modelengine/fit/http/protocol/support/DefaultRequestLineTest.java @@ -10,6 +10,7 @@ import modelengine.fit.http.protocol.HttpRequestMethod; import modelengine.fit.http.protocol.HttpVersion; +import modelengine.fit.http.protocol.QueryCollection; import modelengine.fit.http.protocol.RequestLine; import org.junit.jupiter.api.DisplayName; @@ -26,7 +27,8 @@ public class DefaultRequestLineTest { private final HttpVersion httpVersion = HttpVersion.HTTP_1_0; private final HttpRequestMethod method = HttpRequestMethod.CONNECT; private final String requestUri = "testRequestUri"; - private final RequestLine defaultRequestLine = RequestLine.create(httpVersion, method, requestUri); + private final QueryCollection queries = QueryCollection.create(); + private final RequestLine defaultRequestLine = RequestLine.create(httpVersion, method, requestUri, queries); @Test @DisplayName("获取的方法与给定的方法值相等") @@ -48,4 +50,11 @@ void theHttpVersionShouldBeEqualsToTheGivenVersion() { HttpVersion actualHttpVersion = this.defaultRequestLine.httpVersion(); assertThat(actualHttpVersion).isEqualTo(this.httpVersion); } + + @Test + @DisplayName("获取的查询集合与给定的查询集合相等") + void theQueriesShouldBeEqualsToTheGivenQueries() { + QueryCollection actualQueries = this.defaultRequestLine.queries(); + assertThat(actualQueries).isEqualTo(this.queries); + } }