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; 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 {
@ -57,13 +60,16 @@ public class ProductServletConfigurator implements ServletConfigurator {
private final WebSocketManager webSocketManager; private final WebSocketManager webSocketManager;
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/*");
} }
} }