c65f866b by Ean Schuessler

more security and attempts to control the content-type

1 parent d3a4a479
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
34 <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc" artifactTypeEnumId="AT_REST_PATH"/> 34 <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc" artifactTypeEnumId="AT_REST_PATH"/>
35 <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc/*" artifactTypeEnumId="AT_REST_PATH"/> 35 <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc/*" artifactTypeEnumId="AT_REST_PATH"/>
36 <moqui.security.ArtifactGroupMember artifactGroupId="McpScreenTransitions" artifactName="component://moqui-mcp-2/screen/webroot/mcp.xml/rpc" artifactTypeEnumId="AT_XML_SCREEN_TRANS"/> 36 <moqui.security.ArtifactGroupMember artifactGroupId="McpScreenTransitions" artifactName="component://moqui-mcp-2/screen/webroot/mcp.xml/rpc" artifactTypeEnumId="AT_XML_SCREEN_TRANS"/>
37 <moqui.security.ArtifactGroupMember artifactGroupId="McpScreenTransitions" artifactName="component://moqui-mcp-2/screen/webroot/mcp.xml" artifactTypeEnumId="AT_XML_SCREEN"/>
37 38
38 <!-- MCP Artifact Authz --> 39 <!-- MCP Artifact Authz -->
39 <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> 40 <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
......
...@@ -11,18 +11,47 @@ ...@@ -11,18 +11,47 @@
11 <https://creativecommons.org/publicdomain/zero/1.0/>. --> 11 <https://creativecommons.org/publicdomain/zero/1.0/>. -->
12 12
13 <screen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd" 13 <screen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"
14 require-authentication="true" track-artifact-hit="false" default-menu-include="false"> 14 require-authentication="false" track-artifact-hit="false" default-menu-include="false">
15 15
16 <parameter name="jsonrpc"/> 16 <parameter name="jsonrpc"/>
17 <parameter name="id"/> 17 <parameter name="id"/>
18 <parameter name="method"/> 18 <parameter name="method"/>
19 <parameter name="params"/> 19 <parameter name="params"/>
20 20
21 <transition name="rpc" method="post" require-session-token="false"> 21
22 <actions> 22
23 <!-- SSE Helper Functions --> 23 <actions>
24 <script><![CDATA[ 24 <script><![CDATA[
25 def handleSseStream(ec, protocolVersion) { 25 import groovy.json.JsonBuilder
26 import groovy.json.JsonSlurper
27 import java.util.UUID
28
29 // DEBUG: Log initial request details
30 ec.logger.info("=== MCP SCREEN REQUEST START ===")
31 ec.logger.info("MCP Screen Request - Method: ${ec.web?.request?.method}, ID: ${id}")
32 ec.logger.info("MCP Screen Request - Params: ${params}")
33 ec.logger.info("MCP Screen Request - User: ${ec.user.username}, UserID: ${ec.user.userId}")
34 ec.logger.info("MCP Screen Request - Current Time: ${ec.user.getNowTimestamp()}")
35
36 // Check MCP protocol version header
37 def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version")
38 if (!protocolVersion) {
39 protocolVersion = "2025-06-18" // Default to latest supported
40 }
41 ec.logger.info("MCP Protocol Version: ${protocolVersion}")
42
43 // Validate HTTP method - only POST for JSON-RPC, GET for SSE streams
44 def httpMethod = ec.web?.request?.method
45 ec.logger.info("Validating HTTP method: ${httpMethod}")
46
47 // Handle GET requests for SSE streams
48 if (httpMethod == "GET") {
49 ec.logger.info("GET request detected - checking for SSE support")
50 def acceptHeader = ec.web?.request?.getHeader("Accept")
51 ec.logger.info("GET Accept header: ${acceptHeader}")
52
53 if (acceptHeader?.contains("text/event-stream")) {
54 ec.logger.info("Client wants SSE stream - starting SSE")
26 // Set SSE headers 55 // Set SSE headers
27 ec.web.response.setContentType("text/event-stream") 56 ec.web.response.setContentType("text/event-stream")
28 ec.web.response.setCharacterEncoding("UTF-8") 57 ec.web.response.setCharacterEncoding("UTF-8")
...@@ -35,7 +64,7 @@ ...@@ -35,7 +64,7 @@
35 try { 64 try {
36 // Send initial connection event 65 // Send initial connection event
37 writer.write("event: connected\n") 66 writer.write("event: connected\n")
38 writer.write("data: {\"type\":\"connected\",\"timestamp\":\"${ec.user.now}\"}\n") 67 writer.write("data: {\"type\":\"connected\",\"timestamp\":\"${ec.user.nowTimestamp}\"}\n")
39 writer.write("\n") 68 writer.write("\n")
40 writer.flush() 69 writer.flush()
41 70
...@@ -44,7 +73,7 @@ ...@@ -44,7 +73,7 @@
44 while (count < 30) { // Keep alive for ~30 seconds 73 while (count < 30) { // Keep alive for ~30 seconds
45 Thread.sleep(1000) 74 Thread.sleep(1000)
46 writer.write("event: ping\n") 75 writer.write("event: ping\n")
47 writer.write("data: {\"timestamp\":\"${ec.user.now}\"}\n") 76 writer.write("data: {\"timestamp\":\"${ec.user.nowTimestamp}\"}\n")
48 writer.write("\n") 77 writer.write("\n")
49 writer.flush() 78 writer.flush()
50 count++ 79 count++
...@@ -55,69 +84,35 @@ ...@@ -55,69 +84,35 @@
55 } finally { 84 } finally {
56 writer.close() 85 writer.close()
57 } 86 }
87 return
88 } else {
89 // For GET requests without SSE Accept, return 405 as expected by MCP spec
90 ec.logger.info("GET request without SSE Accept - returning 405 Method Not Allowed")
91 throw new org.moqui.BaseException("Method Not Allowed. Use POST for JSON-RPC requests.")
58 } 92 }
59 93 }
60 def sendSseResponse(ec, responseObj, protocolVersion) { 94
61 // Set SSE headers 95 if (httpMethod != "POST") {
62 ec.web.response.setContentType("text/event-stream") 96 ec.logger.warn("Invalid HTTP method: ${httpMethod}, expected POST")
63 ec.web.response.setCharacterEncoding("UTF-8") 97 throw new org.moqui.BaseException("Method Not Allowed. Use POST for JSON-RPC requests.")
64 ec.web.response.setHeader("Cache-Control", "no-cache") 98 }
65 ec.web.response.setHeader("Connection", "keep-alive") 99 ec.logger.info("HTTP method validation passed")
66 ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion) 100 ]]></script>
67 101 </actions>
68 def writer = ec.web.response.writer 102
69 def jsonBuilder = new JsonBuilder(responseObj) 103 <transition name="rpc" method="post" require-session-token="false">
70 104 <actions>
71 try {
72 // Send the response as SSE event
73 writer.write("event: response\n")
74 writer.write("data: ${jsonBuilder.toString()}\n")
75 writer.write("\n")
76 writer.flush()
77
78 } catch (Exception e) {
79 ec.logger.error("Error sending SSE response: ${e.message}")
80 } finally {
81 writer.close()
82 }
83 }
84
85 def sendSseError(ec, errorResponse, protocolVersion) {
86 // Set SSE headers
87 ec.web.response.setContentType("text/event-stream")
88 ec.web.response.setCharacterEncoding("UTF-8")
89 ec.web.response.setHeader("Cache-Control", "no-cache")
90 ec.web.response.setHeader("Connection", "keep-alive")
91 ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
92
93 def writer = ec.web.response.writer
94
95 try {
96 // Send the error as SSE event
97 writer.write("event: error\n")
98 writer.write("data: ${errorResponse}\n")
99 writer.write("\n")
100 writer.flush()
101
102 } catch (Exception e) {
103 ec.logger.error("Error sending SSE error: ${e.message}")
104 } finally {
105 writer.close()
106 }
107 }
108 ]]></script>
109 <!-- Your existing MCP handling script goes here -->
110 <script><![CDATA[ 105 <script><![CDATA[
111 import groovy.json.JsonBuilder 106 import groovy.json.JsonBuilder
112 import groovy.json.JsonSlurper 107 import groovy.json.JsonSlurper
113 import java.util.UUID 108 import java.util.UUID
114 109
115 // DEBUG: Log initial request details 110 // DEBUG: Log transition request details
116 ec.logger.info("=== MCP SCREEN REQUEST START ===") 111 ec.logger.info("=== MCP RPC TRANSITION START ===")
117 ec.logger.info("MCP Screen Request - Method: ${ec.web?.request?.method}, ID: ${id}") 112 ec.logger.info("MCP RPC Request - ID: ${id}")
118 ec.logger.info("MCP Screen Request - Params: ${params}") 113 ec.logger.info("MCP RPC Request - Params: ${params}")
119 ec.logger.info("MCP Screen Request - User: ${ec.user.username}, UserID: ${ec.user.userId}") 114 ec.logger.info("MCP RPC Request - User: ${ec.user.username}, UserID: ${ec.user.userId}")
120 ec.logger.info("MCP Screen Request - Current Time: ${ec.user.getNowTimestamp()}") 115 ec.logger.info("MCP RPC Request - Current Time: ${ec.user.getNowTimestamp()}")
121 116
122 // Check MCP protocol version header 117 // Check MCP protocol version header
123 def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version") 118 def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version")
...@@ -126,26 +121,6 @@ ...@@ -126,26 +121,6 @@
126 } 121 }
127 ec.logger.info("MCP Protocol Version: ${protocolVersion}") 122 ec.logger.info("MCP Protocol Version: ${protocolVersion}")
128 123
129 // Validate HTTP method - only POST for JSON-RPC, GET for SSE streams
130 def httpMethod = ec.web?.request?.method
131 ec.logger.info("Validating HTTP method: ${httpMethod}")
132 if (httpMethod != "POST" && httpMethod != "GET") {
133 ec.logger.warn("Invalid HTTP method: ${httpMethod}, expected POST or GET")
134 ec.web?.response?.setStatus(405) // Method Not Allowed
135 ec.web?.response?.setHeader("Allow", "POST, GET")
136 ec.web?.response?.setContentType("text/plain")
137 ec.web?.response?.getWriter()?.write("Method Not Allowed. Use POST for JSON-RPC or GET for SSE streams.")
138 ec.web?.response?.getWriter()?.flush()
139 return
140 }
141 ec.logger.info("HTTP method validation passed")
142
143 // Handle GET requests for SSE streams
144 if (httpMethod == "GET") {
145 handleSseStream(ec, protocolVersion)
146 return
147 }
148
149 // Validate Content-Type header for POST requests 124 // Validate Content-Type header for POST requests
150 def contentType = ec.web?.request?.getContentType() 125 def contentType = ec.web?.request?.getContentType()
151 ec.logger.info("Validating Content-Type: ${contentType}") 126 ec.logger.info("Validating Content-Type: ${contentType}")
...@@ -372,262 +347,89 @@ ...@@ -372,262 +347,89 @@
372 <actions> 347 <actions>
373 <!-- SSE Helper Functions --> 348 <!-- SSE Helper Functions -->
374 <script><![CDATA[ 349 <script><![CDATA[
375 import groovy.json.JsonBuilder 350 def handleSseStream(ec, protocolVersion) {
376 import groovy.json.JsonSlurper 351 // Set SSE headers
377 import java.util.UUID 352 ec.web.response.setContentType("text/event-stream")
378 353 ec.web.response.setCharacterEncoding("UTF-8")
379 // DEBUG: Log initial request details 354 ec.web.response.setHeader("Cache-Control", "no-cache")
380 ec.logger.info("=== MCP SCREEN REQUEST START ===") 355 ec.web.response.setHeader("Connection", "keep-alive")
381 ec.logger.info("MCP Screen Request - Method: ${ec.web?.request?.method}, ID: ${id}") 356 ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
382 ec.logger.info("MCP Screen Request - Params: ${params}") 357
383 ec.logger.info("MCP Screen Request - User: ${ec.user.username}, UserID: ${ec.user.userId}") 358 def writer = ec.web.response.writer
384 ec.logger.info("MCP Screen Request - Current Time: ${ec.user.getNowTimestamp()}") 359
385
386 // Check MCP protocol version header
387 def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version")
388 if (!protocolVersion) {
389 protocolVersion = "2025-06-18" // Default to latest supported
390 }
391 ec.logger.info("MCP Protocol Version: ${protocolVersion}")
392
393 // Validate HTTP method - only POST for JSON-RPC, GET for SSE streams
394 def httpMethod = ec.web?.request?.method
395 ec.logger.info("Validating HTTP method: ${httpMethod}")
396 if (httpMethod != "POST" && httpMethod != "GET") {
397 ec.logger.warn("Invalid HTTP method: ${httpMethod}, expected POST or GET")
398 ec.web?.response?.setStatus(405) // Method Not Allowed
399 ec.web?.response?.setHeader("Allow", "POST, GET")
400 ec.web?.response?.setContentType("text/plain")
401 ec.web?.response?.getWriter()?.write("Method Not Allowed. Use POST for JSON-RPC or GET for SSE streams.")
402 ec.web?.response?.getWriter()?.flush()
403 return
404 }
405 ec.logger.info("HTTP method validation passed")
406
407 // Handle GET requests for SSE streams
408 if (httpMethod == "GET") {
409 handleSseStream(ec, protocolVersion)
410 return
411 }
412
413 // Validate Content-Type header for POST requests
414 def contentType = ec.web?.request?.getContentType()
415 ec.logger.info("Validating Content-Type: ${contentType}")
416 if (!contentType?.contains("application/json")) {
417 ec.logger.warn("Invalid Content-Type: ${contentType}, expected application/json or application/json-rpc")
418 ec.web?.response?.setStatus(415) // Unsupported Media Type
419 ec.web?.response?.setContentType("text/plain")
420 ec.web?.response?.getWriter()?.write("Content-Type must be application/json or application/json-rpc for JSON-RPC messages")
421 ec.web?.response?.getWriter()?.flush()
422 return
423 }
424 ec.logger.info("Content-Type validation passed")
425
426 // Validate Accept header - prioritize application/json over text/event-stream when both present
427 def acceptHeader = ec.web?.request?.getHeader("Accept")
428 ec.logger.info("Validating Accept header: ${acceptHeader}")
429 def wantsStreaming = false
430 if (acceptHeader) {
431 if (acceptHeader.contains("text/event-stream") && !acceptHeader.contains("application/json")) {
432 wantsStreaming = true
433 }
434 // If both are present, prefer application/json (don't set wantsStreaming)
435 }
436 ec.logger.info("Client wants streaming: ${wantsStreaming} (Accept: ${acceptHeader})")
437
438 // Validate Origin header for DNS rebinding protection
439 def originHeader = ec.web?.request?.getHeader("Origin")
440 ec.logger.info("Checking Origin header: ${originHeader}")
441 if (originHeader) {
442 try { 360 try {
443 def originValid = ec.service.sync("McpServices.validate#Origin", [origin: originHeader]).isValid 361 // Send initial connection event
444 ec.logger.info("Origin validation result: ${originValid}") 362 writer.write("event: connected\n")
445 if (!originValid) { 363 writer.write("data: {\"type\":\"connected\",\"timestamp\":\"${ec.user.nowTimestamp}\"}\n")
446 ec.logger.warn("Invalid Origin header rejected: ${originHeader}") 364 writer.write("\n")
447 ec.web?.response?.setStatus(403) // Forbidden 365 writer.flush()
448 ec.web?.response?.setContentType("text/plain") 366
449 ec.web?.response?.getWriter()?.write("Invalid Origin header") 367 // Keep connection alive with periodic pings
450 ec.web?.response?.getWriter()?.flush() 368 def count = 0
451 return 369 while (count < 30) { // Keep alive for ~30 seconds
370 Thread.sleep(1000)
371 writer.write("event: ping\n")
372 writer.write("data: {\"timestamp\":\"${ec.user.nowTimestamp}\"}\n")
373 writer.write("\n")
374 writer.flush()
375 count++
452 } 376 }
377
453 } catch (Exception e) { 378 } catch (Exception e) {
454 ec.logger.error("Error during Origin validation", e) 379 ec.logger.warn("SSE stream interrupted: ${e.message}")
455 ec.web?.response?.setStatus(500) // Internal Server Error 380 } finally {
456 ec.web?.response?.setContentType("text/plain") 381 writer.close()
457 ec.web?.response?.getWriter()?.write("Error during Origin validation: ${e.message}")
458 ec.web?.response?.getWriter()?.flush()
459 return
460 } 382 }
461 } else {
462 ec.logger.info("No Origin header present")
463 } 383 }
464 384
465 // Set protocol version header on all responses 385 def sendSseResponse(ec, responseObj, protocolVersion) {
466 ec.web?.response?.setHeader("MCP-Protocol-Version", protocolVersion) 386 // Set SSE headers
467 ec.logger.info("Set MCP protocol version header") 387 ec.web.response.setContentType("text/event-stream")
468 388 ec.web.response.setCharacterEncoding("UTF-8")
469 // Handle session management 389 ec.web.response.setHeader("Cache-Control", "no-cache")
470 def sessionId = ec.web?.request?.getHeader("Mcp-Session-Id") 390 ec.web.response.setHeader("Connection", "keep-alive")
471 def isInitialize = (method == "initialize") 391 ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
472 ec.logger.info("Session management - SessionId: ${sessionId}, IsInitialize: ${isInitialize}")
473
474 if (!isInitialize && !sessionId) {
475 ec.logger.warn("Missing session ID for non-initialization request")
476 ec.web?.response?.setStatus(400) // Bad Request
477 ec.web?.response?.setContentType("text/plain")
478 ec.web?.response?.getWriter()?.write("Mcp-Session-Id header required for non-initialization requests")
479 ec.web?.response?.getWriter()?.flush()
480 return
481 }
482
483 // Generate new session ID for initialization
484 if (isInitialize) {
485 def newSessionId = UUID.randomUUID().toString()
486 ec.web?.response?.setHeader("Mcp-Session-Id", newSessionId)
487 ec.logger.info("Generated new session ID: ${newSessionId}")
488 }
489
490 // Parse JSON-RPC request body if not already in parameters
491 if (!jsonrpc && !method) {
492 def requestBody = ec.web.request.getInputStream()?.getText()
493 if (requestBody) {
494 def jsonSlurper = new JsonSlurper()
495 def jsonRequest = jsonSlurper.parseText(requestBody)
496 jsonrpc = jsonRequest.jsonrpc
497 id = jsonRequest.id
498 method = jsonRequest.method
499 params = jsonRequest.params
500 }
501 }
502
503 // Validate JSON-RPC version
504 ec.logger.info("Validating JSON-RPC version: ${jsonrpc}")
505 if (jsonrpc && jsonrpc != "2.0") {
506 ec.logger.warn("Invalid JSON-RPC version: ${jsonrpc}")
507 def errorResponse = new JsonBuilder([
508 jsonrpc: "2.0",
509 error: [
510 code: -32600,
511 message: "Invalid Request: Only JSON-RPC 2.0 supported"
512 ],
513 id: id
514 ]).toString()
515 392
516 if (wantsStreaming) { 393 def writer = ec.web.response.writer
517 sendSseError(ec, errorResponse, protocolVersion) 394 def jsonBuilder = new JsonBuilder(responseObj)
518 } else { 395
519 ec.web.sendJsonResponse(errorResponse) 396 try {
397 // Send response as SSE event
398 writer.write("event: response\n")
399 writer.write("data: ${jsonBuilder.toString()}\n")
400 writer.write("\n")
401 writer.flush()
402
403 } catch (Exception e) {
404 ec.logger.error("Error sending SSE response: ${e.message}")
405 } finally {
406 writer.close()
520 } 407 }
521 return
522 } 408 }
523 ec.logger.info("JSON-RPC version validation passed")
524
525 def result = null
526 def error = null
527 409
528 try { 410 def sendSseError(ec, errorResponse, protocolVersion) {
529 // Route to appropriate MCP service using correct service names 411 // Set SSE headers
530 def serviceName = null 412 ec.web.response.setContentType("text/event-stream")
531 ec.logger.info("Mapping method '${method}' to service name") 413 ec.web.response.setCharacterEncoding("UTF-8")
532 switch (method) { 414 ec.web.response.setHeader("Cache-Control", "no-cache")
533 case "mcp#Ping": 415 ec.web.response.setHeader("Connection", "keep-alive")
534 case "ping": 416 ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
535 serviceName = "McpServices.mcp#Ping"
536 ec.logger.info("Mapped to service: ${serviceName}")
537 break
538 case "initialize":
539 case "mcp#Initialize":
540 serviceName = "McpServices.mcp#Initialize"
541 ec.logger.info("Mapped to service: ${serviceName}")
542 break
543 case "tools/list":
544 case "mcp#ToolsList":
545 serviceName = "McpServices.mcp#ToolsList"
546 ec.logger.info("Mapped to service: ${serviceName}")
547 break
548 case "tools/call":
549 case "mcp#ToolsCall":
550 serviceName = "McpServices.mcp#ToolsCall"
551 ec.logger.info("Mapped to service: ${serviceName}")
552 break
553 case "resources/list":
554 case "mcp#ResourcesList":
555 serviceName = "McpServices.mcp#ResourcesList"
556 ec.logger.info("Mapped to service: ${serviceName}")
557 break
558 case "resources/read":
559 case "mcp#ResourcesRead":
560 serviceName = "McpServices.mcp#ResourcesRead"
561 ec.logger.info("Mapped to service: ${serviceName}")
562 break
563 default:
564 ec.logger.warn("Unknown method: ${method}")
565 error = [
566 code: -32601,
567 message: "Method not found: ${method}"
568 ]
569 }
570 417
571 if (serviceName && !error) { 418 def writer = ec.web.response.writer
572 ec.logger.info("Calling service: ${serviceName} with params: ${params}")
573 // Check if service exists before calling
574 if (!ec.service.isServiceDefined(serviceName)) {
575 ec.logger.error("Service not defined: ${serviceName}")
576 error = [
577 code: -32601,
578 message: "Service not found: ${serviceName}"
579 ]
580 } else {
581 // Call the actual MCP service
582 def serviceStartTime = System.currentTimeMillis()
583 result = ec.service.sync().name(serviceName).parameters(params ?: [:]).call()
584 def serviceEndTime = System.currentTimeMillis()
585 ec.logger.info("Service ${serviceName} completed in ${serviceEndTime - serviceStartTime}ms")
586 ec.logger.info("Service result type: ${result?.getClass()?.getSimpleName()}")
587 ec.logger.info("Service result: ${result}")
588 }
589 }
590 419
591 } catch (Exception e) { 420 try {
592 ec.logger.error("MCP request error for method ${method}", e) 421 // Send error as SSE event
593 ec.logger.error("Exception details: ${e.getClass().getName()}: ${e.message}") 422 writer.write("event: error\n")
594 ec.logger.error("Exception stack trace: ${e.getStackTrace()}") 423 writer.write("data: ${errorResponse}\n")
595 error = [ 424 writer.write("\n")
596 code: -32603, 425 writer.flush()
597 message: "Internal error: ${e.message}" 426
598 ] 427 } catch (Exception e) {
599 } 428 ec.logger.error("Error sending SSE error: ${e.message}")
600 429 } finally {
601 // Build JSON-RPC response 430 writer.close()
602 ec.logger.info("Building JSON-RPC response") 431 }
603 def responseObj = [
604 jsonrpc: "2.0",
605 id: id
606 ]
607
608 if (error) {
609 responseObj.error = error
610 ec.logger.info("Response includes error: ${error}")
611 } else {
612 responseObj.result = result
613 ec.logger.info("Response includes result")
614 }
615
616 def jsonResponse = new JsonBuilder(responseObj).toString()
617 ec.logger.info("Built JSON response: ${jsonResponse}")
618 ec.logger.info("JSON response length: ${jsonResponse.length()}")
619
620 // Handle both JSON-RPC 2.0 and SSE responses
621 if (wantsStreaming) {
622 ec.logger.info("Creating SSE response")
623 sendSseResponse(ec, responseObj, protocolVersion)
624 } else {
625 ec.logger.info("Creating JSON-RPC 2.0 response")
626 ec.web.sendJsonResponse(jsonResponse)
627 ec.logger.info("Sent JSON-RPC response, length: ${jsonResponse.length()}")
628 } 432 }
629
630 ec.logger.info("=== MCP SCREEN REQUEST END ===")
631 ]]></script> 433 ]]></script>
632 </actions> 434 </actions>
633 435
......