From 653b5bc751325bfc879343ba07a43dd48d7abeea Mon Sep 17 00:00:00 2001 From: CodeCaster Date: Sun, 25 May 2025 21:10:21 +0800 Subject: [PATCH] [fel] add tools/list in MCP client --- framework/fel/java/plugins/pom.xml | 2 + .../fel/java/plugins/tool-discoverer/pom.xml | 6 +- .../fel/java/plugins/tool-executor/pom.xml | 6 +- .../plugins/tool-factory-repository/pom.xml | 6 +- .../fel/java/plugins/tool-mcp-client/pom.xml | 103 ++++++ .../mcp/client/support/DefaultMcpClient.java | 346 ++++++++++++++++++ .../support/DefaultMcpClientFactory.java | 50 +++ .../src/main/resources/application.yml | 4 + .../fel/java/plugins/tool-mcp-server/pom.xml | 13 +- .../fel/tool/mcp/server/McpServer.java | 9 +- ...ntroller.java => McpServerController.java} | 60 ++- .../tool/mcp/server/entity/JsonRpcEntity.java | 140 ------- .../tool/mcp/server/handler/PingHandler.java | 38 ++ .../mcp/server/support/DefaultMcpServer.java | 35 +- ...Test.java => McpServerControllerTest.java} | 12 +- .../server/support/DefaultMcpServerTest.java | 26 +- .../fel/java/plugins/tool-mcp-test/pom.xml | 84 +++++ .../fel/tool/mcp/test/TestController.java | 50 +++ .../src/main/resources/application.yml | 4 + .../plugins/tool-repository-simple/pom.xml | 6 +- framework/fel/java/pom.xml | 10 + framework/fel/java/services/pom.xml | 2 + .../services/tool-mcp-client-service/pom.xml | 49 +++ .../fel/tool/mcp/client/McpClient.java | 44 +++ .../fel/tool/mcp/client/McpClientFactory.java | 25 ++ .../fel/java/services/tool-mcp-common/pom.xml | 39 ++ .../fel/tool/mcp/entity/Event.java | 45 +++ .../fel/tool/mcp/entity/JsonRpc.java | 107 ++++++ .../fel/tool/mcp/entity/Method.java | 60 +++ .../fel/tool/mcp/entity/Server.java | 213 +++++++++++ .../fel/tool/mcp/entity/Tool.java} | 4 +- .../fel/tool/support/AbstractTool.java | 3 +- .../json/jackson/JacksonObjectSerializer.java | 4 +- .../serializer/TextEventStreamSerializer.java | 2 +- 34 files changed, 1376 insertions(+), 231 deletions(-) create mode 100644 framework/fel/java/plugins/tool-mcp-client/pom.xml create mode 100644 framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java create mode 100644 framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java create mode 100644 framework/fel/java/plugins/tool-mcp-client/src/main/resources/application.yml rename framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/{McpController.java => McpServerController.java} (78%) delete mode 100644 framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java create mode 100644 framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/PingHandler.java rename framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/{McpControllerTest.java => McpServerControllerTest.java} (85%) create mode 100644 framework/fel/java/plugins/tool-mcp-test/pom.xml create mode 100644 framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java create mode 100644 framework/fel/java/plugins/tool-mcp-test/src/main/resources/application.yml create mode 100644 framework/fel/java/services/tool-mcp-client-service/pom.xml create mode 100644 framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClient.java create mode 100644 framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java create mode 100644 framework/fel/java/services/tool-mcp-common/pom.xml create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Event.java create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Method.java create mode 100644 framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java rename framework/fel/java/{plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java => services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Tool.java} (97%) diff --git a/framework/fel/java/plugins/pom.xml b/framework/fel/java/plugins/pom.xml index 95322044..6bc4994e 100644 --- a/framework/fel/java/plugins/pom.xml +++ b/framework/fel/java/plugins/pom.xml @@ -16,7 +16,9 @@ tool-discoverer tool-executor tool-factory-repository + tool-mcp-client tool-mcp-server + tool-mcp-test tool-repository-simple \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-discoverer/pom.xml b/framework/fel/java/plugins/tool-discoverer/pom.xml index 2cbd48f2..0f601af5 100644 --- a/framework/fel/java/plugins/tool-discoverer/pom.xml +++ b/framework/fel/java/plugins/tool-discoverer/pom.xml @@ -58,8 +58,8 @@ fit-build-maven-plugin ${fit.version} - user - 2 + system + 5 @@ -86,7 +86,7 @@ + todir="../../../../../build/plugins"/> diff --git a/framework/fel/java/plugins/tool-executor/pom.xml b/framework/fel/java/plugins/tool-executor/pom.xml index 987266d2..802df1b6 100644 --- a/framework/fel/java/plugins/tool-executor/pom.xml +++ b/framework/fel/java/plugins/tool-executor/pom.xml @@ -58,8 +58,8 @@ fit-build-maven-plugin ${fit.version} - user - 2 + system + 5 @@ -86,7 +86,7 @@ + todir="../../../../../build/plugins"/> diff --git a/framework/fel/java/plugins/tool-factory-repository/pom.xml b/framework/fel/java/plugins/tool-factory-repository/pom.xml index bb80c6ed..c02bc201 100644 --- a/framework/fel/java/plugins/tool-factory-repository/pom.xml +++ b/framework/fel/java/plugins/tool-factory-repository/pom.xml @@ -36,8 +36,8 @@ fit-build-maven-plugin ${fit.version} - user - 2 + system + 5 @@ -64,7 +64,7 @@ + todir="../../../../../build/plugins"/> diff --git a/framework/fel/java/plugins/tool-mcp-client/pom.xml b/framework/fel/java/plugins/tool-mcp-client/pom.xml new file mode 100644 index 00000000..d33b7bdf --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-client/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + + org.fitframework.fel + fel-plugin-parent + 3.5.0-SNAPSHOT + + + fel-tool-mcp-client + + + + + org.fitframework + fit-api + + + org.fitframework + fit-util + + + org.fitframework + fit-reactor + + + org.fitframework.service + fit-http-classic + + + + + org.fitframework.fel + tool-mcp-client-service + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + + + + + org.fitframework + fit-build-maven-plugin + ${fit.version} + + system + 5 + + + + build-plugin + + build-plugin + + + + package-plugin + + package-plugin + + + + + + org.apache.maven.plugins + maven-antrun-plugin + ${maven.antrun.version} + + + package + + + + + + + run + + + + + + + \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-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 new file mode 100644 index 00000000..74c08c12 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClient.java @@ -0,0 +1,346 @@ +/*--------------------------------------------------------------------------------------------- + * 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.client.support; + +import static modelengine.fitframework.util.ObjectUtils.cast; + +import modelengine.fel.tool.mcp.client.McpClient; +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.Tool; +import modelengine.fit.http.client.HttpClassicClient; +import modelengine.fit.http.client.HttpClassicClientRequest; +import modelengine.fit.http.client.HttpClassicClientResponse; +import modelengine.fit.http.entity.Entity; +import modelengine.fit.http.entity.TextEvent; +import modelengine.fit.http.protocol.HttpRequestMethod; +import modelengine.fitframework.flowable.Choir; +import modelengine.fitframework.log.Logger; +import modelengine.fitframework.schedule.ExecutePolicy; +import modelengine.fitframework.schedule.Task; +import modelengine.fitframework.schedule.ThreadPoolExecutor; +import modelengine.fitframework.schedule.ThreadPoolScheduler; +import modelengine.fitframework.serialization.ObjectSerializer; +import modelengine.fitframework.util.LockUtils; +import modelengine.fitframework.util.ObjectUtils; +import modelengine.fitframework.util.ThreadUtils; +import modelengine.fitframework.util.UuidUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +/** + * Represents a default implementation of the MCP client, responsible for interacting with the MCP server. + * This class provides methods for initializing the client, retrieving tools, and calling tools. + * + * @author 季聿阶 + * @since 2025-05-21 + */ +public class DefaultMcpClient implements McpClient { + private static final Logger log = Logger.get(DefaultMcpClient.class); + private static final long DELAY_MILLIS = 30_000L; + + private final ObjectSerializer jsonSerializer; + private final HttpClassicClient client; + private final String connectionString; + private final String name; + private final AtomicLong id = new AtomicLong(0); + + private volatile String messageUrl; + private volatile String sessionId; + private volatile Server server; + 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<>(); + + /** + * 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. + */ + public DefaultMcpClient(ObjectSerializer jsonSerializer, HttpClassicClient client, String connectionString) { + this.jsonSerializer = jsonSerializer; + this.client = client; + this.connectionString = connectionString; + this.name = UuidUtils.randomUuidString(); + } + + @Override + public void initialize() { + HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.GET, connectionString); + Choir messages = this.client.exchangeStream(request, TextEvent.class); + ThreadPoolExecutor threadPool = ThreadPoolExecutor.custom() + .threadPoolName("mcp-client-" + this.name) + .awaitTermination(3, TimeUnit.SECONDS) + .isImmediateShutdown(true) + .corePoolSize(1) + .maximumPoolSize(1) + .keepAliveTime(60, TimeUnit.SECONDS) + .workQueueCapacity(Integer.MAX_VALUE) + .isDaemonThread(true) + .exceptionHandler((thread, cause) -> log.warn("Exception in MCP client pool.", cause)) + .rejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()) + .build(); + messages.subscribeOn(threadPool).subscribe(subscription -> { + log.info("Prepare to create SSE channel."); + subscription.request(Long.MAX_VALUE); + }, + (subscription, textEvent) -> this.consumeTextEvent(textEvent), + subscription -> log.info("SSE channel is completed."), + (subscription, cause) -> log.error("SSE channel is failed.", cause)); + ThreadPoolScheduler pingScheduler = ThreadPoolScheduler.custom() + .threadPoolName("mcp-client-ping-" + this.name) + .awaitTermination(3, TimeUnit.SECONDS) + .isImmediateShutdown(true) + .corePoolSize(1) + .maximumPoolSize(1) + .keepAliveTime(60, TimeUnit.SECONDS) + .workQueueCapacity(Integer.MAX_VALUE) + .isDaemonThread(true) + .build(); + pingScheduler.schedule(Task.builder() + .runnable(this::pingServer) + .policy(ExecutePolicy.fixedDelay(DELAY_MILLIS)) + .build(), DELAY_MILLIS); + while (!this.waitInitialized()) { + ThreadUtils.sleep(100); + } + } + + private void consumeTextEvent(TextEvent textEvent) { + log.info("Receive message from MCP server. [message={}]", textEvent.data()); + if (Objects.equals(textEvent.event(), "endpoint")) { + this.initializeMcpServer(textEvent); + return; + } + Map jsonRpc = this.jsonSerializer.deserialize((String) textEvent.data(), Object.class); + Object messageId = jsonRpc.get("id"); + if (messageId == null) { + // Notification message, ignore. + return; + } + long actualId = Long.parseLong(messageId.toString()); + Consumer> consumer = this.responseConsumers.remove(actualId); + if (consumer == null) { + log.info("No consumer registered. [id={}]", actualId); + return; + } + Object error = jsonRpc.get("error"); + JsonRpc.Response response; + if (error == null) { + response = JsonRpc.createResponse(actualId, jsonRpc.get("result")); + } else { + response = JsonRpc.createResponseWithError(actualId, error); + } + consumer.accept(response); + } + + private void pingServer() { + if (this.isNotInitialized()) { + 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); + long currentId = this.getNextId(); + JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.PING.code()); + request.entity(Entity.createObject(request, rpcRequest)); + log.info("Send {} method to MCP server. [sessionId={}, request={}]", + Method.PING.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.PING.code(), + this.sessionId, + exchange.statusCode()); + } else { + log.error("Failed to {} MCP server. [sessionId={}, statusCode={}]", + Method.PING.code(), + this.sessionId, + exchange.statusCode()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void initializeMcpServer(TextEvent textEvent) { + this.messageUrl = textEvent.data().toString(); + this.sessionId = textEvent.id(); + HttpClassicClientRequest request = this.client.createRequest(HttpRequestMethod.POST, this.messageUrl); + long currentId = this.getNextId(); + this.responseConsumers.put(currentId, this::initializedMcpServer); + JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.INITIALIZE.code()); + request.entity(Entity.createObject(request, rpcRequest)); + log.info("Send {} method to MCP server. [sessionId={}, request={}]", + Method.INITIALIZE.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.INITIALIZE.code(), + this.sessionId, + exchange.statusCode()); + } else { + log.error("Failed to {} MCP server. [sessionId={}, statusCode={}]", + Method.INITIALIZE.code(), + this.sessionId, + exchange.statusCode()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + private void initializedMcpServer(JsonRpc.Response response) { + if (response.error() != null) { + log.error("Abort send {} method to MCP server. [sessionId={}, error={}]", + Method.NOTIFICATION_INITIALIZED.code(), + this.sessionId, + response.error()); + throw new IllegalStateException(response.error().toString()); + } + synchronized (this.initializedLock) { + 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); + JsonRpc.Notification notification = JsonRpc.createNotification(Method.NOTIFICATION_INITIALIZED.code()); + request.entity(Entity.createObject(request, notification)); + log.info("Send {} method to MCP server. [sessionId={}, notification={}]", + Method.NOTIFICATION_INITIALIZED.code(), + this.sessionId, + notification); + 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.NOTIFICATION_INITIALIZED.code(), + this.sessionId, + exchange.statusCode()); + } else { + log.error("Failed to {} MCP server. [sessionId={}, statusCode={}]", + Method.NOTIFICATION_INITIALIZED.code(), + this.sessionId, + exchange.statusCode()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @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); + long currentId = this.getNextId(); + this.responseConsumers.put(currentId, this::getTools0); + this.pendingRequests.put(currentId, true); + JsonRpc.Request rpcRequest = JsonRpc.createRequest(currentId, Method.TOOLS_LIST.code()); + request.entity(Entity.createObject(request, rpcRequest)); + log.info("Send {} method to MCP server. [sessionId={}, request={}]", + Method.TOOLS_LIST.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_LIST.code(), + this.sessionId, + exchange.statusCode()); + } else { + log.error("Failed to {} MCP server. [sessionId={}, statusCode={}]", + Method.TOOLS_LIST.code(), + this.sessionId, + exchange.statusCode()); + } + } catch (IOException e) { + throw new IllegalStateException(e); + } + while (this.pendingRequests.get(currentId)) { + ThreadUtils.sleep(100); + } + synchronized (this.toolsLock) { + return this.tools; + } + } + + private void getTools0(JsonRpc.Response response) { + if (response.error() != null) { + log.error("Failed to get tools list from MCP server. [sessionId={}, response={}]", + this.sessionId, + response); + return; + } + Map result = cast(response.result()); + List> rawTools = cast(result.get("tools")); + synchronized (this.toolsLock) { + this.tools.clear(); + this.tools.addAll(rawTools.stream() + .map(rawTool -> ObjectUtils.toCustomObject(rawTool, Tool.class)) + .toList()); + this.pendingRequests.put(response.id(), false); + } + } + + @Override + public Object callTool(String name, Map arguments) { + if (this.isNotInitialized()) { + throw new IllegalStateException("MCP client is not initialized. Please wait a moment."); + } + return null; + } + + private long getNextId() { + long tmpId = this.id.getAndIncrement(); + if (tmpId < 0) { + this.id.set(0); + return 0; + } + return tmpId; + } + + private boolean isNotInitialized() { + return !this.initialized; + } + + private boolean waitInitialized() { + if (this.initialized) { + return true; + } + synchronized (this.initializedLock) { + if (this.initialized) { + return true; + } + try { + this.initializedLock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + return this.initialized; + } +} 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 new file mode 100644 index 00000000..61412104 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-client/src/main/java/modelengine/fel/tool/mcp/client/support/DefaultMcpClientFactory.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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.client.support; + +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; +import modelengine.fit.http.client.HttpClassicClient; +import modelengine.fit.http.client.HttpClassicClientFactory; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.annotation.Fit; +import modelengine.fitframework.serialization.ObjectSerializer; + +/** + * Represents a factory for creating instances of the DefaultMcpClient. + * This class is responsible for initializing and configuring the HTTP client and JSON serializer + * required by the DefaultMcpClient. + * + * @author 季聿阶 + * @since 2025-05-21 + */ +@Component +public class DefaultMcpClientFactory implements McpClientFactory { + private final HttpClassicClient client; + private final ObjectSerializer jsonSerializer; + + /** + * Constructs a new instance of the DefaultMcpClientFactory. + * + * @param clientFactory The factory used to create the HTTP client. + * @param jsonSerializer The JSON serializer used for serialization and deserialization. + */ + public DefaultMcpClientFactory(HttpClassicClientFactory clientFactory, + @Fit(alias = "json") ObjectSerializer jsonSerializer) { + this.client = clientFactory.create(HttpClassicClientFactory.Config.builder() + .connectTimeout(30_000) + .socketTimeout(60_000) + .connectionRequestTimeout(60_000) + .build()); + this.jsonSerializer = jsonSerializer; + } + + @Override + public McpClient create(String connectionString) { + return new DefaultMcpClient(this.jsonSerializer, this.client, connectionString); + } +} diff --git a/framework/fel/java/plugins/tool-mcp-client/src/main/resources/application.yml b/framework/fel/java/plugins/tool-mcp-client/src/main/resources/application.yml new file mode 100644 index 00000000..9c9531e3 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-client/src/main/resources/application.yml @@ -0,0 +1,4 @@ +fit: + beans: + packages: + - 'modelengine.fel.tool.mcp.client.support' \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-server/pom.xml b/framework/fel/java/plugins/tool-mcp-server/pom.xml index 123964db..e92ee75c 100644 --- a/framework/fel/java/plugins/tool-mcp-server/pom.xml +++ b/framework/fel/java/plugins/tool-mcp-server/pom.xml @@ -25,11 +25,20 @@ org.fitframework fit-reactor + + org.fitframework.service + fit-http-classic + + org.fitframework.fel tool-service + + org.fitframework.fel + tool-mcp-common + @@ -57,7 +66,7 @@ ${fit.version} system - 4 + 5 @@ -84,7 +93,7 @@ + todir="../../../../../build/plugins"/> 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 121a4686..8da9c35e 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,8 @@ package modelengine.fel.tool.mcp.server; -import modelengine.fel.tool.mcp.server.entity.ToolEntity; +import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.Tool; import java.util.List; import java.util.Map; @@ -23,14 +24,14 @@ public interface McpServer { * * @return The MCP Server Info as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. */ - Map getInfo(); + Server getInfo(); /** * Gets MCP Server Tools. * - * @return The MCP Server Tools as a {@link List}{@code <}{@link ToolEntity}{@code >}. + * @return The MCP Server Tools as a {@link List}{@code <}{@link Tool}{@code >}. */ - List getTools(); + List getTools(); /** * Calls MCP Server Tool. diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java similarity index 78% rename from framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java rename to framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java index cbf3b539..e53838dd 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpController.java +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/McpServerController.java @@ -8,9 +8,13 @@ import static modelengine.fitframework.inspection.Validation.notBlank; import static modelengine.fitframework.inspection.Validation.notNull; +import static modelengine.fitframework.util.ObjectUtils.cast; -import modelengine.fel.tool.mcp.server.entity.JsonRpcEntity; +import modelengine.fel.tool.mcp.entity.Event; +import modelengine.fel.tool.mcp.entity.JsonRpc; +import modelengine.fel.tool.mcp.entity.Method; import modelengine.fel.tool.mcp.server.handler.InitializeHandler; +import modelengine.fel.tool.mcp.server.handler.PingHandler; import modelengine.fel.tool.mcp.server.handler.ToolCallHandler; import modelengine.fel.tool.mcp.server.handler.ToolListHandler; import modelengine.fel.tool.mcp.server.handler.UnsupportedMethodHandler; @@ -48,15 +52,9 @@ * @since 2025-05-13 */ @Component -public class McpController implements McpServer.ToolsChangedObserver { - private static final Logger log = Logger.get(McpController.class); +public class McpServerController implements McpServer.ToolsChangedObserver { + private static final Logger log = Logger.get(McpServerController.class); private static final String MESSAGE_PATH = "/mcp/message"; - private static final String EVENT_ENDPOINT = "endpoint"; - private static final String EVENT_MESSAGE = "message"; - private static final String METHOD_INITIALIZE = "initialize"; - private static final String METHOD_TOOLS_LIST = "tools/list"; - private static final String METHOD_TOOLS_CALL = "tools/call"; - private static final String METHOD_NOTIFICATION_TOOLS_CHANGED = "notifications/tools/list_changed"; private static final String RESPONSE_OK = StringUtils.EMPTY; private final Map> emitters = new ConcurrentHashMap<>(); @@ -75,16 +73,17 @@ public class McpController implements McpServer.ToolsChangedObserver { * @param mcpServer The MCP server instance used to handle tool operations such as initialization, * listing tools, and calling tools, as a {@link McpServer}. */ - public McpController(@Value("${base-url}") String baseUrl, @Fit(alias = "json") ObjectSerializer serializer, + 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."); this.serializer = notNull(serializer, "The json serializer cannot be null."); notNull(mcpServer, "The MCP server cannot be null."); mcpServer.registerToolsChangedObserver(this); - this.methodHandlers.put(METHOD_INITIALIZE, new InitializeHandler(mcpServer)); - this.methodHandlers.put(METHOD_TOOLS_LIST, new ToolListHandler(mcpServer)); - this.methodHandlers.put(METHOD_TOOLS_CALL, new ToolCallHandler(mcpServer, this.serializer)); + this.methodHandlers.put(Method.INITIALIZE.code(), new InitializeHandler(mcpServer)); + this.methodHandlers.put(Method.PING.code(), new PingHandler()); + this.methodHandlers.put(Method.TOOLS_LIST.code(), new ToolListHandler(mcpServer)); + this.methodHandlers.put(Method.TOOLS_CALL.code(), new ToolCallHandler(mcpServer, this.serializer)); ThreadPoolScheduler channelDetectorScheduler = ThreadPoolScheduler.custom() .corePoolSize(1) @@ -127,12 +126,10 @@ public Choir createSse(HttpClassicServerResponse response) { log.info("New SSE channel for MCP server created. [sessionId={}]", sessionId); return Choir.create(emitter -> { emitters.put(sessionId, emitter); - TextEvent textEvent = TextEvent.custom() - .id(sessionId) - .event(EVENT_ENDPOINT) - .data(this.baseUrl + MESSAGE_PATH + "?sessionId=" + sessionId) - .build(); + String data = this.baseUrl + MESSAGE_PATH + "?sessionId=" + sessionId; + TextEvent textEvent = TextEvent.custom().id(sessionId).event(Event.ENDPOINT.code()).data(data).build(); emitter.emit(textEvent); + log.info("Send MCP endpoint. [endpoint={}]", data); }); } @@ -148,38 +145,37 @@ public Choir createSse(HttpClassicServerResponse response) { */ @PostMapping(path = MESSAGE_PATH) public Object receiveMcpMessage(@RequestQuery(name = "sessionId") String sessionId, - @RequestBody JsonRpcEntity request) { - log.info("Receive MCP message. [sessionId={}, request={}]", sessionId, request); - Object id = request.getId(); + @RequestBody Map request) { + log.info("Receive MCP message. [sessionId={}, message={}]", sessionId, request); + Object id = request.get("id"); if (id == null) { // Request without an ID indicates a notification message, ignore. return RESPONSE_OK; } - MessageHandler handler = this.methodHandlers.getOrDefault(request.getMethod(), this.unsupportedMethodHandler); - JsonRpcEntity response = new JsonRpcEntity(); - response.setId(id); + String method = cast(request.getOrDefault("method", StringUtils.EMPTY)); + MessageHandler handler = this.methodHandlers.getOrDefault(method, this.unsupportedMethodHandler); + JsonRpc.Response response; try { - Object result = handler.handle(request.getParams()); - response.setResult(result); + Object result = handler.handle(cast(request.get("params"))); + response = JsonRpc.createResponse(id, result); } catch (Exception e) { log.error("Failed to handle MCP message.", e); - response.setError(e.getMessage()); + response = JsonRpc.createResponseWithError(id, e.getMessage()); } String serialized = this.serializer.serialize(response); - TextEvent textEvent = TextEvent.custom().id(sessionId).event(EVENT_MESSAGE).data(serialized).build(); + TextEvent textEvent = TextEvent.custom().id(sessionId).event(Event.MESSAGE.code()).data(serialized).build(); Emitter emitter = this.emitters.get(sessionId); emitter.emit(textEvent); - log.info("Send MCP message. [response={}]", serialized); + log.info("Send MCP message. [message={}]", serialized); return RESPONSE_OK; } @Override public void onToolsChanged() { - JsonRpcEntity notification = new JsonRpcEntity(); - notification.setMethod(METHOD_NOTIFICATION_TOOLS_CHANGED); + JsonRpc.Notification notification = JsonRpc.createNotification(Method.NOTIFICATION_TOOLS_CHANGED.code()); String serialized = this.serializer.serialize(notification); this.emitters.forEach((sessionId, emitter) -> { - TextEvent textEvent = TextEvent.custom().id(sessionId).event(EVENT_MESSAGE).data(serialized).build(); + TextEvent textEvent = TextEvent.custom().id(sessionId).event(Event.MESSAGE.code()).data(serialized).build(); emitter.emit(textEvent); log.info("Send MCP notification: tools changed. [sessionId={}]", sessionId); }); diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java deleted file mode 100644 index 25019a2d..00000000 --- a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/JsonRpcEntity.java +++ /dev/null @@ -1,140 +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.server.entity; - -import java.util.Map; - -/** - * Represents a JSON RPC request entity, encapsulating information related to JSON RPC requests. - * This class follows the JSON RPC specification, supporting the construction and parsing of JSON RPC request objects. - * - * @author 季聿阶 - * @since 2025-05-15 - */ -public class JsonRpcEntity { - private String jsonrpc = "2.0"; - private String method; - private Object id; - private Map params; - private Object result; - private Object error; - - /** - * Gets the JSON RPC version used in the request or response. - * - * @return The JSON RPC version as a {@link String}, typically "2.0". - */ - public String getJsonrpc() { - return this.jsonrpc; - } - - /** - * Sets the JSON RPC version used in the request or response. - * - * @param jsonrpc The JSON RPC version as a {@link String}, typically "2.0". - */ - public void setJsonrpc(String jsonrpc) { - this.jsonrpc = jsonrpc; - } - - /** - * Gets the method name to be invoked in the JSON RPC request. - * - * @return The method name as a {@link String}. - */ - public String getMethod() { - return this.method; - } - - /** - * Sets the method name to be invoked in the JSON RPC request. - * - * @param method The method name as a {@link String}. - */ - public void setMethod(String method) { - this.method = method; - } - - /** - * Gets the identifier used to correlate the request with its corresponding response. - * - * @return The identifier as an {@link Object}, which can be of type {@link String}, {@link Number}, or null. - */ - public Object getId() { - return this.id; - } - - /** - * Sets the identifier used to correlate the request with its corresponding response. - * - * @param id The identifier as an {@link Object}, which can be of type {@link String}, {@link Number}, or null. - */ - public void setId(Object id) { - this.id = id; - } - - /** - * Gets the parameters associated with the JSON RPC method call. - * - * @return A map containing the parameters as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. - */ - public Map getParams() { - return this.params; - } - - /** - * Sets the parameters associated with the JSON RPC method call. - * - * @param params A map containing the parameters as a - * {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. - */ - public void setParams(Map params) { - this.params = params; - } - - /** - * Gets the result returned by the JSON RPC method execution. - * - * @return The result as an {@link Object}, which can be of any type depending on the method implementation. - */ - public Object getResult() { - return this.result; - } - - /** - * Sets the result returned by the JSON RPC method execution. - * - * @param result The result as an {@link Object}, which can be of any type depending on the method implementation. - */ - public void setResult(Object result) { - this.result = result; - } - - /** - * Gets the error information if the JSON RPC method execution resulted in an error. - * - * @return The error information as an {@link Object}, or null if no error occurred. - */ - public Object getError() { - return this.error; - } - - /** - * Sets the error information indicating that the JSON RPC method execution resulted in an error. - * - * @param error The error information as an {@link Object}, or null if no error occurred. - */ - public void setError(Object error) { - this.error = error; - } - - @Override - public String toString() { - return "JsonRpcEntity{" + "jsonrpc='" + this.jsonrpc + '\'' + ", method='" + this.method + '\'' + ", id=" - + this.id + ", params=" + this.params + ", result=" + this.result + ", error=" + this.error + '}'; - } -} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/PingHandler.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/PingHandler.java new file mode 100644 index 00000000..5e0f1fd2 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/handler/PingHandler.java @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.server.handler; + +import modelengine.fel.tool.mcp.server.MessageRequest; + +import java.util.Collections; +import java.util.Map; + +/** + * @author 季聿阶 + * @since 2025-05-15 + */ +public class PingHandler extends AbstractMessageHandler { + private static final Map PING_RESULT = Collections.emptyMap(); + + /** + * Constructs a new instance of the PingHandler class. + */ + public PingHandler() { + super(PingRequest.class); + } + + @Override + public Object handle(PingRequest request) { + return PING_RESULT; + } + + /** + * @author 季聿阶 + * @since 2025-05-15 + */ + public static class PingRequest extends MessageRequest {} +} diff --git a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java b/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/support/DefaultMcpServer.java index 48edf120..b0b95e9a 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,13 +8,13 @@ import static modelengine.fitframework.inspection.Validation.notNull; +import modelengine.fel.tool.mcp.entity.Server; +import modelengine.fel.tool.mcp.entity.Tool; import modelengine.fel.tool.mcp.server.McpServer; -import modelengine.fel.tool.mcp.server.entity.ToolEntity; import modelengine.fel.tool.service.ToolChangedObserver; import modelengine.fel.tool.service.ToolExecuteService; import modelengine.fitframework.annotation.Component; import modelengine.fitframework.log.Logger; -import modelengine.fitframework.util.MapBuilder; import modelengine.fitframework.util.MapUtils; import modelengine.fitframework.util.StringUtils; @@ -34,7 +34,7 @@ public class DefaultMcpServer implements McpServer, ToolChangedObserver { private static final Logger log = Logger.get(DefaultMcpServer.class); private final ToolExecuteService toolExecuteService; - private final Map tools = new ConcurrentHashMap<>(); + private final Map tools = new ConcurrentHashMap<>(); private final List toolsChangedObservers = new ArrayList<>(); /** @@ -48,21 +48,24 @@ public DefaultMcpServer(ToolExecuteService toolExecuteService) { } @Override - public Map getInfo() { - return MapBuilder.get() - .put("protocolVersion", "2025-03-26") - .put("capabilities", - MapBuilder.get() - .put("logging", MapBuilder.get().build()) - .put("tools", MapBuilder.get().put("listChanged", true).build()) - .build()) - .put("serverInfo", - MapBuilder.get().put("name", "FIT Store MCP Server").put("version", "3.5.0-SNAPSHOT").build()) - .build(); + 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; } @Override - public List getTools() { + public List getTools() { return List.copyOf(this.tools.values()); } @@ -95,7 +98,7 @@ public void onToolAdded(String name, String description, Map sch log.warn("Tool addition is ignored: tool schema is null or empty. [toolName={}]", name); return; } - ToolEntity tool = new ToolEntity(); + Tool tool = new Tool(); tool.setName(name); tool.setDescription(description); tool.setInputSchema(schema); diff --git a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpControllerTest.java b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java similarity index 85% rename from framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpControllerTest.java rename to framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java index e1baa97d..fd75a33d 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpControllerTest.java +++ b/framework/fel/java/plugins/tool-mcp-server/src/test/java/modelengine/fel/tool/mcp/server/McpServerControllerTest.java @@ -18,13 +18,13 @@ import org.junit.jupiter.api.Test; /** - * Unit test for {@link McpController}. + * Unit test for {@link McpServerController}. * * @author 季聿阶 * @since 2025-05-20 */ @DisplayName("Unit tests for McpController") -public class McpControllerTest { +public class McpServerControllerTest { private ObjectSerializer objectSerializer; private McpServer mcpServer; private String baseUrl; @@ -44,12 +44,12 @@ class GivenConstructor { void shouldThrowExceptionWhenBaseUrlIsNullOrEmpty() { // Null var exception1 = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpController(null, objectSerializer, mcpServer)); + () -> new McpServerController(null, objectSerializer, mcpServer)); assertThat(exception1).hasMessage("The base URL for MCP server cannot be blank."); // Blank var exception2 = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpController("", objectSerializer, mcpServer)); + () -> new McpServerController("", objectSerializer, mcpServer)); assertThat(exception2).hasMessage("The base URL for MCP server cannot be blank."); } @@ -57,7 +57,7 @@ void shouldThrowExceptionWhenBaseUrlIsNullOrEmpty() { @DisplayName("Should throw exception when serializer is null") void shouldThrowExceptionWhenSerializerIsNull() { var exception = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpController(baseUrl, null, mcpServer)); + () -> new McpServerController(baseUrl, null, mcpServer)); assertThat(exception).hasMessage("The json serializer cannot be null."); } @@ -65,7 +65,7 @@ void shouldThrowExceptionWhenSerializerIsNull() { @DisplayName("Should throw exception when mcpServer is null") void shouldThrowExceptionWhenMcpServerIsNull() { var exception = catchThrowableOfType(IllegalArgumentException.class, - () -> new McpController(baseUrl, objectSerializer, null)); + () -> new McpServerController(baseUrl, 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 ec85628b..e4c9af1e 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 @@ -6,7 +6,6 @@ package modelengine.fel.tool.mcp.server.support; -import static modelengine.fitframework.util.ObjectUtils.cast; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowableOfType; import static org.mockito.Mockito.anyMap; @@ -17,8 +16,9 @@ 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.Tool; import modelengine.fel.tool.mcp.server.McpServer; -import modelengine.fel.tool.mcp.server.entity.ToolEntity; import modelengine.fel.tool.service.ToolExecuteService; import modelengine.fitframework.util.MapBuilder; @@ -64,19 +64,19 @@ class GivenGetInfo { @DisplayName("Should return expected server information") void returnExpectedServerInfo() { McpServer server = new DefaultMcpServer(toolExecuteService); - Map info = server.getInfo(); + Server info = server.getInfo(); - assertThat(info).containsKey("protocolVersion").containsValue("2025-03-26"); + assertThat(info).returns("2025-03-26", Server::getProtocolVersion); - Map capabilities = cast(info.get("capabilities")); - assertThat(capabilities).containsKey("logging").containsKey("tools"); + Server.Capabilities capabilities = info.getCapabilities(); + assertThat(capabilities).isNotNull(); - Map toolsCapability = cast(capabilities.get("tools")); - assertThat(toolsCapability).containsEntry("listChanged", true); + Server.Capabilities.Tools toolsCapability = capabilities.getTools(); + assertThat(toolsCapability).returns(true, Server.Capabilities.Tools::isListChanged); - Map serverInfo = cast(info.get("serverInfo")); - assertThat(serverInfo).containsEntry("name", "FIT Store MCP Server") - .containsEntry("version", "3.5.0-SNAPSHOT"); + Server.Info serverInfo = info.getServerInfo(); + assertThat(serverInfo).returns("FIT Store MCP Server", Server.Info::getName) + .returns("3.5.0-SNAPSHOT", Server.Info::getVersion); } } @@ -113,10 +113,10 @@ void addToolSuccessfully() { server.onToolAdded(name, description, schema); - List tools = server.getTools(); + List tools = server.getTools(); assertThat(tools).hasSize(1); - ToolEntity tool = tools.get(0); + Tool tool = tools.get(0); assertThat(tool.getName()).isEqualTo(name); assertThat(tool.getDescription()).isEqualTo(description); assertThat(tool.getInputSchema()).isEqualTo(schema); diff --git a/framework/fel/java/plugins/tool-mcp-test/pom.xml b/framework/fel/java/plugins/tool-mcp-test/pom.xml new file mode 100644 index 00000000..dfdf0864 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-test/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + + org.fitframework.fel + fel-plugin-parent + 3.5.0-SNAPSHOT + + + fel-tool-mcp-test + + + + + org.fitframework + fit-api + + + org.fitframework + fit-util + + + org.fitframework + fit-reactor + + + org.fitframework.service + fit-http-classic + + + + + org.fitframework.fel + tool-mcp-client-service + + + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + org.assertj + assertj-core + test + + + + + + + org.fitframework + fit-build-maven-plugin + ${fit.version} + + system + 5 + + + + build-plugin + + build-plugin + + + + package-plugin + + package-plugin + + + + + + + \ No newline at end of file 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 new file mode 100644 index 00000000..26d8aab5 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-test/src/main/java/modelengine/fel/tool/mcp/test/TestController.java @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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.test; + +import modelengine.fel.tool.mcp.client.McpClient; +import modelengine.fel.tool.mcp.client.McpClientFactory; +import modelengine.fel.tool.mcp.entity.Tool; +import modelengine.fit.http.annotation.GetMapping; +import modelengine.fitframework.annotation.Component; + +import java.util.List; + +/** + * Represents a test controller for interacting with the MCP (Model Communication Protocol) client. + * This class provides methods to initialize the MCP client and retrieve a list of available tools. + * + * @author 季聿阶 + * @since 2025-05-21 + */ +@Component +public class TestController { + private final McpClientFactory mcpClientFactory; + private McpClient client; + + /** + * Constructs a new instance of the TestController. + * + * @param mcpClientFactory The factory used to create instances of the MCP client. + */ + public TestController(McpClientFactory mcpClientFactory) { + this.mcpClientFactory = mcpClientFactory; + } + + /** + * Handles the HTTP GET request to "/test/mcp". + * Initializes the MCP client and retrieves a list of available tools from the server. + * + * @return A list of tools retrieved from the MCP server. + */ + @GetMapping(path = "/test/mcp") + public List testMcp() { + this.client = this.mcpClientFactory.create("http://localhost:8080/sse"); + this.client.initialize(); + return this.client.getTools(); + } +} \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-mcp-test/src/main/resources/application.yml b/framework/fel/java/plugins/tool-mcp-test/src/main/resources/application.yml new file mode 100644 index 00000000..fa2bf6c9 --- /dev/null +++ b/framework/fel/java/plugins/tool-mcp-test/src/main/resources/application.yml @@ -0,0 +1,4 @@ +fit: + beans: + packages: + - 'modelengine.fel.tool.mcp.test' \ No newline at end of file diff --git a/framework/fel/java/plugins/tool-repository-simple/pom.xml b/framework/fel/java/plugins/tool-repository-simple/pom.xml index 547f45f7..ba94d377 100644 --- a/framework/fel/java/plugins/tool-repository-simple/pom.xml +++ b/framework/fel/java/plugins/tool-repository-simple/pom.xml @@ -41,8 +41,8 @@ fit-build-maven-plugin ${fit.version} - user - 1 + system + 5 @@ -69,7 +69,7 @@ + todir="../../../../../build/plugins"/> diff --git a/framework/fel/java/pom.xml b/framework/fel/java/pom.xml index af2d7726..08c3107a 100644 --- a/framework/fel/java/pom.xml +++ b/framework/fel/java/pom.xml @@ -143,6 +143,16 @@ tool-info ${fel.version} + + org.fitframework.fel + tool-mcp-common + ${fel.version} + + + org.fitframework.fel + tool-mcp-client-service + ${fel.version} + diff --git a/framework/fel/java/services/pom.xml b/framework/fel/java/services/pom.xml index 06f02617..7241c67c 100644 --- a/framework/fel/java/services/pom.xml +++ b/framework/fel/java/services/pom.xml @@ -14,6 +14,8 @@ tool-info + tool-mcp-client-service + tool-mcp-common tool-service diff --git a/framework/fel/java/services/tool-mcp-client-service/pom.xml b/framework/fel/java/services/tool-mcp-client-service/pom.xml new file mode 100644 index 00000000..851deb2c --- /dev/null +++ b/framework/fel/java/services/tool-mcp-client-service/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + org.fitframework.fel + fel-services-parent + 3.5.0-SNAPSHOT + + + tool-mcp-client-service + + + + + org.fitframework + fit-api + + + org.fitframework + fit-util + + + org.fitframework.service + fit-http-classic + + + + + org.fitframework.fel + tool-mcp-common + + + + + org.junit.jupiter + junit-jupiter + + + org.mockito + mockito-core + + + org.assertj + assertj-core + + + \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClient.java b/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClient.java new file mode 100644 index 00000000..9f75bbfd --- /dev/null +++ b/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClient.java @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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.client; + +import modelengine.fel.tool.mcp.entity.Tool; + +import java.util.List; +import java.util.Map; + +/** + * The {@code McpClient} interface defines the contract for interacting with the MCP server. + * It provides methods to retrieve available tools and execute specific tools with provided arguments. + * This interface is designed to facilitate communication between the client application and the MCP server, + * enabling seamless integration and tool invocation. + * + * @author 季聿阶 + * @since 2025-05-21 + */ +public interface McpClient { + /** + * Initializes the MCP Client. + */ + void initialize(); + + /** + * Gets MCP Server Tools. + * + * @return The MCP Server Tools as a {@link List}{@code <}{@link Tool}{@code >}. + */ + List getTools(); + + /** + * Calls MCP Server Tool. + * + * @param name The tool name as a {@link String}. + * @param arguments The tool arguments as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}. + * @return The tool result as a {@link Object}. + */ + Object callTool(String name, Map arguments); +} \ No newline at end of file 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 new file mode 100644 index 00000000..1956b909 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-client-service/src/main/java/modelengine/fel/tool/mcp/client/McpClientFactory.java @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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.client; + +/** + * Indicates the factory of {@link McpClient}. + *

+ * Each {@link McpClient} instance created by this factory is designed to connect to a single specified MCP server. + * + * @author 季聿阶 + * @since 2025-05-21 + */ +public interface McpClientFactory { + /** + * Creates a {@link McpClient} instance. + * + * @param connectionString The connection {@link String}. + * @return The connected {@link McpClient} instance. + */ + McpClient create(String connectionString); +} \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-common/pom.xml b/framework/fel/java/services/tool-mcp-common/pom.xml new file mode 100644 index 00000000..09c0fd96 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + + + org.fitframework.fel + fel-services-parent + 3.5.0-SNAPSHOT + + + tool-mcp-common + + + + + org.fitframework + fit-api + + + org.fitframework + fit-util + + + + + org.junit.jupiter + junit-jupiter + + + org.mockito + mockito-core + + + org.assertj + assertj-core + + + \ No newline at end of file diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Event.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Event.java new file mode 100644 index 00000000..61adf739 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Event.java @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fel.tool.mcp.entity; + +/** + * Represents different types of events used in MCP. + * + * @author 季聿阶 + * @since 2025-05-22 + */ +public enum Event { + /** + * Represents an endpoint event. + */ + ENDPOINT("endpoint"), + + /** + * Represents a message event. + */ + MESSAGE("message"); + + private final String code; + + /** + * Constructor to initialize the event with a specific code. + * + * @param code The code associated with the event. + */ + Event(String code) { + this.code = code; + } + + /** + * Returns the code associated with the event. + * + * @return The code of the event. + */ + public String code() { + return this.code; + } +} \ 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 new file mode 100644 index 00000000..1e0e84d5 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/JsonRpc.java @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * 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 java.util.Map; + +/** + * Represents a JSON RPC request entity, encapsulating information related to JSON RPC requests. + * This class follows the JSON RPC specification, supporting the construction and parsing of JSON RPC request objects. + * + * @author 季聿阶 + * @since 2025-05-15 + */ +public class JsonRpc { + /** + * Creates a JSON RPC request with the specified ID and method. + * + * @param id The unique identifier for the request. + * @param method The method name to be invoked. + * @param The type of the request ID. + * @return A JSON RPC request object. + */ + public static Request createRequest(T id, String method) { + return new Request<>("2.0", id, method, null); + } + + /** + * Creates a JSON RPC request with the specified ID, method, and parameters. + * + * @param id The unique identifier for the request. + * @param method The method name to be invoked. + * @param params The parameters associated with the method. + * @param The type of the request ID. + * @return A JSON RPC request object. + */ + public static Request createRequest(T id, String method, Map params) { + return new Request<>("2.0", id, method, params); + } + + /** + * Creates a JSON RPC response with the specified ID and result. + * + * @param id The unique identifier for the response. + * @param result The result of the request. + * @param The type of the response ID. + * @return A JSON RPC response object. + */ + public static Response createResponse(T id, Object result) { + return new Response<>("2.0", id, result, null); + } + + /** + * Creates a JSON RPC response with the specified ID and error. + * + * @param id The unique identifier for the response. + * @param error The error associated with the request. + * @param The type of the response ID. + * @return A JSON RPC response object. + */ + public static Response createResponseWithError(T id, Object error) { + return new Response<>("2.0", id, null, error); + } + + /** + * Creates a JSON RPC notification with the specified method. + * + * @param method The method name to be invoked. + * @return A JSON RPC notification object. + */ + public static Notification createNotification(String method) { + return new Notification("2.0", method, null); + } + + /** + * Creates a JSON RPC notification with the specified method and parameters. + * + * @param method The method name to be invoked. + * @param params The parameters associated with the method. + * @return A JSON RPC notification object. + */ + public static Notification createNotification(String method, Map params) { + return new Notification("2.0", method, params); + } + + /** + * Represents a JSON RPC request. + * + * @param The type of the request ID. + */ + public record Request(String jsonrpc, T id, String method, Map params) {} + + /** + * Represents a JSON RPC response. + * + * @param The type of the response ID. + */ + public record Response(String jsonrpc, T id, Object result, Object error) {} + + /** + * Represents a JSON RPC notification. + */ + public record Notification(String jsonrpc, String method, Map params) {} +} diff --git a/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Method.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Method.java new file mode 100644 index 00000000..b281aecd --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Method.java @@ -0,0 +1,60 @@ +/*--------------------------------------------------------------------------------------------- + * 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 different methods used in MCP, which are essential for communication and interaction. + * + * @author 季聿阶 + * @since 2025-05-23 + */ +public enum Method { + /** + * Represents the initialization method, used to set up the environment or system. + */ + INITIALIZE("initialize"), + + /** + * Represents the ping method, used to check the availability or connectivity of a service. + */ + PING("ping"), + + /** + * Represents the method to retrieve a list of tools, typically used in tool management. + */ + TOOLS_LIST("tools/list"), + + /** + * Represents the method to call a specific tool, used for executing tool functions. + */ + TOOLS_CALL("tools/call"), + + /** + * Represents the notification method indicating that the system has been initialized. + */ + NOTIFICATION_INITIALIZED("notifications/initialized"), + + /** + * Represents the notification method indicating a change in the list of tools. + */ + NOTIFICATION_TOOLS_CHANGED("notifications/tools/list_changed"); + + private final String code; + + Method(String code) { + this.code = code; + } + + /** + * Returns the code associated with the method. + * + * @return The code of the method. + */ + public String code() { + return this.code; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..9700c823 --- /dev/null +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Server.java @@ -0,0 +1,213 @@ +/*--------------------------------------------------------------------------------------------- + * 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/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Tool.java similarity index 97% rename from framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java rename to framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Tool.java index 7ffe45b9..0b7c0f69 100644 --- a/framework/fel/java/plugins/tool-mcp-server/src/main/java/modelengine/fel/tool/mcp/server/entity/ToolEntity.java +++ b/framework/fel/java/services/tool-mcp-common/src/main/java/modelengine/fel/tool/mcp/entity/Tool.java @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -package modelengine.fel.tool.mcp.server.entity; +package modelengine.fel.tool.mcp.entity; import java.util.Map; @@ -14,7 +14,7 @@ * @author 季聿阶 * @since 2025-05-15 */ -public class ToolEntity { +public class Tool { /** * The name of the tool. * This serves as a unique identifier for the tool within the system. diff --git a/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/support/AbstractTool.java b/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/support/AbstractTool.java index d50157b6..022198d6 100644 --- a/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/support/AbstractTool.java +++ b/framework/fel/java/services/tool-service/src/main/java/modelengine/fel/tool/support/AbstractTool.java @@ -86,8 +86,7 @@ public Object executeWithJsonObject(Map jsonObject) { } private static Object getArg(Object value, Type type) { - if (type instanceof OneOfType) { - OneOfType oneOfType = (OneOfType) type; + if (type instanceof OneOfType oneOfType) { List types = oneOfType.types(); for (Type actualType : types) { try { diff --git a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java index e5efcf4c..aac622dc 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java +++ b/framework/fit/java/fit-builtin/plugins/fit-message-serializer-json-jackson/src/main/java/modelengine/fit/serialization/json/jackson/JacksonObjectSerializer.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.core.StreamReadConstraints; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.introspect.VisibilityChecker; import com.fasterxml.jackson.databind.module.SimpleModule; @@ -85,7 +86,8 @@ public JacksonObjectSerializer(@Value("${date-time-format}") String dateTimeForm .build()).build()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .setAnnotationIntrospector(new FitAnnotationIntrospector()) .setVisibility(visibilityChecker) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); SimpleModule module = new SimpleModule(); module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormat)); module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormat)); diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/entity/serializer/TextEventStreamSerializer.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/entity/serializer/TextEventStreamSerializer.java index 4cc6194b..5d8e7728 100644 --- a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/entity/serializer/TextEventStreamSerializer.java +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/entity/serializer/TextEventStreamSerializer.java @@ -86,7 +86,7 @@ private void emitData(Emitter emitter, BufferedReader reader) throws } private Object deserializeData(String data) { - if (this.type == String.class) { + if (this.type == String.class || this.type == TextEvent.class) { return data; } return this.jsonSerializer.deserialize(data, this.type);