Skip to content

Add a new MCP Servlet (/mcp/*) with unit tests β€” target branch mcp1Β #402

@arnaudroques

Description

@arnaudroques

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:

  1. Provide predictable, JSON-only endpoints.

  2. Never bypass existing PlantUML security rules.

  3. Be fully isolated from the existing rendering servlets.

  4. 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:

  • McpServlet exists 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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions