Skip to content

Commit 13d8200

Browse files
committed
feat(main): Introduce McpHttpServer and related components for enhanced server functionality
1 parent 32147c7 commit 13d8200

13 files changed

+564
-168
lines changed

src/main/java/com/github/codeboyzhou/mcp/declarative/McpServers.java

Lines changed: 51 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,26 @@
22

33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.github.codeboyzhou.mcp.declarative.annotation.McpComponentScan;
5-
import com.github.codeboyzhou.mcp.declarative.annotation.McpResource;
6-
import com.github.codeboyzhou.mcp.declarative.annotation.McpResources;
7-
import com.github.codeboyzhou.mcp.declarative.annotation.McpTool;
8-
import com.github.codeboyzhou.mcp.declarative.annotation.McpToolParam;
9-
import com.github.codeboyzhou.mcp.declarative.annotation.McpTools;
10-
import com.github.codeboyzhou.mcp.declarative.util.ReflectionHelper;
11-
import io.modelcontextprotocol.server.McpServer;
12-
import io.modelcontextprotocol.server.McpServerFeatures;
5+
import com.github.codeboyzhou.mcp.declarative.listener.DefaultMcpSyncHttpServerStatusListener;
6+
import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener;
7+
import com.github.codeboyzhou.mcp.declarative.server.McpHttpServer;
8+
import com.github.codeboyzhou.mcp.declarative.server.McpServerComponentRegisters;
9+
import com.github.codeboyzhou.mcp.declarative.server.McpServerFactory;
10+
import com.github.codeboyzhou.mcp.declarative.server.McpServerInfo;
11+
import com.github.codeboyzhou.mcp.declarative.server.McpSseServerInfo;
12+
import com.github.codeboyzhou.mcp.declarative.server.McpSyncServerFactory;
1313
import io.modelcontextprotocol.server.McpSyncServer;
1414
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
1515
import io.modelcontextprotocol.server.transport.StdioServerTransportProvider;
16-
import io.modelcontextprotocol.spec.McpSchema;
17-
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
18-
import org.eclipse.jetty.ee10.servlet.ServletHolder;
19-
import org.eclipse.jetty.server.Server;
16+
import io.modelcontextprotocol.spec.McpServerTransportProvider;
2017
import org.reflections.Reflections;
21-
import org.slf4j.Logger;
22-
import org.slf4j.LoggerFactory;
23-
24-
import java.lang.reflect.Method;
25-
import java.lang.reflect.Parameter;
26-
import java.util.ArrayList;
27-
import java.util.HashMap;
28-
import java.util.List;
29-
import java.util.Map;
30-
import java.util.Set;
3118

