diff --git a/server-core/src/main/java/io/onedev/server/mcp/MCPModule.java b/server-core/src/main/java/io/onedev/server/mcp/MCPModule.java deleted file mode 100644 index 5034240f08..0000000000 --- a/server-core/src/main/java/io/onedev/server/mcp/MCPModule.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.onedev.server.mcp; - -import io.onedev.commons.loader.AbstractPluginModule; -import io.onedev.server.jetty.ServletConfigurator; - -/** - * Module for the MCP (Model Context Protocol) server. - * - * This module configures the dependency injection for MCP server components - * and registers the servlet configurator with the Jetty server. - */ -public class MCPModule extends AbstractPluginModule { - - @Override - protected void configure() { - super.configure(); - - // Bind the MCP server servlet as a singleton - bind(MCPServerServlet.class).asEagerSingleton(); - - // Contribute the MCP servlet configurator to the ServletConfigurator extension point - contribute(ServletConfigurator.class, MCPServletConfigurator.class); - } -} \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java b/server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java index d9c4fdb30f..0f52500fc6 100644 --- a/server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java +++ b/server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java @@ -1,23 +1,28 @@ package io.onedev.server.mcp; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import io.onedev.commons.utils.ExceptionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import io.onedev.server.util.IOUtils; /** * Example MCP (Model Context Protocol) Server Servlet @@ -30,16 +35,19 @@ import java.util.concurrent.ConcurrentHashMap; * * The MCP server provides access to OneDev server information and basic operations. */ +@Singleton public class MCPServerServlet extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(MCPServerServlet.class); - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper; // In-memory storage for demo purposes private final Map resources = new ConcurrentHashMap<>(); - public MCPServerServlet() { + @Inject + public MCPServerServlet(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; // Initialize with some example resources initializeExampleResources(); } @@ -71,28 +79,30 @@ public class MCPServerServlet extends HttpServlet { response.setCharacterEncoding("UTF-8"); try { - // Read request body - StringBuilder requestBody = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - requestBody.append(line); - } - } - - JsonNode requestNode = objectMapper.readTree(requestBody.toString()); + var os = new ByteArrayOutputStream(); + try (var is = request.getInputStream()) { + IOUtils.copy(is, os, IOUtils.BUFFER_SIZE); + } + + System.out.println(os.toString(StandardCharsets.UTF_8)); + + JsonNode requestNode = objectMapper.readTree(os.toString(StandardCharsets.UTF_8)); String method = requestNode.get("method").asText(); JsonNode params = requestNode.get("params"); - String id = requestNode.get("id").asText(); + Long id = requestNode.has("id")?requestNode.get("id").asLong():null; JsonNode result = handleRequest(method, params); // Send response ObjectNode responseNode = objectMapper.createObjectNode(); responseNode.put("jsonrpc", "2.0"); - responseNode.put("id", id); + + if (id != null) + responseNode.put("id", id); responseNode.set("result", result); + System.out.println(objectMapper.writeValueAsString(responseNode)); + try (PrintWriter writer = response.getWriter()) { writer.write(objectMapper.writeValueAsString(responseNode)); } @@ -107,6 +117,8 @@ public class MCPServerServlet extends HttpServlet { switch (method) { case "initialize": return handleInitialize(params); + case "notifications/initialized": + return handleNotificationInitialized(params); case "resources/list": return handleListResources(params); case "resources/read": @@ -123,13 +135,25 @@ public class MCPServerServlet extends HttpServlet { private JsonNode handleInitialize(JsonNode params) { ObjectNode result = objectMapper.createObjectNode(); result.put("protocolVersion", "2024-11-05"); - result.put("capabilities", objectMapper.createObjectNode()); - result.put("serverInfo", objectMapper.createObjectNode() + + // Declare server capabilities + ObjectNode capabilities = objectMapper.createObjectNode(); + capabilities.set("tools", objectMapper.createObjectNode()); + capabilities.set("resources", objectMapper.createObjectNode()); + result.set("capabilities", capabilities); + + result.set("serverInfo", objectMapper.createObjectNode() .put("name", "OneDev MCP Server") .put("version", "1.0.0")); return result; } + private JsonNode handleNotificationInitialized(JsonNode params) { + ObjectNode result = objectMapper.createObjectNode(); + result.put("status", "initialized"); + return result; + } + private JsonNode handleListResources(JsonNode params) { ArrayNode resourcesArray = objectMapper.createArrayNode(); @@ -170,20 +194,34 @@ public class MCPServerServlet extends HttpServlet { ObjectNode serverStatusTool = objectMapper.createObjectNode(); serverStatusTool.put("name", "getServerStatus"); serverStatusTool.put("description", "Get the current status of the OneDev server"); - serverStatusTool.set("inputSchema", objectMapper.createObjectNode()); + + // Input schema for getServerStatus + ObjectNode serverStatusInputSchema = objectMapper.createObjectNode(); + serverStatusInputSchema.put("type", "object"); + ObjectNode properties = objectMapper.createObjectNode(); + serverStatusInputSchema.set("properties", properties); + serverStatusTool.set("inputSchema", serverStatusInputSchema); + toolsArray.add(serverStatusTool); ObjectNode createResourceTool = objectMapper.createObjectNode(); createResourceTool.put("name", "createResource"); createResourceTool.put("description", "Create a new resource"); - ObjectNode inputSchema = objectMapper.createObjectNode(); - inputSchema.put("type", "object"); - ObjectNode properties = objectMapper.createObjectNode(); - properties.put("uri", objectMapper.createObjectNode().put("type", "string")); - properties.put("content", objectMapper.createObjectNode().put("type", "object")); - inputSchema.set("properties", properties); - inputSchema.set("required", objectMapper.createArrayNode().add("uri").add("content")); - createResourceTool.set("inputSchema", inputSchema); + + // Input schema for createResource + ObjectNode createResourceInputSchema = objectMapper.createObjectNode(); + createResourceInputSchema.put("type", "object"); + properties = objectMapper.createObjectNode(); + properties.set("uri", objectMapper.createObjectNode() + .put("type", "string") + .put("description", "URI identifier for the new resource")); + properties.set("content", objectMapper.createObjectNode() + .put("type", "object") + .put("description", "Content object to store in the resource")); + createResourceInputSchema.set("properties", properties); + createResourceInputSchema.set("required", objectMapper.createArrayNode().add("uri").add("content")); + createResourceTool.set("inputSchema", createResourceInputSchema); + toolsArray.add(createResourceTool); ObjectNode result = objectMapper.createObjectNode(); @@ -194,23 +232,35 @@ public class MCPServerServlet extends HttpServlet { private JsonNode handleCallTool(JsonNode params) { String name = params.get("name").asText(); JsonNode arguments = params.get("arguments"); - + + JsonNode toolResult; switch (name) { case "getServerStatus": - return handleGetServerStatus(arguments); + toolResult = handleGetServerStatus(arguments); + break; case "createResource": - return handleCreateResource(arguments); + toolResult = handleCreateResource(arguments); + break; default: throw new IllegalArgumentException("Unknown tool: " + name); } + + // Format the response according to MCP specification + ObjectNode result = objectMapper.createObjectNode(); + ArrayNode contentArray = objectMapper.createArrayNode(); + + ObjectNode contentItem = objectMapper.createObjectNode(); + contentItem.put("type", "text"); + contentItem.put("text", toolResult.toString()); + contentArray.add(contentItem); + + result.set("content", contentArray); + return result; } - + private JsonNode handleGetServerStatus(JsonNode arguments) { ObjectNode result = objectMapper.createObjectNode(); result.put("status", "running"); - result.put("uptime", System.currentTimeMillis()); - result.put("memoryUsage", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()); - result.put("totalMemory", Runtime.getRuntime().totalMemory()); return result; } @@ -251,65 +301,10 @@ public class MCPServerServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.setContentType("text/html"); - response.setCharacterEncoding("UTF-8"); - - try (PrintWriter writer = response.getWriter()) { - writer.write("\n" + - "\n" + - "\n" + - " OneDev MCP Server\n" + - " \n" + - "\n" + - "\n" + - "
\n" + - "

OneDev MCP Server

\n" + - "

This is an example Model Context Protocol (MCP) server for OneDev.

\n" + - " \n" + - "
\n" + - "
POST /mcp
\n" + - "
\n" + - " Main MCP endpoint that handles all protocol requests including:\n" + - "
    \n" + - "
  • initialize - Initialize the MCP connection
  • \n" + - "
  • resources/list - List available resources
  • \n" + - "
  • resources/read - Read a specific resource
  • \n" + - "
  • tools/list - List available tools
  • \n" + - "
  • tools/call - Call a specific tool
  • \n" + - "
\n" + - "
\n" + - "
\n" + - " \n" + - "

Available Resources

\n" + - " \n" + - " \n" + - "

Available Tools

\n" + - " \n" + - " \n" + - "

Example Request

\n" + - "
\n" +
-                "{\n" +
-                "  \"jsonrpc\": \"2.0\",\n" +
-                "  \"id\": 1,\n" +
-                "  \"method\": \"resources/list\",\n" +
-                "  \"params\": {}\n" +
-                "}\n" +
-                "        
\n" + - "
\n" + - "\n" + - ""); + response.setContentType("text/plain"); + try (var os = response.getOutputStream()) { + os.println("OneDev MCP server"); } + response.setStatus(HttpServletResponse.SC_OK); } } \ No newline at end of file diff --git a/server-core/src/main/java/io/onedev/server/mcp/MCPServletConfigurator.java b/server-core/src/main/java/io/onedev/server/mcp/MCPServletConfigurator.java deleted file mode 100644 index a6fb76eaef..0000000000 --- a/server-core/src/main/java/io/onedev/server/mcp/MCPServletConfigurator.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.onedev.server.mcp; - -import io.onedev.server.jetty.ServletConfigurator; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Servlet configurator for the MCP (Model Context Protocol) server. - * - * This configurator registers the MCP servlet with the Jetty server - * to handle MCP protocol requests at the /mcp endpoint. - */ -@Singleton -public class MCPServletConfigurator implements ServletConfigurator { - - private final MCPServerServlet mcpServerServlet; - - @Inject - public MCPServletConfigurator(MCPServerServlet mcpServerServlet) { - this.mcpServerServlet = mcpServerServlet; - } - - @Override - public void configure(ServletContextHandler context) { - // Register the MCP servlet at the /mcp path - ServletHolder mcpServletHolder = new ServletHolder(mcpServerServlet); - context.addServlet(mcpServletHolder, "/mcp"); - - // Also register at /mcp/* to handle all MCP-related requests - context.addServlet(mcpServletHolder, "/mcp/*"); - } -} \ No newline at end of file diff --git a/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java b/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java index 21846c966c..4d20288ab3 100644 --- a/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java +++ b/server-product/src/main/java/io/onedev/server/product/ProductServletConfigurator.java @@ -1,5 +1,22 @@ package io.onedev.server.product; +import java.io.File; +import java.util.EnumSet; + +import javax.inject.Inject; +import javax.servlet.DispatcherType; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; + +import org.apache.shiro.web.env.EnvironmentLoader; +import org.apache.shiro.web.env.EnvironmentLoaderListener; +import org.apache.shiro.web.servlet.ShiroFilter; +import org.apache.wicket.protocol.http.WicketServlet; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.glassfish.jersey.servlet.ServletContainer; + import io.onedev.commons.bootstrap.Bootstrap; import io.onedev.server.OneDev; import io.onedev.server.agent.ServerSocketServlet; @@ -11,26 +28,12 @@ import io.onedev.server.git.hook.GitPreReceiveCallback; import io.onedev.server.jetty.ClasspathAssetServlet; import io.onedev.server.jetty.FileAssetServlet; import io.onedev.server.jetty.ServletConfigurator; +import io.onedev.server.mcp.MCPServerServlet; import io.onedev.server.security.CorsFilter; import io.onedev.server.security.DefaultWebEnvironment; import io.onedev.server.web.asset.icon.IconScope; import io.onedev.server.web.img.ImageScope; import io.onedev.server.web.websocket.WebSocketManager; -import org.apache.shiro.web.env.EnvironmentLoader; -import org.apache.shiro.web.env.EnvironmentLoaderListener; -import org.apache.shiro.web.servlet.ShiroFilter; -import org.apache.wicket.protocol.http.WicketServlet; -import org.eclipse.jetty.servlet.FilterHolder; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.glassfish.jersey.servlet.ServletContainer; - -import javax.inject.Inject; -import javax.servlet.DispatcherType; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; -import java.io.File; -import java.util.EnumSet; public class ProductServletConfigurator implements ServletConfigurator { @@ -57,13 +60,16 @@ public class ProductServletConfigurator implements ServletConfigurator { private final WebSocketManager webSocketManager; private final ServerSocketServlet serverServlet; + + private final MCPServerServlet mcpServerServlet; @Inject public ProductServletConfigurator(ShiroFilter shiroFilter, CorsFilter corsFilter, GitFilter gitFilter, GitLfsFilter gitLfsFilter, GitPreReceiveCallback preReceiveServlet, GitPostReceiveCallback postReceiveServlet, WicketServlet wicketServlet, WebSocketManager webSocketManager, ServletContainer jerseyServlet, - ServerSocketServlet serverServlet, GoGetFilter goGetFilter) { + ServerSocketServlet serverServlet, GoGetFilter goGetFilter, + MCPServerServlet mcpServerServlet) { this.corsFilter = corsFilter; this.shiroFilter = shiroFilter; this.gitFilter = gitFilter; @@ -75,6 +81,7 @@ public class ProductServletConfigurator implements ServletConfigurator { this.jerseyServlet = jerseyServlet; this.serverServlet = serverServlet; this.goGetFilter = goGetFilter; + this.mcpServerServlet = mcpServerServlet; } @Override @@ -159,6 +166,10 @@ public class ProductServletConfigurator implements ServletConfigurator { context.addServlet(new ServletHolder(jerseyServlet), "/~api/*"); context.addServlet(new ServletHolder(serverServlet), "/~server"); + + var mcpServletHolder = new ServletHolder(mcpServerServlet); + context.addServlet(mcpServletHolder, "/~mcp"); + context.addServlet(mcpServletHolder, "/~mcp/*"); } }