-
-
Notifications
You must be signed in to change notification settings - Fork 571
Description
Summary
We would like to introduce a new MCP (Model Context Protocol) servlet inside plantuml-server, exposing a structured HTTP/JSON API under /mcp/*.
This API will be consumed by AI assistants (e.g., GitHub Copilot, ChatGPT, Claude, IDE integrations) to validate, analyse, and render PlantUML diagrams in a safe and structured way.
👉 Important: all development for this feature must target the dedicated branch: mcp1.
The goal of this issue is to define the minimal MCP feature set and especially provide unit tests so that Copilot can implement the servlet safely and consistently.
Goals
The MCP servlet must:
-
Provide predictable, JSON-only endpoints.
-
Never bypass existing PlantUML security rules.
-
Be fully isolated from the existing rendering servlets.
-
Offer a minimal but useful toolset for MCP clients:
- syntax checking,
- rendering,
- diagram metadata extraction,
- ephemeral workspace concept.
Endpoints (initial version)
1. POST /mcp/check
Validates a PlantUML source.
Request
{
"source": "@startuml\nAlice -> Bob: Hello\n@enduml"
}Response
{
"ok": true,
"errors": []
}If errors occur:
{
"ok": false,
"errors": [
{ "line": 2, "message": "Syntax error XYZ" }
]
}2. POST /mcp/render
Renders a diagram and returns a PNG encoded as Base64.
Request
{
"source": "@startuml\nAlice -> Bob\n@enduml"
}Response
{
"ok": true,
"format": "png",
"dataBase64": "iVBORw0KGgoAAAANSUhEUgAA..."
}Errors must follow the same structure as /mcp/check.
3. POST /mcp/metadata
Returns structural metadata extracted from a diagram (participants, classes, relationships, etc.).
Response example
{
"participants": ["Alice", "Bob"],
"directives": ["skinparam ..."],
"diagramType": "sequence",
"warnings": []
}4. POST /mcp/workspace/create
Creates a new ephemeral workspace (in-memory only).
Response
{
"workspaceId": "w-8f1b0341"
}5. POST /mcp/workspace/put
Adds or updates a file in the workspace.
Request
{
"workspaceId": "w-8f1b0341",
"filename": "test.puml",
"content": "..."
}6. POST /mcp/workspace/render
Renders a file stored in the workspace.
Implementation Notes
- Create a new dedicated servlet:
net.sourceforge.plantuml.server.servlet.McpServlet - URL mapping:
/mcp/* - Only accept POST requests with JSON bodies.
- Enforce all PlantUML security profiles and limits.
- Workspaces must be stored in a thread-safe in-memory map.
✔️ Unit Tests (JUnit 5)
These are essential for Copilot to generate correct code.
All tests must be placed in:
src/test/java/net/sourceforge/plantuml/server/servlet/McpServletTest.java
### 1. Test: check endpoint accepts valid diagram
@Test
void checkEndpointShouldReturnOkForValidDiagram() throws Exception {
String json = "{ \"source\": \"@startuml\nAlice -> Bob\n@enduml\" }";
MockHttpServletRequest req = postJson("/mcp/check", json);
MockHttpServletResponse resp = new MockHttpServletResponse();
servlet.service(req, resp);
assertEquals(200, resp.getStatus());
String body = resp.getContentAsString();
assertTrue(body.contains("\"ok\":true"));
assertTrue(body.contains("\"errors\":[]"));
}2. Test: check endpoint should report syntax errors
@Test
void checkEndpointShouldReportErrors() throws Exception {
String json = "{ \"source\": \"@startuml\nThis is wrong\n@enduml\" }";
MockHttpServletResponse resp = call("/mcp/check", json);
assertTrue(resp.getContentAsString().contains("\"ok\":false"));
assertTrue(resp.getContentAsString().contains("errors"));
}3. Test: render endpoint returns Base64 PNG
@Test
void renderEndpointReturnsPngBase64() throws Exception {
String json = "{ \"source\": \"@startuml\nAlice -> Bob\n@enduml\" }";
MockHttpServletResponse resp = call("/mcp/render", json);
assertTrue(resp.getContentAsString().contains("\"format\":\"png\""));
assertTrue(resp.getContentAsString().contains("\"dataBase64\""));
}4. Test: metadata endpoint returns participants
@Test
void metadataEndpointReturnsParticipants() throws Exception {
String json = "{ \"source\": \"@startuml\nAlice -> Bob\n@enduml\" }";
MockHttpServletResponse resp = call("/mcp/metadata", json);
assertTrue(resp.getContentAsString().contains("Alice"));
assertTrue(resp.getContentAsString().contains("Bob"));
}5. Test: workspace lifecycle
@Test
void workspaceLifecycle() throws Exception {
// 1) create workspace
MockHttpServletResponse r1 =
call("/mcp/workspace/create", "{}");
String ws = extractWorkspaceId(r1.getContentAsString());
assertNotNull(ws);
// 2) put file
String putJson = "{ \"workspaceId\":\"" + ws + "\", "
+ "\"filename\":\"test.puml\", "
+ "\"content\":\"@startuml\nAlice->Bob\n@enduml\" }";
MockHttpServletResponse r2 =
call("/mcp/workspace/put", putJson);
assertEquals(200, r2.getStatus());
// 3) render file
String renderJson = "{ \"workspaceId\":\"" + ws + "\", "
+ "\"filename\":\"test.puml\" }";
MockHttpServletResponse r3 =
call("/mcp/workspace/render", renderJson);
assertTrue(r3.getContentAsString().contains("\"dataBase64\""));
}6. Test: invalid JSON must return 400
@Test
void invalidJsonShouldReturn400() throws Exception {
MockHttpServletResponse resp =
call("/mcp/check", "{ invalid json }");
assertEquals(400, resp.getStatus());
}Expected Result
After implementing this issue:
McpServletexists and handles all routes.- JSON parsing & errors are robust.
- Workspace management is isolated and thread-safe.
- All tests above pass on the branch
mcp1. - Existing server behavior is untouched.
Branch
All development must be done in:
branch: mcp1