3219
public class McpServers {
3320

34-
private static final Logger logger = LoggerFactory.getLogger(McpServers.class);
35-
3621
private static final McpServers INSTANCE = new McpServers();
3722

38-
private static final McpSchema.ServerCapabilities DEFAULT_SERVER_CAPABILITIES = McpSchema.ServerCapabilities
39-
.builder()
40-
.resources(true, true)
41-
.prompts(true)
42-
.tools(true)
43-
.build();
44-
4523
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
4624

47-
private static final String OBJECT_TYPE_NAME = Object.class.getSimpleName().toLowerCase();
48-
4925
private static final String DEFAULT_MESSAGE_ENDPOINT = "/message";
5026

5127
private static final String DEFAULT_SSE_ENDPOINT = "/sse";
@@ -72,150 +48,64 @@ private static String determineBasePackage(McpComponentScan scan, Class<?> appli
7248
return applicationMainClass.getPackageName();
7349
}
7450

75-
public void startSyncStdioServer(String name, String version) {
76-
McpSyncServer server = McpServer.sync(new StdioServerTransportProvider())
77-
.capabilities(DEFAULT_SERVER_CAPABILITIES)
78-
.serverInfo(name, version)
79-
.build();
51+
public void startSyncStdioServer(String name, String version, String instructions) {
52+
McpServerFactory<McpSyncServer> factory = new McpSyncServerFactory();
53+
McpServerInfo serverInfo = McpServerInfo.builder().name(name).version(version).instructions(instructions).build();
54+
McpServerTransportProvider transportProvider = new StdioServerTransportProvider();
55+
McpSyncServer server = factory.create(serverInfo, transportProvider);
56+
McpServerComponentRegisters.registerAllTo(server, reflections);
57+
}
8058

81-
registerResources(server);
82-
registerTools(server);
59+
@Deprecated(since = "0.2.0")
60+
public void startSyncStdioServer(String name, String version) {
61+
startSyncStdioServer(name, version, "You are using a deprecated API with default server instructions");
8362
}
8463

85-
public void startSyncSseServer(String name, String version) {
86-
startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, DEFAULT_HTTP_SERVER_PORT);
64+
public void startSyncSseServer(McpSseServerInfo serverInfo, McpHttpServerStatusListener<McpSyncServer> listener) {
65+
McpServerFactory<McpSyncServer> factory = new McpSyncServerFactory();
66+
HttpServletSseServerTransportProvider transportProvider = new HttpServletSseServerTransportProvider(
67+
OBJECT_MAPPER, serverInfo.baseUrl(), serverInfo.messageEndpoint(), serverInfo.sseEndpoint()
68+
);
69+
McpSyncServer server = factory.create(serverInfo, transportProvider);
70+
McpServerComponentRegisters.registerAllTo(server, reflections);
71+
McpHttpServer<McpSyncServer> httpServer = new McpHttpServer<>();
72+
httpServer.with(transportProvider).with(serverInfo).with(listener).attach(server).start();
8773
}
8874

89-
public void startSyncSseServer(String name, String version, int port) {
90-
startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, port);
75+
public void startSyncSseServer(McpSseServerInfo serverInfo) {
76+
startSyncSseServer(serverInfo, new DefaultMcpSyncHttpServerStatusListener());
9177
}
9278

79+
@Deprecated(since = "0.2.0")
9380
public void startSyncSseServer(String name, String version, String messageEndpoint, String sseEndpoint, int port) {
94-
HttpServletSseServerTransportProvider transport = new HttpServletSseServerTransportProvider(
95-
OBJECT_MAPPER, messageEndpoint, sseEndpoint
96-
);
97-
98-
McpSyncServer server = McpServer.sync(transport)
99-
.capabilities(DEFAULT_SERVER_CAPABILITIES)
100-
.serverInfo(name, version)
81+
McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version)
82+
.instructions("You are using a deprecated API with default server instructions")
83+
.baseUrl("").messageEndpoint(messageEndpoint).sseEndpoint(sseEndpoint).port(port)
10184
.build();
10285

103-
registerResources(server);
104-
registerTools(server);
105-
106-
startHttpServer(server, transport, port);
86+
startSyncSseServer(serverInfo);
10787
}
10888

109-
private void startHttpServer(McpSyncServer server, HttpServletSseServerTransportProvider transport, int port) {
110-
ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS);
111-
servletContextHandler.setContextPath("/");
112-
113-
ServletHolder servletHolder = new ServletHolder(transport);
114-
servletContextHandler.addServlet(servletHolder, "/*");
115-
116-
Server httpserver = new Server(port);
117-
httpserver.setHandler(servletContextHandler);
118-
119-
try {
120-
httpserver.start();
121-
logger.info("Jetty-based HTTP server started on http://127.0.0.1:{}", port);
122-
123-
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
124-
try {
125-
logger.info("Shutting down HTTP server");
126-
httpserver.stop();
127-
server.close();
128-
} catch (Exception e) {
129-
logger.error("Error stopping HTTP server", e);
130-
}
131-
}));
132-
133-
// Wait for the HTTP server to stop
134-
httpserver.join();
135-
} catch (Exception e) {
136-
logger.error("Error starting HTTP server on http://127.0.0.1:{}", port, e);
137-
server.close();
138-
}
139-
}
140-
141-
private void registerResources(McpSyncServer server) {
142-
Set<Class<?>> resourceClasses = reflections.getTypesAnnotatedWith(McpResources.class);
143-
for (Class<?> resourceClass : resourceClasses) {
144-
Set<Method> methods = ReflectionHelper.getMethodsAnnotatedWith(resourceClass, McpResource.class);
145-
for (Method method : methods) {
146-
McpResource resourceMethod = method.getAnnotation(McpResource.class);
147-
McpSchema.Resource resource = new McpSchema.Resource(
148-
resourceMethod.uri(),
149-
resourceMethod.name().isBlank() ? method.getName() : resourceMethod.name(),
150-
resourceMethod.description(),
151-
resourceMethod.mimeType(),
152-
new McpSchema.Annotations(List.of(resourceMethod.roles()), resourceMethod.priority())
153-
);
154-
server.addResource(new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> {
155-
Object result;
156-
try {
157-
result = ReflectionHelper.invokeMethod(resourceClass, method);
158-
} catch (Throwable e) {
159-
logger.error("Error invoking resource method", e);
160-
result = e + ": " + e.getMessage();
161-
}
162-
McpSchema.ResourceContents contents = new McpSchema.TextResourceContents(
163-
resource.uri(), resource.mimeType(), result.toString()
164-
);
165-
return new McpSchema.ReadResourceResult(List.of(contents));
166-
}));
167-
}
168-
}
169-
}
89+
@Deprecated(since = "0.2.0")
90+
public void startSyncSseServer(String name, String version, int port) {
91+
McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version)
92+
.instructions("You are using a deprecated API with default server instructions")
93+
.baseUrl("").messageEndpoint(DEFAULT_MESSAGE_ENDPOINT).sseEndpoint(DEFAULT_SSE_ENDPOINT)
94+
.port(port)
95+
.build();
17096

