|
| 1 | +package com.github.codeboyzhou.mcp.declarative; |
| 2 | + |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 4 | +import com.github.codeboyzhou.mcp.declarative.annotation.*; |
| 5 | +import com.github.codeboyzhou.mcp.declarative.util.Annotations; |
| 6 | +import io.modelcontextprotocol.server.McpServer; |
| 7 | +import io.modelcontextprotocol.server.McpServerFeatures; |
| 8 | +import io.modelcontextprotocol.server.McpSyncServer; |
| 9 | +import io.modelcontextprotocol.server.transport.HttpServletSseServerTransportProvider; |
| 10 | +import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; |
| 11 | +import io.modelcontextprotocol.spec.McpSchema; |
| 12 | +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; |
| 13 | +import org.eclipse.jetty.ee10.servlet.ServletHolder; |
| 14 | +import org.eclipse.jetty.server.Server; |
| 15 | +import org.reflections.Reflections; |
| 16 | +import org.slf4j.Logger; |
| 17 | +import org.slf4j.LoggerFactory; |
| 18 | + |
| 19 | +import java.lang.reflect.Method; |
| 20 | +import java.lang.reflect.Parameter; |
| 21 | +import java.util.*; |
| 22 | + |
| 23 | +public class McpServers { |
| 24 | + |
| 25 | + private static final Logger logger = LoggerFactory.getLogger(McpServers.class); |
| 26 | + |
| 27 | + private static final McpSchema.ServerCapabilities DEFAULT_SERVER_CAPABILITIES = McpSchema.ServerCapabilities |
| 28 | + .builder() |
| 29 | + .resources(true, true) |
| 30 | + .prompts(true) |
| 31 | + .tools(true) |
| 32 | + .build(); |
| 33 | + |
| 34 | + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); |
| 35 | + |
| 36 | + private static final String OBJECT_TYPE_NAME = Object.class.getName().toLowerCase(); |
| 37 | + |
| 38 | + private static final Reflections REFLECTIONS = new Reflections(); |
| 39 | + |
| 40 | + private static final String DEFAULT_MESSAGE_ENDPOINT = "/message"; |
| 41 | + |
| 42 | + private static final String DEFAULT_SSE_ENDPOINT = "/sse"; |
| 43 | + |
| 44 | + private static final int DEFAULT_HTTP_SERVER_PORT = 8080; |
| 45 | + |
| 46 | + public static void startSyncStdioServer(String name, String version) { |
| 47 | + McpSyncServer server = McpServer.sync(new StdioServerTransportProvider()) |
| 48 | + .capabilities(DEFAULT_SERVER_CAPABILITIES) |
| 49 | + .serverInfo(name, version) |
| 50 | + .build(); |
| 51 | + |
| 52 | + registerResources(server); |
| 53 | + registerTools(server); |
| 54 | + } |
| 55 | + |
| 56 | + public static void startSyncSseServer(String name, String version) { |
| 57 | + startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, DEFAULT_HTTP_SERVER_PORT); |
| 58 | + } |
| 59 | + |
| 60 | + public static void startSyncSseServer(String name, String version, int port) { |
| 61 | + startSyncSseServer(name, version, DEFAULT_MESSAGE_ENDPOINT, DEFAULT_SSE_ENDPOINT, port); |
| 62 | + } |
| 63 | + |
| 64 | + public static void startSyncSseServer(String name, String version, String messageEndpoint, String sseEndpoint, int port) { |
| 65 | + HttpServletSseServerTransportProvider transport = new HttpServletSseServerTransportProvider( |
| 66 | + OBJECT_MAPPER, messageEndpoint, sseEndpoint |
| 67 | + ); |
| 68 | + |
| 69 | + McpSyncServer server = McpServer.sync(transport) |
| 70 | + .capabilities(DEFAULT_SERVER_CAPABILITIES) |
| 71 | + .serverInfo(name, version) |
| 72 | + .build(); |
| 73 | + |
| 74 | + registerResources(server); |
| 75 | + registerTools(server); |
| 76 | + |
| 77 | + startHttpServer(server, transport, port); |
| 78 | + } |
| 79 | + |
| 80 | + private static void startHttpServer(McpSyncServer server, HttpServletSseServerTransportProvider transport, int port) { |
| 81 | + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); |
| 82 | + servletContextHandler.setContextPath("/"); |
| 83 | + |
| 84 | + ServletHolder servletHolder = new ServletHolder(transport); |
| 85 | + servletContextHandler.addServlet(servletHolder, "/*"); |
| 86 | + |
| 87 | + Server httpserver = new Server(port); |
| 88 | + httpserver.setHandler(servletContextHandler); |
| 89 | + |
| 90 | + try { |
| 91 | + httpserver.start(); |
| 92 | + logger.info("Jetty-based HTTP server started on http://127.0.0.1:{}", port); |
| 93 | + |
| 94 | + Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
| 95 | + try { |
| 96 | + logger.info("Shutting down HTTP server"); |
| 97 | + httpserver.stop(); |
| 98 | + server.close(); |
| 99 | + } catch (Exception e) { |
| 100 | + logger.error("Error stopping HTTP server", e); |
| 101 | + } |
| 102 | + })); |
| 103 | + |
| 104 | + // Wait for the HTTP server to stop |
| 105 | + httpserver.join(); |
| 106 | + } catch (Exception e) { |
| 107 | + logger.error("Error starting HTTP server on http://127.0.0.1:{}", port, e); |
| 108 | + server.close(); |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + private static void registerResources(McpSyncServer server) { |
| 113 | + Set<Class<?>> resourceClasses = REFLECTIONS.getTypesAnnotatedWith(McpResources.class); |
| 114 | + for (Class<?> resourceClass : resourceClasses) { |
| 115 | + Set<Method> methods = Annotations.getMethodsAnnotatedWith(resourceClass, McpResource.class); |
| 116 | + for (Method method : methods) { |
| 117 | + McpResource resourceMethod = method.getAnnotation(McpResource.class); |
| 118 | + McpSchema.Resource resource = new McpSchema.Resource( |
| 119 | + resourceMethod.uri(), |
| 120 | + resourceMethod.name(), |
| 121 | + resourceMethod.description(), |
| 122 | + resourceMethod.mimeType(), |
| 123 | + new McpSchema.Annotations(List.of(resourceMethod.roles()), resourceMethod.priority()) |
| 124 | + ); |
| 125 | + server.addResource(new McpServerFeatures.SyncResourceSpecification(resource, (exchange, request) -> { |
| 126 | + Object result; |
| 127 | + try { |
| 128 | + Object resourceObject = resourceClass.getDeclaredConstructor().newInstance(); |
| 129 | + result = method.invoke(resourceObject); |
| 130 | + } catch (Exception e) { |
| 131 | + result = e + ": " + e.getMessage(); |
| 132 | + } |
| 133 | + McpSchema.ResourceContents contents = new McpSchema.TextResourceContents( |
| 134 | + resource.uri(), resource.mimeType(), result.toString() |
| 135 | + ); |
| 136 | + return new McpSchema.ReadResourceResult(List.of(contents)); |
| 137 | + })); |
| 138 | + } |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + private static void registerTools(McpSyncServer server) { |
| 143 | + Set<Class<?>> toolClasses = REFLECTIONS.getTypesAnnotatedWith(McpTools.class); |
| 144 | + for (Class<?> toolClass : toolClasses) { |
| 145 | + Set<Method> methods = Annotations.getMethodsAnnotatedWith(toolClass, McpTool.class); |
| 146 | + for (Method method : methods) { |
| 147 | + McpTool toolMethod = method.getAnnotation(McpTool.class); |
| 148 | + McpSchema.JsonSchema paramSchema = createJsonSchema(method); |
| 149 | + McpSchema.Tool tool = new McpSchema.Tool(toolMethod.name(), toolMethod.description(), paramSchema); |
| 150 | + server.addTool(new McpServerFeatures.SyncToolSpecification(tool, (exchange, params) -> { |
| 151 | + Object result; |
| 152 | + boolean isError = false; |
| 153 | + try { |
| 154 | + Object toolObject = toolClass.getDeclaredConstructor().newInstance(); |
| 155 | + result = method.invoke(toolObject, params.values()); |
| 156 | + } catch (Exception e) { |
| 157 | + result = e + ": " + e.getMessage(); |
| 158 | + isError = true; |
| 159 | + } |
| 160 | + McpSchema.Content content = new McpSchema.TextContent(result.toString()); |
| 161 | + return new McpSchema.CallToolResult(List.of(content), isError); |
| 162 | + })); |
| 163 | + } |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + private static McpSchema.JsonSchema createJsonSchema(Method method) { |
| 168 | + Map<String, Object> properties = new HashMap<>(); |
| 169 | + List<String> required = new ArrayList<>(); |
| 170 | + |
| 171 | + Set<Parameter> parameters = Annotations.getParametersAnnotatedWith(method, McpToolParam.class); |
| 172 | + for (Parameter parameter : parameters) { |
| 173 | + final String parameterName = parameter.getName(); |
| 174 | + final String parameterType = parameter.getType().getName().toLowerCase(); |
| 175 | + McpToolParam toolParam = parameter.getAnnotation(McpToolParam.class); |
| 176 | + |
| 177 | + Map<String, String> parameterProperties = Map.of( |
| 178 | + "type", parameterType, |
| 179 | + "description", toolParam.description() |
| 180 | + ); |
| 181 | + properties.put(parameterName, parameterProperties); |
| 182 | + |
| 183 | + if (toolParam.required()) { |
| 184 | + required.add(parameterName); |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + return new McpSchema.JsonSchema(OBJECT_TYPE_NAME, properties, required, false); |
| 189 | + } |
| 190 | + |
| 191 | +} |
0 commit comments