webapp.xml 8.53 KB
<?xml version="1.0" encoding="UTF-8"?>
<!-- This software is in the public domain under CC0 1.0 Universal plus a 
     Grant of Patent License.

     To the extent possible under law, the author(s) have dedicated all
     copyright and related and neighboring rights to this software to the
     public domain worldwide. This software is distributed without any warranty.

     You should have received a copy of the CC0 Public Domain Dedication
     along with this software (see the LICENSE.md file). If not, see
     <https://creativecommons.org/publicdomain/zero/1.0/>. -->

<screen xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/xml-screen-3.xsd"
        require-authentication="true" track-artifact-hit="false" default-menu-include="false">
    
    <parameter name="jsonrpc"/>
    <parameter name="id"/>
    <parameter name="method"/>
    <parameter name="params"/>
    
    <actions>
        <!-- Handle MCP JSON-RPC requests -->
        <script><![CDATA[
            import groovy.json.JsonBuilder
            import groovy.json.JsonSlurper
            
            // Check MCP protocol version header
            def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version")
            if (!protocolVersion) {
                protocolVersion = "2025-03-26" // Default for backwards compatibility
            }
            
            // Only handle POST requests for JSON-RPC, GET for SSE streams
            if (ec.web.request.method != "POST" && ec.web.request.method != "GET") {
                ec.web.sendError(405, "Method Not Allowed: MCP supports POST and GET")
                return
            }
            
            // Handle GET requests for SSE streams
            if (ec.web.request.method == "GET") {
                handleSseStream(ec, protocolVersion)
                return
            }
            
            // Parse JSON-RPC request body if not already in parameters
            if (!jsonrpc && !method) {
                def requestBody = ec.web.request.getInputStream()?.getText()
                if (requestBody) {
                    def jsonSlurper = new JsonSlurper()
                    def jsonRequest = jsonSlurper.parseText(requestBody)
                    jsonrpc = jsonRequest.jsonrpc
                    id = jsonRequest.id
                    method = jsonRequest.method
                    params = jsonRequest.params
                }
            }
            
            // Validate JSON-RPC version
            if (jsonrpc && jsonrpc != "2.0") {
                def errorResponse = new JsonBuilder([
                    jsonrpc: "2.0",
                    error: [
                        code: -32600,
                        message: "Invalid Request: Only JSON-RPC 2.0 supported"
                    ],
                    id: id
                ]).toString()
                ec.web.sendJsonResponse(errorResponse)
                return
            }
            
            def result = null
            def error = null
            
            try {
                // Route to appropriate MCP service
                def serviceName = null
                switch (method) {
                    case "initialize":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#Initialize"
                        break
                    case "tools/list":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#ToolsList"
                        break
                    case "tools/call":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#ToolsCall"
                        break
                    case "resources/list":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#ResourcesList"
                        break
                    case "resources/read":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#ResourcesRead"
                        break
                    case "ping":
                        serviceName = "mo-mcp.McpJsonRpcServices.handle#Ping"
                        break
                    default:
                        error = [
                            code: -32601,
                            message: "Method not found: ${method}"
                        ]
                }
                
                if (serviceName && !error) {
                    // Call the MCP service
                    result = ec.service.sync(serviceName, params ?: [:])
                }
                
            } catch (Exception e) {
                ec.logger.error("MCP JSON-RPC error for method ${method}", e)
                error = [
                    code: -32603,
                    message: "Internal error: ${e.message}"
                ]
            }
            
            // Build JSON-RPC response
            def responseObj = [
                jsonrpc: "2.0",
                id: id
            ]
            
            if (error) {
                responseObj.error = error
            } else {
                responseObj.result = result
            }
            
            def response = new JsonBuilder(responseObj).toString()
            
            // Check Accept header for response format negotiation
            def acceptHeader = ec.web.request.getHeader("Accept") ?: ""
            def wantsSse = acceptHeader.contains("text/event-stream")
            
            if (wantsSse && method) {
                // Send SSE response for streaming
                sendSseResponse(ec, responseObj, protocolVersion)
            } else {
                // Send regular JSON response
                ec.web.sendJsonResponse(response)
            }
        ]]></script>
    </actions>
    
    <actions>
        <!-- SSE Helper Functions -->
        <script><![CDATA[
            def handleSseStream(ec, protocolVersion) {
                // Set SSE headers
                ec.web.response.setContentType("text/event-stream")
                ec.web.response.setCharacterEncoding("UTF-8")
                ec.web.response.setHeader("Cache-Control", "no-cache")
                ec.web.response.setHeader("Connection", "keep-alive")
                ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
                
                def writer = ec.web.response.writer
                
                try {
                    // Send initial connection event
                    writer.write("event: connected\n")
                    writer.write("data: {\"type\":\"connected\",\"timestamp\":\"${ec.user.now}\"}\n")
                    writer.write("\n")
                    writer.flush()
                    
                    // Keep connection alive with periodic pings
                    def count = 0
                    while (count < 30) { // Keep alive for ~30 seconds
                        Thread.sleep(1000)
                        writer.write("event: ping\n")
                        writer.write("data: {\"timestamp\":\"${ec.user.now}\"}\n")
                        writer.write("\n")
                        writer.flush()
                        count++
                    }
                    
                } catch (Exception e) {
                    ec.logger.warn("SSE stream interrupted: ${e.message}")
                } finally {
                    writer.close()
                }
            }
            
            def sendSseResponse(ec, responseObj, protocolVersion) {
                // Set SSE headers
                ec.web.response.setContentType("text/event-stream")
                ec.web.response.setCharacterEncoding("UTF-8")
                ec.web.response.setHeader("Cache-Control", "no-cache")
                ec.web.response.setHeader("Connection", "keep-alive")
                ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion)
                
                def writer = ec.web.response.writer
                def jsonBuilder = new JsonBuilder(responseObj)
                
                try {
                    // Send the response as SSE event
                    writer.write("event: response\n")
                    writer.write("data: ${jsonBuilder.toString()}\n")
                    writer.write("\n")
                    writer.flush()
                    
                } catch (Exception e) {
                    ec.logger.error("Error sending SSE response: ${e.message}")
                } finally {
                    writer.close()
                }
            }
        ]]></script>
    </actions>
    
    <widgets>
        <!-- This screen should never render widgets - it handles JSON-RPC requests directly -->
    </widgets>
</screen>