Skip to content

Commit eff23e2

Browse files
committed
clean
1 parent 70ad5c9 commit eff23e2

File tree

2 files changed

+99
-79
lines changed

2 files changed

+99
-79
lines changed

src/main/java/net/sourceforge/plantuml/servlet/mcp/McpServlet.java

Lines changed: 65 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@
3535
import jakarta.servlet.http.HttpServletResponse;
3636

3737
import java.io.IOException;
38+
import java.util.LinkedHashMap;
39+
import java.util.Map;
3840

3941
/**
4042
* MCP (Model Context Protocol) servlet for PlantUML server.
4143
* Handles JSON-RPC 2.0 messages over HTTP POST.
4244
*
43-
* Configuration via environment variables:
45+
* Configuration via environment variables / system properties:
4446
* - PLANTUML_MCP_ENABLED: "true" to enable the endpoint (default: false)
4547
* - PLANTUML_MCP_API_KEY: Optional API key for Bearer authentication
4648
*
@@ -55,67 +57,77 @@
5557
@SuppressWarnings("serial")
5658
public class McpServlet extends HttpServlet {
5759

60+
/** Exposed for unit tests. */
5861
public JsonRpcServer jsonRpcServer;
62+
5963
private String apiKey;
6064
private boolean mcpEnabled;
65+
private ObjectMapper objectMapper;
6166

6267
@Override
6368
public void init() throws ServletException {
6469
super.init();
6570

71+
// Shared ObjectMapper for both JSON-RPC and HTTP responses
72+
this.objectMapper = new ObjectMapper();
73+
6674
// Read configuration
67-
mcpEnabled = isMcpEnabled();
68-
apiKey = getConfigString("PLANTUML_MCP_API_KEY", "");
75+
this.mcpEnabled = isMcpEnabled();
76+
this.apiKey = getConfigString("PLANTUML_MCP_API_KEY", "");
6977

7078
if (mcpEnabled) {
71-
// Initialize JSON-RPC server
72-
ObjectMapper mapper = new ObjectMapper();
73-
McpServiceImpl service = new McpServiceImpl(mapper);
74-
this.jsonRpcServer = new JsonRpcServer(mapper, service, McpService.class);
79+
// Initialize JSON-RPC server with the MCP service implementation
80+
McpServiceImpl service = new McpServiceImpl(objectMapper);
81+
this.jsonRpcServer = new JsonRpcServer(objectMapper, service, McpService.class);
7582

7683
log("MCP endpoint initialized" +
7784
(apiKey.isEmpty() ? " (no authentication)" : " (with API key authentication)"));
7885
} else {
86+
this.jsonRpcServer = null;
7987
log("MCP endpoint disabled (set PLANTUML_MCP_ENABLED=true to enable)");
8088
}
8189
}
8290

8391
@Override
8492
protected void doPost(HttpServletRequest request, HttpServletResponse response)
85-
throws ServletException, IOException {
93+
throws ServletException, IOException {
8694

8795
// Check if MCP is enabled
88-
if (!mcpEnabled) {
96+
if (!mcpEnabled || jsonRpcServer == null) {
8997
sendJsonError(response, HttpServletResponse.SC_NOT_FOUND,
90-
"MCP API is not enabled. Set PLANTUML_MCP_ENABLED=true to enable.");
98+
"MCP API is not enabled. Set PLANTUML_MCP_ENABLED=true to enable.");
9199
return;
92100
}
93101

94102
// Authenticate if API key is configured
95103
if (!authenticate(request, response)) {
96-
return;
104+
return; // sendJsonError already called
97105
}
98106

99-
// Set response headers
107+
// CORS + JSON response headers
100108
response.setContentType("application/json");
101109
response.setCharacterEncoding("UTF-8");
102110
response.setHeader("Access-Control-Allow-Origin", "*");
103111
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
104112
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
105113

106114
try {
107-
// Laisser jsonrpc4j gérer la requête Jakarta directement
115+
// Delegate full JSON-RPC handling to jsonrpc4j (Jakarta Servlet API)
108116
jsonRpcServer.handle(request, response);
109117
} catch (Exception e) {
110118
log("Error handling JSON-RPC request: " + e.getMessage(), e);
111-
sendJsonError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
112-
"Internal server error: " + e.getMessage());
113-
} }
119+
120+
if (!response.isCommitted()) {
121+
sendJsonError(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
122+
"Internal server error: " + e.getMessage());
123+
}
124+
}
125+
}
114126

