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); + } }