This commit is contained in:
Robin Shen 2025-07-27 21:16:21 +08:00
parent 593db97cd3
commit 634279171b
4 changed files with 126 additions and 179 deletions

View File

@ -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);
}
}

View File

@ -1,23 +1,28 @@
package io.onedev.server.mcp; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; 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 io.onedev.server.util.IOUtils;
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 * 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. * The MCP server provides access to OneDev server information and basic operations.
*/ */
@Singleton
public class MCPServerServlet extends HttpServlet { public class MCPServerServlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(MCPServerServlet.class); 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 // In-memory storage for demo purposes
private final Map<String, JsonNode> resources = new ConcurrentHashMap<>(); private final Map<String, JsonNode> resources = new ConcurrentHashMap<>();
public MCPServerServlet() { @Inject
public MCPServerServlet(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
// Initialize with some example resources // Initialize with some example resources
initializeExampleResources(); initializeExampleResources();
} }
@ -71,28 +79,30 @@ public class MCPServerServlet extends HttpServlet {
response.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");
try { try {
// Read request body var os = new ByteArrayOutputStream();
StringBuilder requestBody = new StringBuilder(); try (var is = request.getInputStream()) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()))) { IOUtils.copy(is, os, IOUtils.BUFFER_SIZE);
String line;
while ((line = reader.readLine()) != null) {
requestBody.append(line);
}
} }
JsonNode requestNode = objectMapper.readTree(requestBody.toString()); System.out.println(os.toString(StandardCharsets.UTF_8));
JsonNode requestNode = objectMapper.readTree(os.toString(StandardCharsets.UTF_8));
String method = requestNode.get("method").asText(); String method = requestNode.get("method").asText();
JsonNode params = requestNode.get("params"); 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); JsonNode result = handleRequest(method, params);
// Send response // Send response
ObjectNode responseNode = objectMapper.createObjectNode(); ObjectNode responseNode = objectMapper.createObjectNode();
responseNode.put("jsonrpc", "2.0"); responseNode.put("jsonrpc", "2.0");
if (id != null)
responseNode.put("id", id); responseNode.put("id", id);
responseNode.set("result", result); responseNode.set("result", result);
System.out.println(objectMapper.writeValueAsString(responseNode));
try (PrintWriter writer = response.getWriter()) { try (PrintWriter writer = response.getWriter()) {
writer.write(objectMapper.writeValueAsString(responseNode)); writer.write(objectMapper.writeValueAsString(responseNode));
} }
@ -107,6 +117,8 @@ public class MCPServerServlet extends HttpServlet {
switch (method) { switch (method) {
case "initialize": case "initialize":
return handleInitialize(params); return handleInitialize(params);
case "notifications/initialized":
return handleNotificationInitialized(params);
case "resources/list": case "resources/list":
return handleListResources(params); return handleListResources(params);
case "resources/read": case "resources/read":
@ -123,13 +135,25 @@ public class MCPServerServlet extends HttpServlet {
private JsonNode handleInitialize(JsonNode params) { private JsonNode handleInitialize(JsonNode params) {
ObjectNode result = objectMapper.createObjectNode(); ObjectNode result = objectMapper.createObjectNode();
result.put("protocolVersion", "2024-11-05"); 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("name", "OneDev MCP Server")
.put("version", "1.0.0")); .put("version", "1.0.0"));
return result; return result;
} }
private JsonNode handleNotificationInitialized(JsonNode params) {
ObjectNode result = objectMapper.createObjectNode();
result.put("status", "initialized");
return result;
}
private JsonNode handleListResources(JsonNode params) { private JsonNode handleListResources(JsonNode params) {
ArrayNode resourcesArray = objectMapper.createArrayNode(); ArrayNode resourcesArray = objectMapper.createArrayNode();
@ -170,20 +194,34 @@ public class MCPServerServlet extends HttpServlet {
ObjectNode serverStatusTool = objectMapper.createObjectNode(); ObjectNode serverStatusTool = objectMapper.createObjectNode();
serverStatusTool.put("name", "getServerStatus"); serverStatusTool.put("name", "getServerStatus");
serverStatusTool.put("description", "Get the current status of the OneDev server"); 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); toolsArray.add(serverStatusTool);
ObjectNode createResourceTool = objectMapper.createObjectNode(); ObjectNode createResourceTool = objectMapper.createObjectNode();
createResourceTool.put("name", "createResource"); createResourceTool.put("name", "createResource");
createResourceTool.put("description", "Create a new resource"); createResourceTool.put("description", "Create a new resource");
ObjectNode inputSchema = objectMapper.createObjectNode();
inputSchema.put("type", "object"); // Input schema for createResource
ObjectNode properties = objectMapper.createObjectNode(); ObjectNode createResourceInputSchema = objectMapper.createObjectNode();
properties.put("uri", objectMapper.createObjectNode().put("type", "string")); createResourceInputSchema.put("type", "object");
properties.put("content", objectMapper.createObjectNode().put("type", "object")); properties = objectMapper.createObjectNode();
inputSchema.set("properties", properties); properties.set("uri", objectMapper.createObjectNode()
inputSchema.set("required", objectMapper.createArrayNode().add("uri").add("content")); .put("type", "string")
createResourceTool.set("inputSchema", inputSchema); .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); toolsArray.add(createResourceTool);
ObjectNode result = objectMapper.createObjectNode(); ObjectNode result = objectMapper.createObjectNode();
@ -195,22 +233,34 @@ public class MCPServerServlet extends HttpServlet {
String name = params.get("name").asText(); String name = params.get("name").asText();
JsonNode arguments = params.get("arguments"); JsonNode arguments = params.get("arguments");
JsonNode toolResult;
switch (name) { switch (name) {
case "getServerStatus": case "getServerStatus":
return handleGetServerStatus(arguments); toolResult = handleGetServerStatus(arguments);
break;
case "createResource": case "createResource":
return handleCreateResource(arguments); toolResult = handleCreateResource(arguments);
break;
default: default:
throw new IllegalArgumentException("Unknown tool: " + name); 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) { private JsonNode handleGetServerStatus(JsonNode arguments) {
ObjectNode result = objectMapper.createObjectNode(); ObjectNode result = objectMapper.createObjectNode();
result.put("status", "running"); 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; return result;
} }
@ -251,65 +301,10 @@ public class MCPServerServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { throws ServletException, IOException {
response.setContentType("text/html"); response.setContentType("text/plain");
response.setCharacterEncoding("UTF-8"); try (var os = response.getOutputStream()) {
os.println("OneDev MCP server");
try (PrintWriter writer = response.getWriter()) { }
writer.write("<!DOCTYPE html>\n" + response.setStatus(HttpServletResponse.SC_OK);
"<html>\n" +
"<head>\n" +
" <title>OneDev MCP Server</title>\n" +
" <style>\n" +
" body { font-family: Arial, sans-serif; margin: 40px; }\n" +
" .container { max-width: 800px; margin: 0 auto; }\n" +
" .endpoint { background: #f5f5f5; padding: 20px; margin: 20px 0; border-radius: 5px; }\n" +
" .method { font-weight: bold; color: #0066cc; }\n" +
" .description { margin-top: 10px; color: #666; }\n" +
" </style>\n" +
"</head>\n" +
"<body>\n" +
" <div class=\"container\">\n" +
" <h1>OneDev MCP Server</h1>\n" +
" <p>This is an example Model Context Protocol (MCP) server for OneDev.</p>\n" +
" \n" +
" <div class=\"endpoint\">\n" +
" <div class=\"method\">POST /mcp</div>\n" +
" <div class=\"description\">\n" +
" Main MCP endpoint that handles all protocol requests including:\n" +
" <ul>\n" +
" <li>initialize - Initialize the MCP connection</li>\n" +
" <li>resources/list - List available resources</li>\n" +
" <li>resources/read - Read a specific resource</li>\n" +
" <li>tools/list - List available tools</li>\n" +
" <li>tools/call - Call a specific tool</li>\n" +
" </ul>\n" +
" </div>\n" +
" </div>\n" +
" \n" +
" <h2>Available Resources</h2>\n" +
" <ul>\n" +
" <li><code>server:info</code> - Basic server information</li>\n" +
" <li><code>server:config</code> - Server configuration</li>\n" +
" </ul>\n" +
" \n" +
" <h2>Available Tools</h2>\n" +
" <ul>\n" +
" <li><code>getServerStatus</code> - Get current server status</li>\n" +
" <li><code>createResource</code> - Create a new resource</li>\n" +
" </ul>\n" +
" \n" +
" <h2>Example Request</h2>\n" +
" <pre>\n" +
"{\n" +
" \"jsonrpc\": \"2.0\",\n" +
" \"id\": 1,\n" +
" \"method\": \"resources/list\",\n" +
" \"params\": {}\n" +
"}\n" +
" </pre>\n" +
" </div>\n" +
"</body>\n" +
"</html>");
}
} }
} }

View File

@ -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/*");
}
}

View File

@ -1,5 +1,22 @@
package io.onedev.server.product; 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.commons.bootstrap.Bootstrap;
import io.onedev.server.OneDev; import io.onedev.server.OneDev;
import io.onedev.server.agent.ServerSocketServlet; 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.ClasspathAssetServlet;
import io.onedev.server.jetty.FileAssetServlet; import io.onedev.server.jetty.FileAssetServlet;
import io.onedev.server.jetty.ServletConfigurator; import io.onedev.server.jetty.ServletConfigurator;
import io.onedev.server.mcp.MCPServerServlet;
import io.onedev.server.security.CorsFilter; import io.onedev.server.security.CorsFilter;
import io.onedev.server.security.DefaultWebEnvironment; import io.onedev.server.security.DefaultWebEnvironment;
import io.onedev.server.web.asset.icon.IconScope; import io.onedev.server.web.asset.icon.IconScope;
import io.onedev.server.web.img.ImageScope; import io.onedev.server.web.img.ImageScope;
import io.onedev.server.web.websocket.WebSocketManager; 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 { public class ProductServletConfigurator implements ServletConfigurator {
@ -58,12 +61,15 @@ public class ProductServletConfigurator implements ServletConfigurator {
private final ServerSocketServlet serverServlet; private final ServerSocketServlet serverServlet;
private final MCPServerServlet mcpServerServlet;
@Inject @Inject
public ProductServletConfigurator(ShiroFilter shiroFilter, CorsFilter corsFilter, public ProductServletConfigurator(ShiroFilter shiroFilter, CorsFilter corsFilter,
GitFilter gitFilter, GitLfsFilter gitLfsFilter, GitPreReceiveCallback preReceiveServlet, GitFilter gitFilter, GitLfsFilter gitLfsFilter, GitPreReceiveCallback preReceiveServlet,
GitPostReceiveCallback postReceiveServlet, WicketServlet wicketServlet, GitPostReceiveCallback postReceiveServlet, WicketServlet wicketServlet,
WebSocketManager webSocketManager, ServletContainer jerseyServlet, WebSocketManager webSocketManager, ServletContainer jerseyServlet,
ServerSocketServlet serverServlet, GoGetFilter goGetFilter) { ServerSocketServlet serverServlet, GoGetFilter goGetFilter,
MCPServerServlet mcpServerServlet) {
this.corsFilter = corsFilter; this.corsFilter = corsFilter;
this.shiroFilter = shiroFilter; this.shiroFilter = shiroFilter;
this.gitFilter = gitFilter; this.gitFilter = gitFilter;
@ -75,6 +81,7 @@ public class ProductServletConfigurator implements ServletConfigurator {
this.jerseyServlet = jerseyServlet; this.jerseyServlet = jerseyServlet;
this.serverServlet = serverServlet; this.serverServlet = serverServlet;
this.goGetFilter = goGetFilter; this.goGetFilter = goGetFilter;
this.mcpServerServlet = mcpServerServlet;
} }
@Override @Override
@ -159,6 +166,10 @@ public class ProductServletConfigurator implements ServletConfigurator {
context.addServlet(new ServletHolder(jerseyServlet), "/~api/*"); context.addServlet(new ServletHolder(jerseyServlet), "/~api/*");
context.addServlet(new ServletHolder(serverServlet), "/~server"); context.addServlet(new ServletHolder(serverServlet), "/~server");
var mcpServletHolder = new ServletHolder(mcpServerServlet);
context.addServlet(mcpServletHolder, "/~mcp");
context.addServlet(mcpServletHolder, "/~mcp/*");
} }
} }