171-
private void registerTools(McpSyncServer server) {
172-
Set<Class<?>> toolClasses = reflections.getTypesAnnotatedWith(McpTools.class);
173-
for (Class<?> toolClass : toolClasses) {
174-
Set<Method> methods = ReflectionHelper.getMethodsAnnotatedWith(toolClass, McpTool.class);
175-
for (Method method : methods) {
176-
McpTool toolMethod = method.getAnnotation(McpTool.class);
177-
McpSchema.JsonSchema paramSchema = createJsonSchema(method);
178-
final String toolName = toolMethod.name().isBlank() ? method.getName() : toolMethod.name();
179-
McpSchema.Tool tool = new McpSchema.Tool(toolName, toolMethod.description(), paramSchema);
180-
server.addTool(new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> {
181-
Object result;
182-
boolean isError = false;
183-
try {
184-
result = ReflectionHelper.invokeMethod(toolClass, method, paramSchema, params);
185-
} catch (Throwable e) {
186-
logger.error("Error invoking tool method", e);
187-
result = e + ": " + e.getMessage();
188-
isError = true;
189-
}
190-
McpSchema.Content content = new McpSchema.TextContent(result.toString());
191-
return new McpSchema.CallToolResult(List.of(content), isError);
192-
}));
193-
}
194-
}
97+
startSyncSseServer(serverInfo);
19598
}
19699

197-
private McpSchema.JsonSchema createJsonSchema(Method method) {
198-
Map<String, Object> properties = new HashMap<>();
199-
List<String> required = new ArrayList<>();
200-
201-
Set<Parameter> parameters = ReflectionHelper.getParametersAnnotatedWith(method, McpToolParam.class);
202-
for (Parameter parameter : parameters) {
203-
McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class);
204-
final String parameterName = toolParam.name();
205-
final String parameterType = parameter.getType().getName().toLowerCase();
206-
207-
Map<String, String> parameterProperties = Map.of(
208-
"type", parameterType,
209-
"description", toolParam.description()
210-
);
211-
properties.put(parameterName, parameterProperties);
212-
213-
if (toolParam.required()) {
214-
required.add(parameterName);
215-
}
216-
}
100+
@Deprecated(since = "0.2.0")
101+
public void startSyncSseServer(String name, String version) {
102+
McpSseServerInfo serverInfo = McpSseServerInfo.builder().name(name).version(version)
103+
.instructions("You are using a deprecated API with default server instructions")
104+
.baseUrl("").messageEndpoint(DEFAULT_MESSAGE_ENDPOINT).sseEndpoint(DEFAULT_SSE_ENDPOINT)
105+
.port(DEFAULT_HTTP_SERVER_PORT)
106+
.build();
217107

218-
return new McpSchema.JsonSchema(OBJECT_TYPE_NAME, properties, required, false);
108+
startSyncSseServer(serverInfo);
219109
}
220110

