From 593db97cd3e52c36b1c4d992169b0b56f0a81ed5 Mon Sep 17 00:00:00 2001 From: Robin Shen Date: Sat, 26 Jul 2025 12:13:19 +0800 Subject: [PATCH] midwork --- .../java/io/onedev/server/mcp/MCPModule.java | 24 ++ .../onedev/server/mcp/MCPServerServlet.java | 315 ++++++++++++++++++ .../server/mcp/MCPServletConfigurator.java | 35 ++ .../main/java/io/onedev/server/mcp/README.md | 131 ++++++++ 4 files changed, 505 insertions(+) create mode 100644 server-core/src/main/java/io/onedev/server/mcp/MCPModule.java create mode 100644 server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java create mode 100644 server-core/src/main/java/io/onedev/server/mcp/MCPServletConfigurator.java create mode 100644 server-core/src/main/java/io/onedev/server/mcp/README.md 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 new file mode 100644 index 0000000000..5034240f08 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/mcp/MCPModule.java @@ -0,0 +1,24 @@ +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 new file mode 100644 index 0000000000..d9c4fdb30f --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/mcp/MCPServerServlet.java @@ -0,0 +1,315 @@ +package io.onedev.server.mcp; + +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; + +/** + * Example MCP (Model Context Protocol) Server Servlet + * + * This servlet implements a basic MCP server that can handle: + * - List resources + * - Read resources + * - List tools + * - Call tools + * + * The MCP server provides access to OneDev server information and basic operations. + */ +public class MCPServerServlet extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(MCPServerServlet.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + // In-memory storage for demo purposes + private final Map resources = new ConcurrentHashMap<>(); + + public MCPServerServlet() { + // Initialize with some example resources + initializeExampleResources(); + } + + private void initializeExampleResources() { + try { + ObjectNode serverInfo = objectMapper.createObjectNode(); + serverInfo.put("name", "OneDev Server"); + serverInfo.put("version", "1.0.0"); + serverInfo.put("status", "running"); + resources.put("server:info", serverInfo); + + ObjectNode config = objectMapper.createObjectNode(); + config.put("port", 8080); + config.put("contextPath", "/"); + config.put("sessionTimeout", 300); + resources.put("server:config", config); + + } catch (Exception e) { + logger.error("Error initializing example resources", e); + } + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + response.setContentType("application/json"); + 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()); + String method = requestNode.get("method").asText(); + JsonNode params = requestNode.get("params"); + String id = requestNode.get("id").asText(); + + JsonNode result = handleRequest(method, params); + + // Send response + ObjectNode responseNode = objectMapper.createObjectNode(); + responseNode.put("jsonrpc", "2.0"); + responseNode.put("id", id); + responseNode.set("result", result); + + try (PrintWriter writer = response.getWriter()) { + writer.write(objectMapper.writeValueAsString(responseNode)); + } + + } catch (Exception e) { + logger.error("Error handling MCP request", e); + sendErrorResponse(response, -1, "Internal server error: " + e.getMessage()); + } + } + + private JsonNode handleRequest(String method, JsonNode params) { + switch (method) { + case "initialize": + return handleInitialize(params); + case "resources/list": + return handleListResources(params); + case "resources/read": + return handleReadResource(params); + case "tools/list": + return handleListTools(params); + case "tools/call": + return handleCallTool(params); + default: + throw new IllegalArgumentException("Unknown method: " + method); + } + } + + 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() + .put("name", "OneDev MCP Server") + .put("version", "1.0.0")); + return result; + } + + private JsonNode handleListResources(JsonNode params) { + ArrayNode resourcesArray = objectMapper.createArrayNode(); + + for (String uri : resources.keySet()) { + ObjectNode resource = objectMapper.createObjectNode(); + resource.put("uri", uri); + resource.put("name", uri.substring(uri.lastIndexOf(':') + 1)); + resource.put("description", "OneDev server " + uri.substring(uri.lastIndexOf(':') + 1)); + resource.put("mimeType", "application/json"); + resourcesArray.add(resource); + } + + ObjectNode result = objectMapper.createObjectNode(); + result.set("resources", resourcesArray); + return result; + } + + private JsonNode handleReadResource(JsonNode params) { + String uri = params.get("uri").asText(); + JsonNode resource = resources.get(uri); + + if (resource == null) { + throw new IllegalArgumentException("Resource not found: " + uri); + } + + ObjectNode result = objectMapper.createObjectNode(); + result.set("contents", objectMapper.createArrayNode().add(objectMapper.createObjectNode() + .put("uri", uri) + .put("mimeType", "application/json") + .set("text", resource))); + return result; + } + + private JsonNode handleListTools(JsonNode params) { + ArrayNode toolsArray = objectMapper.createArrayNode(); + + // Example tools + ObjectNode serverStatusTool = objectMapper.createObjectNode(); + serverStatusTool.put("name", "getServerStatus"); + serverStatusTool.put("description", "Get the current status of the OneDev server"); + serverStatusTool.set("inputSchema", objectMapper.createObjectNode()); + 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); + toolsArray.add(createResourceTool); + + ObjectNode result = objectMapper.createObjectNode(); + result.set("tools", toolsArray); + return result; + } + + private JsonNode handleCallTool(JsonNode params) { + String name = params.get("name").asText(); + JsonNode arguments = params.get("arguments"); + + switch (name) { + case "getServerStatus": + return handleGetServerStatus(arguments); + case "createResource": + return handleCreateResource(arguments); + default: + throw new IllegalArgumentException("Unknown tool: " + name); + } + } + + 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; + } + + private JsonNode handleCreateResource(JsonNode arguments) { + String uri = arguments.get("uri").asText(); + JsonNode content = arguments.get("content"); + + if (resources.containsKey(uri)) { + throw new IllegalArgumentException("Resource already exists: " + uri); + } + + resources.put(uri, content); + + ObjectNode result = objectMapper.createObjectNode(); + result.put("uri", uri); + result.put("created", true); + return result; + } + + private void sendErrorResponse(HttpServletResponse response, int code, String message) throws IOException { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setContentType("application/json"); + + ObjectNode errorNode = objectMapper.createObjectNode(); + errorNode.put("jsonrpc", "2.0"); + errorNode.put("id", (String)null); + ObjectNode error = objectMapper.createObjectNode(); + error.put("code", code); + error.put("message", message); + errorNode.set("error", error); + + try (PrintWriter writer = response.getWriter()) { + writer.write(objectMapper.writeValueAsString(errorNode)); + } + } + + @Override + 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" + + "
  • server:info - Basic server information
  • \n" + + "
  • server:config - Server configuration
  • \n" + + "
\n" + + " \n" + + "

Available Tools

\n" + + "
    \n" + + "
  • getServerStatus - Get current server status
  • \n" + + "
  • createResource - Create a new resource
  • \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" + + ""); + } + } +} \ 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 new file mode 100644 index 0000000000..a6fb76eaef --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/mcp/MCPServletConfigurator.java @@ -0,0 +1,35 @@ +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-core/src/main/java/io/onedev/server/mcp/README.md b/server-core/src/main/java/io/onedev/server/mcp/README.md new file mode 100644 index 0000000000..97f0eaf6b3 --- /dev/null +++ b/server-core/src/main/java/io/onedev/server/mcp/README.md @@ -0,0 +1,131 @@ +# OneDev MCP Server + +This is an example Model Context Protocol (MCP) server implementation for OneDev. The MCP server provides a standardized way for AI assistants and other tools to interact with OneDev server data and functionality. + +## Overview + +The MCP server is implemented as a servlet that runs within the existing OneDev Jetty server. It handles HTTP requests and implements the MCP protocol to provide: + +- **Resources**: Access to OneDev server information and configuration +- **Tools**: Operations that can be performed on the OneDev server + +## Architecture + +The MCP server consists of the following components: + +1. **MCPServerServlet**: The main servlet that handles HTTP requests and implements the MCP protocol +2. **MCPServletConfigurator**: Configures the servlet with the Jetty server +3. **MCPModule**: Dependency injection configuration + +## Endpoints + +### GET /mcp +Returns an HTML page with documentation about the MCP server and available endpoints. + +### POST /mcp +Main MCP protocol endpoint that handles JSON-RPC requests. + +## Available Resources + +- `server:info` - Basic server information (name, version, status) +- `server:config` - Server configuration (port, context path, session timeout) + +## Available Tools + +- `getServerStatus` - Get current server status including uptime and memory usage +- `createResource` - Create a new resource with a given URI and content + +## Example Usage + +### Initialize the MCP connection + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": {} +} +``` + +### List available resources + +```json +{ + "jsonrpc": "2.0", + "id": 2, + "method": "resources/list", + "params": {} +} +``` + +### Read a specific resource + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "method": "resources/read", + "params": { + "uri": "server:info" + } +} +``` + +### List available tools + +```json +{ + "jsonrpc": "2.0", + "id": 4, + "method": "tools/list", + "params": {} +} +``` + +### Call a tool + +```json +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "getServerStatus", + "arguments": {} + } +} +``` + +## Integration + +The MCP server is automatically integrated into OneDev through the plugin system. The `MCPModule` extends `AbstractPluginModule` and is automatically discovered and loaded when OneDev starts. + +The servlet is registered at the `/mcp` path and handles both GET requests (for documentation) and POST requests (for MCP protocol communication). + +## Extending the MCP Server + +To add new resources or tools: + +1. **Add new resources**: Modify the `initializeExampleResources()` method in `MCPServerServlet` +2. **Add new tools**: Add new cases to the `handleCallTool()` method and implement the corresponding handler methods +3. **Add new MCP methods**: Add new cases to the `handleRequest()` method + +## Protocol Compliance + +This implementation follows the Model Context Protocol specification and supports: + +- JSON-RPC 2.0 communication +- Resource listing and reading +- Tool listing and calling +- Proper error handling with JSON-RPC error responses + +## Security Considerations + +The current implementation is a basic example. In a production environment, you should consider: + +- Authentication and authorization for MCP requests +- Rate limiting +- Input validation and sanitization +- Secure communication (HTTPS) +- Access control for sensitive server information \ No newline at end of file