more security and attempts to control the content-type
Showing
2 changed files
with
131 additions
and
328 deletions
| ... | @@ -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 | ... | ... |
-
Please register or sign in to post a comment