3535import jakarta .servlet .http .HttpServletResponse ;
3636
3737import 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 *
5557@ SuppressWarnings ("serial" )
5658public 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