221111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.github.codeboyzhou.mcp.declarative.listener;
2+
3+
import io.modelcontextprotocol.server.McpSyncServer;
4+
import io.modelcontextprotocol.spec.McpSchema;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
7+
8+
public class DefaultMcpSyncHttpServerStatusListener implements McpHttpServerStatusListener<McpSyncServer> {
9+
10+
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpSyncHttpServerStatusListener.class);
11+
12+
@Override
13+
public void onStarted(McpSyncServer mcpServer) {
14+
McpSchema.Implementation serverInfo = mcpServer.getServerInfo();
15+
logger.info("MCP server [{}] {} started successfully in HTTP SSE mode", serverInfo.name(), serverInfo.version());
16+
}
17+
18+
@Override
19+
public void onStopped(McpSyncServer mcpServer) {
20+
mcpServer.closeGracefully();
21+
McpSchema.Implementation serverInfo = mcpServer.getServerInfo();
22+
logger.info("MCP server [{}] {} closed gracefully", serverInfo.name(), serverInfo.version());
23+
}
24+
25+
@Override
26+
public void onError(McpSyncServer mcpServer, Throwable throwable) {
27+
mcpServer.close();
28+
McpSchema.Implementation serverInfo = mcpServer.getServerInfo();
29+
logger.info("MCP server [{}] {} closed", serverInfo.name(), serverInfo.version());
30+
}
31+
32+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.github.codeboyzhou.mcp.declarative.listener;
2+
3+
public interface McpHttpServerStatusListener<T> {
4+
5+
void onStarted(T mcpServer);
6+
7+
void onStopped(T mcpServer);
8+
9+
void onError(T mcpServer, Throwable throwable);
10+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package com.github.codeboyzhou.mcp.declarative.server;
2+
3+
import com.github.codeboyzhou.mcp.declarative.listener.McpHttpServerStatusListener;
4+
import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider;
5+
import io.modelcontextprotocol.util.Assert;
6+
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
7+
import org.eclipse.jetty.ee10.servlet.ServletHolder;
8+
import org.eclipse.jetty.server.Server;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
12+
public class McpHttpServer<T> {
13+
14+
private static final Logger logger = LoggerFactory.getLogger(McpHttpServer.class);
15+
16+
private static final String DEFAULT_SERVLET_CONTEXT_PATH = "/";
17+
18+
private static final String DEFAULT_SERVLET_PATH = "/*";
19+
20+
private HttpServletSseServerTransportProvider transportProvider;
21+
22+
private McpSseServerInfo serverInfo;
23+
24+
private McpHttpServerStatusListener<T> statusListener;
25+
26+
private T mcpServer;
27+
28+
public McpHttpServer<T> with(HttpServletSseServerTransportProvider transportProvider) {
29+
Assert.notNull(transportProvider, "transportProvider cannot be null");
30+
this.transportProvider = transportProvider;
31+
return this;
32+
}
33+
34+
public McpHttpServer<T> with(McpSseServerInfo serverInfo) {
35+
Assert.notNull(serverInfo, "serverInfo cannot be null");
36+
this.serverInfo = serverInfo;
37+
return this;
38+
}
39+
40+
public McpHttpServer<T> with(McpHttpServerStatusListener<T> statusListener) {
41+
Assert.notNull(statusListener, "statusListener cannot be null");
42+
this.statusListener = statusListener;
43+
return this;
44+
}
45+
46+
public McpHttpServer<T> attach(T mcpServer) {
47+
Assert.notNull(mcpServer, "mcpServer cannot be null");
48+
this.mcpServer = mcpServer;
49+
return this;
50+
}
51+
52+
public void start() {
53+
ServletContextHandler handler = new ServletContextHandler(ServletContextHandler.SESSIONS);
54+
handler.setContextPath(DEFAULT_SERVLET_CONTEXT_PATH);
55+
56+
ServletHolder servletHolder = new ServletHolder(transportProvider);
57+
handler.addServlet(servletHolder, DEFAULT_SERVLET_PATH);
58+
59+
Server httpserver = new Server(serverInfo.port());
60+
httpserver.setHandler(handler);
61+
62+
try {
63+
httpserver.start();
64+
logger.info("Jetty-based HTTP server started on http://127.0.0.1:{}", serverInfo.port());
65+
66+
// Notify the listener that the server has started
67+
statusListener.onStarted(mcpServer);
68+
69+
// Add a shutdown hook to stop the HTTP server and MCP server gracefully
70+
addShutdownHook(httpserver);
71+
72+
// Wait for the HTTP server to stop
73+
httpserver.join();
74+
} catch (Exception e) {
75+
logger.error("Error starting HTTP server on http://127.0.0.1:{}", serverInfo.port(), e);
76+
statusListener.onError(mcpServer, e);
77+
}
78+
}
79+
80+
private void addShutdownHook(Server httpserver) {
81+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
82+
try {
83+
logger.info("Shutting down HTTP server and MCP server");
84+
httpserver.stop();
85+
statusListener.onStopped(mcpServer);
86+
} catch (Exception e) {
87+
logger.error("Error stopping HTTP server and MCP server", e);
88+
}
89+
}));
90+
}
91+
92+
}

0 commit comments

Comments
 (0)