Merge branch 'mcp'

This commit is contained in:
Robin Shen 2025-07-28 11:16:06 +08:00
commit c01570f4c0
3 changed files with 468 additions and 16 deletions

View File

@ -0,0 +1,310 @@
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.server.util.IOUtils;
/**
* 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.
*/
@Singleton
public class MCPServerServlet extends HttpServlet {
private static final Logger logger = LoggerFactory.getLogger(MCPServerServlet.class);
private final ObjectMapper objectMapper;
// In-memory storage for demo purposes
private final Map<String, JsonNode> resources = new ConcurrentHashMap<>();
@Inject
public MCPServerServlet(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
// 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 {
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");
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");
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));
}
} 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 "notifications/initialized":
return handleNotificationInitialized(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");
// 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();
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");
// 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");
// 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();
result.set("tools", toolsArray);
return result;
}
private JsonNode handleCallTool(JsonNode params) {
String name = params.get("name").asText();
JsonNode arguments = params.get("arguments");
JsonNode toolResult;
switch (name) {
case "getServerStatus":
toolResult = handleGetServerStatus(arguments);
break;
case "createResource":
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");
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/plain");
try (var os = response.getOutputStream()) {
os.println("OneDev MCP server");
}
response.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@ -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

View File

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