Skip to content

Commit 4a74f58

Browse files
authored
[fel] add FIT MCP server (#133)
1 parent f55dfbf commit 4a74f58

File tree

28 files changed

+1336
-92
lines changed

28 files changed

+1336
-92
lines changed

framework/fel/java/plugins/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<module>tool-discoverer</module>
1717
<module>tool-executor</module>
1818
<module>tool-factory-repository</module>
19+
<module>tool-mcp-server</module>
1920
<module>tool-repository-simple</module>
2021
</modules>
2122
</project>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.fitframework.fel</groupId>
8+
<artifactId>fel-plugin-parent</artifactId>
9+
<version>3.5.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>fel-tool-mcp-server</artifactId>
13+
14+
<dependencies>
15+
<!-- FIT core -->
16+
<dependency>
17+
<groupId>org.fitframework</groupId>
18+
<artifactId>fit-api</artifactId>
19+
</dependency>
20+
<dependency>
21+
<groupId>org.fitframework</groupId>
22+
<artifactId>fit-util</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.fitframework</groupId>
26+
<artifactId>fit-reactor</artifactId>
27+
</dependency>
28+
29+
<dependency>
30+
<groupId>org.fitframework.fel</groupId>
31+
<artifactId>tool-service</artifactId>
32+
</dependency>
33+
34+
<!-- Test -->
35+
<dependency>
36+
<groupId>org.assertj</groupId>
37+
<artifactId>assertj-core</artifactId>
38+
</dependency>
39+
</dependencies>
40+
41+
<build>
42+
<plugins>
43+
<plugin>
44+
<groupId>org.fitframework</groupId>
45+
<artifactId>fit-build-maven-plugin</artifactId>
46+
<version>${fit.version}</version>
47+
<configuration>
48+
<category>system</category>
49+
<level>4</level>
50+
</configuration>
51+
<executions>
52+
<execution>
53+
<id>build-plugin</id>
54+
<goals>
55+
<goal>build-plugin</goal>
56+
</goals>
57+
</execution>
58+
<execution>
59+
<id>package-plugin</id>
60+
<goals>
61+
<goal>package-plugin</goal>
62+
</goals>
63+
</execution>
64+
</executions>
65+
</plugin>
66+
<plugin>
67+
<groupId>org.apache.maven.plugins</groupId>
68+
<artifactId>maven-antrun-plugin</artifactId>
69+
<version>${maven.antrun.version}</version>
70+
<executions>
71+
<execution>
72+
<phase>package</phase>
73+
<configuration>
74+
<target>
75+
<copy file="${project.build.directory}/${project.build.finalName}.jar"
76+
todir="../../../../fit/java/target/plugins"/>
77+
</target>
78+
</configuration>
79+
<goals>
80+
<goal>run</goal>
81+
</goals>
82+
</execution>
83+
</executions>
84+
</plugin>
85+
</plugins>
86+
</build>
87+
</project>
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
import static modelengine.fitframework.inspection.Validation.notBlank;
10+
import static modelengine.fitframework.inspection.Validation.notNull;
11+
12+
import modelengine.fel.tool.mcp.server.entity.JsonRpcEntity;
13+
import modelengine.fel.tool.mcp.server.handler.InitializeHandler;
14+
import modelengine.fel.tool.mcp.server.handler.ToolCallHandler;
15+
import modelengine.fel.tool.mcp.server.handler.ToolListHandler;
16+
import modelengine.fel.tool.mcp.server.handler.UnsupportedMethodHandler;
17+
import modelengine.fit.http.annotation.GetMapping;
18+
import modelengine.fit.http.annotation.PostMapping;
19+
import modelengine.fit.http.annotation.RequestBody;
20+
import modelengine.fit.http.annotation.RequestQuery;
21+
import modelengine.fit.http.entity.TextEvent;
22+
import modelengine.fit.http.server.HttpClassicServerResponse;
23+
import modelengine.fitframework.annotation.Component;
24+
import modelengine.fitframework.annotation.Fit;
25+
import modelengine.fitframework.annotation.Value;
26+
import modelengine.fitframework.flowable.Choir;
27+
import modelengine.fitframework.flowable.Emitter;
28+
import modelengine.fitframework.log.Logger;
29+
import modelengine.fitframework.schedule.ExecutePolicy;
30+
import modelengine.fitframework.schedule.Task;
31+
import modelengine.fitframework.schedule.ThreadPoolScheduler;
32+
import modelengine.fitframework.serialization.ObjectSerializer;
33+
import modelengine.fitframework.util.CollectionUtils;
34+
import modelengine.fitframework.util.MapUtils;
35+
import modelengine.fitframework.util.StringUtils;
36+
import modelengine.fitframework.util.UuidUtils;
37+
38+
import java.util.ArrayList;
39+
import java.util.HashMap;
40+
import java.util.List;
41+
import java.util.Map;
42+
import java.util.concurrent.ConcurrentHashMap;
43+
44+
/**
45+
* FIT MCP Server controller.
46+
*
47+
* @author 季聿阶
48+
* @since 2025-05-13
49+
*/
50+
@Component
51+
public class McpController {
52+
private static final Logger log = Logger.get(McpController.class);
53+
private static final String MESSAGE_PATH = "/mcp/message";
54+
private static final String EVENT_ENDPOINT = "endpoint";
55+
private static final String EVENT_MESSAGE = "message";
56+
private static final String METHOD_INITIALIZE = "initialize";
57+
private static final String METHOD_TOOLS_LIST = "tools/list";
58+
private static final String METHOD_TOOLS_CALL = "tools/call";
59+
private static final String RESPONSE_OK = StringUtils.EMPTY;
60+
61+
private final Map<String, Emitter<TextEvent>> emitters = new ConcurrentHashMap<>();
62+
private final Map<String, HttpClassicServerResponse> responses = new ConcurrentHashMap<>();
63+
private final Map<String, MessageHandler> methodHandlers = new HashMap<>();
64+
private final MessageHandler unsupportedMethodHandler = new UnsupportedMethodHandler();
65+
private final String baseUrl;
66+
private final ObjectSerializer serializer;
67+
68+
/**
69+
* Constructs a new instance of the McpController class.
70+
*
71+
* @param baseUrl The base URL for the MCP server as a {@link String}, used to construct message endpoints.
72+
* @param serializer The JSON serializer used to serialize and deserialize RPC messages, as an
73+
* {@link ObjectSerializer}.
74+
* @param mcpServer The MCP server instance used to handle tool operations such as initialization,
75+
* listing tools, and calling tools, as a {@link McpServer}.
76+
*/
77+
public McpController(@Value("${base-url}") String baseUrl, @Fit(alias = "json") ObjectSerializer serializer,
78+
McpServer mcpServer) {
79+
this.baseUrl = notBlank(baseUrl, "The base URL for MCP server cannot be blank.");
80+
this.serializer = notNull(serializer, "The json serializer cannot be null.");
81+
notNull(mcpServer, "The MCP server cannot be null.");
82+
83+
this.methodHandlers.put(METHOD_INITIALIZE, new InitializeHandler(mcpServer));
84+
this.methodHandlers.put(METHOD_TOOLS_LIST, new ToolListHandler(mcpServer));
85+
this.methodHandlers.put(METHOD_TOOLS_CALL, new ToolCallHandler(mcpServer, this.serializer));
86+
87+
ThreadPoolScheduler channelDetectorScheduler = ThreadPoolScheduler.custom()
88+
.corePoolSize(1)
89+
.isDaemonThread(true)
90+
.threadPoolName("mcp-server-channel-detector")
91+
.build();
92+
channelDetectorScheduler.schedule(Task.builder().policy(ExecutePolicy.fixedDelay(10000)).runnable(() -> {
93+
if (MapUtils.isEmpty(this.responses)) {
94+
return;
95+
}
96+
List<String> toRemoved = new ArrayList<>();
97+
for (Map.Entry<String, HttpClassicServerResponse> entry : this.responses.entrySet()) {
98+
if (entry.getValue().isActive()) {
99+
continue;
100+
}
101+
toRemoved.add(entry.getKey());
102+
}
103+
if (CollectionUtils.isEmpty(toRemoved)) {
104+
return;
105+
}
106+
toRemoved.forEach(this.responses::remove);
107+
toRemoved.forEach(this.emitters::remove);
108+
log.info("Channels are inactive, remove emitters and responses. [sessionIds={}]", toRemoved);
109+
}).build());
110+
}
111+
112+
/**
113+
* Creates a Server-Sent Events (SSE) channel for real-time communication with the client.
114+
*
115+
* <p>This method generates a unique session ID and registers an emitter to send events.</p>
116+
*
117+
* @param response The HTTP server response object used to manage the SSE connection as a
118+
* {@link HttpClassicServerResponse}.
119+
* @return A {@link Choir}{@code <}{@link TextEvent}{@code >} object that emits text events to the connected client.
120+
*/
121+
@GetMapping(path = "/sse")
122+
public Choir<TextEvent> createSse(HttpClassicServerResponse response) {
123+
String sessionId = UuidUtils.randomUuidString();
124+
this.responses.put(sessionId, response);
125+
log.info("New SSE channel for MCP server created. [sessionId={}]", sessionId);
126+
return Choir.create(emitter -> {
127+
emitters.put(sessionId, emitter);
128+
TextEvent textEvent = TextEvent.custom()
129+
.id(sessionId)
130+
.event(EVENT_ENDPOINT)
131+
.data(this.baseUrl + MESSAGE_PATH + "?sessionId=" + sessionId)
132+
.build();
133+
emitter.emit(textEvent);
134+
});
135+
}
136+
137+
/**
138+
* Receives and processes an MCP message via HTTP POST request.
139+
*
140+
* <p>This method handles incoming JSON-RPC requests, routes them to the appropriate handler,
141+
* and returns a response via the associated event emitter.</p>
142+
*
143+
* @param sessionId The session ID used to identify the current client session.
144+
* @param request The JSON-RPC request entity containing the method name and parameters.
145+
* @return Always returns an empty string ({@value #RESPONSE_OK}) to indicate success.
146+
*/
147+
@PostMapping(path = MESSAGE_PATH)
148+
public Object receiveMcpMessage(@RequestQuery(name = "sessionId") String sessionId,
149+
@RequestBody JsonRpcEntity request) {
150+
log.info("Receive MCP message. [sessionId={}, request={}]", sessionId, request);
151+
Object id = request.getId();
152+
if (id == null) {
153+
// Request without an ID indicates a notification message, ignore.
154+
return RESPONSE_OK;
155+
}
156+
MessageHandler handler = this.methodHandlers.getOrDefault(request.getMethod(), this.unsupportedMethodHandler);
157+
JsonRpcEntity response = new JsonRpcEntity();
158+
response.setId(id);
159+
try {
160+
Object result = handler.handle(request.getParams());
161+
response.setResult(result);
162+
} catch (Exception e) {
163+
log.error("Failed to handle MCP message.", e);
164+
response.setError(e.getMessage());
165+
}
166+
String serialized = this.serializer.serialize(response);
167+
TextEvent textEvent = TextEvent.custom().id(sessionId).event(EVENT_MESSAGE).data(serialized).build();
168+
Emitter<TextEvent> emitter = this.emitters.get(sessionId);
169+
emitter.emit(textEvent);
170+
log.info("Send MCP message. [response={}]", serialized);
171+
return RESPONSE_OK;
172+
}
173+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
import modelengine.fel.tool.mcp.server.entity.ToolEntity;
10+
11+
import java.util.List;
12+
import java.util.Map;
13+
14+
/**
15+
* Represents the MCP Server.
16+
*
17+
* @author 季聿阶
18+
* @since 2025-05-15
19+
*/
20+
public interface McpServer {
21+
/**
22+
* Gets MCP Server Info.
23+
*
24+
* @return The MCP Server Info as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}.
25+
*/
26+
Map<String, Object> getInfo();
27+
28+
/**
29+
* Gets MCP Server Tools.
30+
*
31+
* @return The MCP Server Tools as a {@link List}{@code <}{@link ToolEntity}{@code >}.
32+
*/
33+
List<ToolEntity> getTools();
34+
35+
/**
36+
* Calls MCP Server Tool.
37+
*
38+
* @param name The tool name as a {@link String}.
39+
* @param arguments The tool arguments as a {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}.
40+
* @return The tool result as a {@link Object}.
41+
*/
42+
Object callTool(String name, Map<String, Object> arguments);
43+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
import java.util.Map;
10+
11+
/**
12+
* A functional interface for handling messages in the MCP server.
13+
* Implementations of this interface are responsible for processing incoming message requests
14+
* and returning an appropriate response object.
15+
*
16+
* @author 季聿阶
17+
* @since 2025-05-15
18+
*/
19+
public interface MessageHandler {
20+
/**
21+
* Handles the given message request.
22+
*
23+
* @param request A map containing the request parameters and data as a
24+
* {@link Map}{@code <}{@link String}{@code , }{@link Object}{@code >}.
25+
* @return The result of processing the request as an {@link Object}, which can be any type of object.
26+
*/
27+
Object handle(Map<String, Object> request);
28+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
/**
10+
* A base class for all message request types in the MCP server.
11+
* This class serves as a common ancestor for specific message request classes,
12+
* providing a shared structure and type for message handling in the system.
13+
*
14+
* @author 季聿阶
15+
* @since 2025-05-15
16+
*/
17+
public class MessageRequest {}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
3+
* This file is a part of the ModelEngine Project.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*--------------------------------------------------------------------------------------------*/
6+
7+
package modelengine.fel.tool.mcp.server;
8+
9+
/**
10+
* A base class for all message response types in the MCP server.
11+
* This class serves as a common ancestor for specific message response classes,
12+
* providing a shared structure and type for returning results after message processing.
13+
*
14+
* @author 季聿阶
15+
* @since 2025-05-15
16+
*/
17+
public class MessageResponse {}

0 commit comments

Comments
 (0)