115127
@Override
116128
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
117-
throws ServletException, IOException {
118-
// Handle CORS preflight
129+
throws ServletException, IOException {
130+
// CORS preflight
119131
response.setHeader("Access-Control-Allow-Origin", "*");
120132
response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
121133
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
@@ -127,15 +139,23 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
127139
throws ServletException, IOException {
128140

129141
if (!mcpEnabled) {
130-
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
142+
sendJsonError(response, HttpServletResponse.SC_NOT_FOUND, "MCP API is not enabled");
131143
return;
132144
}
133145

134-
// éventuellement : authenticate(request, response)
135-
136-
response.setContentType("text/plain");
137-
response.setCharacterEncoding("UTF-8");
138-
response.getWriter().println("PlantUML MCP endpoint is up.");
146+
// Small informational endpoint about the MCP service
147+
Map<String, Object> info = new LinkedHashMap<>();
148+
info.put("service", "PlantUML MCP Server");
149+
info.put("version", "1.0.0");
150+
info.put("protocol", "JSON-RPC 2.0");
151+
info.put("transport", "HTTP POST");
152+
info.put("methods", new String[] { "initialize", "tools/list", "tools/call" });
153+
info.put("tools", new String[] { "diagram_type" });
154+
info.put("authentication", apiKey.isEmpty()
155+
? "none"
156+
: "Bearer token required");
157+
158+
writeJson(response, HttpServletResponse.SC_OK, info);
139159
}
140160

141161
// ============= AUTHENTICATION =============
@@ -145,17 +165,17 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response)
145165
* Returns true if authentication succeeds or is not required.
146166
*/
147167
private boolean authenticate(HttpServletRequest request, HttpServletResponse response)
148-
throws IOException {
168+
throws IOException {
149169

150170
// No authentication required if no API key is set
151-
if (apiKey.isEmpty()) {
171+
if (apiKey == null || apiKey.isEmpty()) {
152172
return true;
153173
}
154174

155175
String authHeader = request.getHeader("Authorization");
156176
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
157177
sendJsonError(response, HttpServletResponse.SC_UNAUTHORIZED,
158-
"Missing or invalid Authorization header. Expected: Bearer <token>");
178+
"Missing or invalid Authorization header. Expected: Bearer <token>");
159179
return false;
160180
}
161181

@@ -186,31 +206,31 @@ private String getConfigString(String key, String defaultValue) {
186206
return defaultValue;
187207
}
188208

189-
// ============= ERROR HANDLING =============
209+
// ============= JSON HELPERS =============
190210

191211
private void sendJsonError(HttpServletResponse response, int status, String message)
192-
throws IOException {
212+
throws IOException {
213+
214+
Map<String, Object> errorBody = new LinkedHashMap<>();
215+
Map<String, Object> error = new LinkedHashMap<>();
216+
error.put("code", status);
217+
error.put("message", message);
218+
errorBody.put("error", error);
219+
220+
writeJson(response, status, errorBody);
221+
}
222+
223+
private void writeJson(HttpServletResponse response, int status, Object body)
224+
throws IOException {
225+
193226
response.setStatus(status);
194227
response.setContentType("application/json");
195228
response.setCharacterEncoding("UTF-8");
196229

197-
String json = "{\n" +
198-
" \"error\": {\n" +
199-
" \"code\": " + status + ",\n" +
200-
" \"message\": \"" + escapeJson(message) + "\"\n" +
201-
" }\n" +
202-
"}";
230+
// Basic CORS header so clients can read error/info responses as well
231+
response.setHeader("Access-Control-Allow-Origin", "*");
203232

204-
response.getWriter().print(json);
233+
objectMapper.writeValue(response.getWriter(), body);
205234
response.getWriter().flush();
206235
}
207-
208-
private String escapeJson(String text) {
209-
if (text == null) return "";
210-
return text.replace("\\", "\\\\")
211-
.replace("\"", "\\\"")
212-
.replace("\n", "\\n")
213-
.replace("\r", "\\r")
214-
.replace("\t", "\\t");
215-
}
216236
}

0 commit comments

Comments
 (0)