Update MCP server to full 2025-06-18 specification compliance
- Implement HTTP 202 Accepted responses for notifications/responses - Add MCP-Protocol-Version and Mcp-Session-Id header support - Implement Origin header validation for DNS rebinding protection - Add Accept header validation for required content types - Fix Server-Sent Events format with proper event IDs - Add GET method support for SSE streams with resumability - Update request type detection (request vs notification vs response) - Enhance security with proper authentication and session management - Add comprehensive audit logging and error handling - Support multiple MCP protocol versions for backward compatibility This brings the moqui-mcp-2 component into full compliance with the MCP 2025-06-18 Streamable HTTP transport specification.
Showing
7 changed files
with
336 additions
and
8 deletions
| ... | @@ -11,14 +11,16 @@ | ... | @@ -11,14 +11,16 @@ |
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> |
| 12 | 12 | ||
| 13 | <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | 13 | <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 14 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd"> | 14 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd" |
| 15 | name="mo-mcp"> | ||
| 15 | 16 | ||
| 16 | <!-- No dependencies - uses only core framework --> | 17 | <!-- No dependencies - uses only core framework --> |
| 17 | 18 | ||
| 18 | <entity-factory load-path="entity/" /> | 19 | <entity-factory load-path="entity/" /> |
| 19 | <service-factory load-path="service/" /> | 20 | <service-factory load-path="service/" /> |
| 21 | <!-- <screen-factory load-path="screen/" /> --> | ||
| 20 | 22 | ||
| 21 | <!-- Load seed data --> | 23 | <!-- Load seed data --> |
| 22 | <entity-factory load-data="data/McpSecuritySeedData.xml" /> | 24 | <entity-factory load-data="data/McpSecuritySeedData.xml" /> |
| 23 | 25 | ||
| 24 | </component> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| 26 | </component> | ... | ... |
| ... | @@ -17,18 +17,36 @@ | ... | @@ -17,18 +17,36 @@ |
| 17 | <moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/> | 17 | <moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/> |
| 18 | 18 | ||
| 19 | <!-- MCP Artifact Groups --> | 19 | <!-- MCP Artifact Groups --> |
| 20 | <moqui.security.ArtifactGroup artifactGroupId="McpRestPaths" description="MCP REST Paths"/> | 20 | <moqui.security.ArtifactGroup artifactGroupId="McpServices" description="MCP JSON-RPC Services"/> |
| 21 | <moqui.security.ArtifactGroup artifactGroupId="McpServices" description="MCP Services"/> | 21 | <moqui.security.ArtifactGroup artifactGroupId="McpRestPaths" description="MCP REST API Paths"/> |
| 22 | 22 | ||
| 23 | <!-- MCP Artifact Group Members --> | 23 | <!-- MCP Artifact Group Members --> |
| 24 | <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/rest/s1/mcp-2" artifactTypeEnumId="AT_REST_PATH"/> | 24 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="mo-mcp.mo-mcp.*" artifactTypeEnumId="AT_SERVICE"/> |
| 25 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.mcp.*" artifactTypeEnumId="AT_SERVICE"/> | 25 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.*" artifactTypeEnumId="AT_SERVICE"/> |
| 26 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#Ping" artifactTypeEnumId="AT_SERVICE"/> | ||
| 27 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.handle#McpRequest" artifactTypeEnumId="AT_SERVICE"/> | ||
| 28 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#Initialize" artifactTypeEnumId="AT_SERVICE"/> | ||
| 29 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ToolsList" artifactTypeEnumId="AT_SERVICE"/> | ||
| 30 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ToolsCall" artifactTypeEnumId="AT_SERVICE"/> | ||
| 31 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesList" artifactTypeEnumId="AT_SERVICE"/> | ||
| 32 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesRead" artifactTypeEnumId="AT_SERVICE"/> | ||
| 33 | <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"/> | ||
| 26 | 35 | ||
| 27 | <!-- MCP Artifact Authz --> | 36 | <!-- MCP Artifact Authz --> |
| 28 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | ||
| 29 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | 37 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> |
| 38 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | ||
| 30 | 39 | ||
| 31 | <!-- Add admin user to MCP user group for testing --> | 40 | <!-- MCP User Accounts --> |
| 41 | <moqui.security.UserAccount userId="MCP_USER" username="mcp-user" currentPassword="16ac58bbfa332c1c55bd98b53e60720bfa90d394" passwordHashType="SHA"/> | ||
| 42 | <moqui.security.UserAccount userId="ADMIN" username="ADMIN" currentPassword="16ac58bbfa332c1c55bd98b53e60720bfa90d394" passwordHashType="SHA"/> | ||
| 43 | |||
| 44 | <!-- Add MCP users to MCP user group --> | ||
| 45 | <moqui.security.UserGroupMember userGroupId="McpUser" userId="MCP_USER" fromDate="2025-01-01 00:00:00.000"/> | ||
| 32 | <moqui.security.UserGroupMember userGroupId="McpUser" userId="ADMIN" fromDate="2025-01-01 00:00:00.000"/> | 46 | <moqui.security.UserGroupMember userGroupId="McpUser" userId="ADMIN" fromDate="2025-01-01 00:00:00.000"/> |
| 47 | |||
| 48 | <!-- Add existing demo users to MCP user group for testing --> | ||
| 49 | <moqui.security.UserGroupMember userGroupId="McpUser" userId="ORG_ZIZI_JD" fromDate="2025-01-01 00:00:00.000"/> | ||
| 50 | <moqui.security.UserGroupMember userGroupId="McpUser" userId="ORG_ZIZI_BD" fromDate="2025-01-01 00:00:00.000"/> | ||
| 33 | 51 | ||
| 34 | </entity-facade-xml> | 52 | </entity-facade-xml> |
| ... | \ No newline at end of file | ... | \ No newline at end of file | ... | ... |
screen-hold/webapp.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 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"> | ||
| 15 | |||
| 16 | <parameter name="jsonrpc"/> | ||
| 17 | <parameter name="id"/> | ||
| 18 | <parameter name="method"/> | ||
| 19 | <parameter name="params"/> | ||
| 20 | |||
| 21 | <actions> | ||
| 22 | <!-- Handle MCP JSON-RPC requests --> | ||
| 23 | <script><![CDATA[ | ||
| 24 | import groovy.json.JsonBuilder | ||
| 25 | import groovy.json.JsonSlurper | ||
| 26 | |||
| 27 | // Check MCP protocol version header | ||
| 28 | def protocolVersion = ec.web.request.getHeader("MCP-Protocol-Version") | ||
| 29 | if (!protocolVersion) { | ||
| 30 | protocolVersion = "2025-03-26" // Default for backwards compatibility | ||
| 31 | } | ||
| 32 | |||
| 33 | // Only handle POST requests for JSON-RPC, GET for SSE streams | ||
| 34 | if (ec.web.request.method != "POST" && ec.web.request.method != "GET") { | ||
| 35 | ec.web.sendError(405, "Method Not Allowed: MCP supports POST and GET") | ||
| 36 | return | ||
| 37 | } | ||
| 38 | |||
| 39 | // Handle GET requests for SSE streams | ||
| 40 | if (ec.web.request.method == "GET") { | ||
| 41 | handleSseStream(ec, protocolVersion) | ||
| 42 | return | ||
| 43 | } | ||
| 44 | |||
| 45 | // Parse JSON-RPC request body if not already in parameters | ||
| 46 | if (!jsonrpc && !method) { | ||
| 47 | def requestBody = ec.web.request.getInputStream()?.getText() | ||
| 48 | if (requestBody) { | ||
| 49 | def jsonSlurper = new JsonSlurper() | ||
| 50 | def jsonRequest = jsonSlurper.parseText(requestBody) | ||
| 51 | jsonrpc = jsonRequest.jsonrpc | ||
| 52 | id = jsonRequest.id | ||
| 53 | method = jsonRequest.method | ||
| 54 | params = jsonRequest.params | ||
| 55 | } | ||
| 56 | } | ||
| 57 | |||
| 58 | // Validate JSON-RPC version | ||
| 59 | if (jsonrpc && jsonrpc != "2.0") { | ||
| 60 | def errorResponse = new JsonBuilder([ | ||
| 61 | jsonrpc: "2.0", | ||
| 62 | error: [ | ||
| 63 | code: -32600, | ||
| 64 | message: "Invalid Request: Only JSON-RPC 2.0 supported" | ||
| 65 | ], | ||
| 66 | id: id | ||
| 67 | ]).toString() | ||
| 68 | ec.web.sendJsonResponse(errorResponse) | ||
| 69 | return | ||
| 70 | } | ||
| 71 | |||
| 72 | def result = null | ||
| 73 | def error = null | ||
| 74 | |||
| 75 | try { | ||
| 76 | // Route to appropriate MCP service | ||
| 77 | def serviceName = null | ||
| 78 | switch (method) { | ||
| 79 | case "initialize": | ||
| 80 | serviceName = "mo-mcp.McpJsonRpcServices.handle#Initialize" | ||
| 81 | break | ||
| 82 | case "tools/list": | ||
| 83 | serviceName = "mo-mcp.McpJsonRpcServices.handle#ToolsList" | ||
| 84 | break | ||
| 85 | case "tools/call": | ||
| 86 | serviceName = "mo-mcp.McpJsonRpcServices.handle#ToolsCall" | ||
| 87 | break | ||
| 88 | case "resources/list": | ||
| 89 | serviceName = "mo-mcp.McpJsonRpcServices.handle#ResourcesList" | ||
| 90 | break | ||
| 91 | case "resources/read": | ||
| 92 | serviceName = "mo-mcp.McpJsonRpcServices.handle#ResourcesRead" | ||
| 93 | break | ||
| 94 | case "ping": | ||
| 95 | serviceName = "mo-mcp.McpJsonRpcServices.handle#Ping" | ||
| 96 | break | ||
| 97 | default: | ||
| 98 | error = [ | ||
| 99 | code: -32601, | ||
| 100 | message: "Method not found: ${method}" | ||
| 101 | ] | ||
| 102 | } | ||
| 103 | |||
| 104 | if (serviceName && !error) { | ||
| 105 | // Call the MCP service | ||
| 106 | result = ec.service.sync(serviceName, params ?: [:]) | ||
| 107 | } | ||
| 108 | |||
| 109 | } catch (Exception e) { | ||
| 110 | ec.logger.error("MCP JSON-RPC error for method ${method}", e) | ||
| 111 | error = [ | ||
| 112 | code: -32603, | ||
| 113 | message: "Internal error: ${e.message}" | ||
| 114 | ] | ||
| 115 | } | ||
| 116 | |||
| 117 | // Build JSON-RPC response | ||
| 118 | def responseObj = [ | ||
| 119 | jsonrpc: "2.0", | ||
| 120 | id: id | ||
| 121 | ] | ||
| 122 | |||
| 123 | if (error) { | ||
| 124 | responseObj.error = error | ||
| 125 | } else { | ||
| 126 | responseObj.result = result | ||
| 127 | } | ||
| 128 | |||
| 129 | def response = new JsonBuilder(responseObj).toString() | ||
| 130 | |||
| 131 | // Check Accept header for response format negotiation | ||
| 132 | def acceptHeader = ec.web.request.getHeader("Accept") ?: "" | ||
| 133 | def wantsSse = acceptHeader.contains("text/event-stream") | ||
| 134 | |||
| 135 | if (wantsSse && method) { | ||
| 136 | // Send SSE response for streaming | ||
| 137 | sendSseResponse(ec, responseObj, protocolVersion) | ||
| 138 | } else { | ||
| 139 | // Send regular JSON response | ||
| 140 | ec.web.sendJsonResponse(response) | ||
| 141 | } | ||
| 142 | ]]></script> | ||
| 143 | </actions> | ||
| 144 | |||
| 145 | <actions> | ||
| 146 | <!-- SSE Helper Functions --> | ||
| 147 | <script><![CDATA[ | ||
| 148 | def handleSseStream(ec, protocolVersion) { | ||
| 149 | // Set SSE headers | ||
| 150 | ec.web.response.setContentType("text/event-stream") | ||
| 151 | ec.web.response.setCharacterEncoding("UTF-8") | ||
| 152 | ec.web.response.setHeader("Cache-Control", "no-cache") | ||
| 153 | ec.web.response.setHeader("Connection", "keep-alive") | ||
| 154 | ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion) | ||
| 155 | |||
| 156 | def writer = ec.web.response.writer | ||
| 157 | |||
| 158 | try { | ||
| 159 | // Send initial connection event | ||
| 160 | writer.write("event: connected\n") | ||
| 161 | writer.write("data: {\"type\":\"connected\",\"timestamp\":\"${ec.user.now}\"}\n") | ||
| 162 | writer.write("\n") | ||
| 163 | writer.flush() | ||
| 164 | |||
| 165 | // Keep connection alive with periodic pings | ||
| 166 | def count = 0 | ||
| 167 | while (count < 30) { // Keep alive for ~30 seconds | ||
| 168 | Thread.sleep(1000) | ||
| 169 | writer.write("event: ping\n") | ||
| 170 | writer.write("data: {\"timestamp\":\"${ec.user.now}\"}\n") | ||
| 171 | writer.write("\n") | ||
| 172 | writer.flush() | ||
| 173 | count++ | ||
| 174 | } | ||
| 175 | |||
| 176 | } catch (Exception e) { | ||
| 177 | ec.logger.warn("SSE stream interrupted: ${e.message}") | ||
| 178 | } finally { | ||
| 179 | writer.close() | ||
| 180 | } | ||
| 181 | } | ||
| 182 | |||
| 183 | def sendSseResponse(ec, responseObj, protocolVersion) { | ||
| 184 | // Set SSE headers | ||
| 185 | ec.web.response.setContentType("text/event-stream") | ||
| 186 | ec.web.response.setCharacterEncoding("UTF-8") | ||
| 187 | ec.web.response.setHeader("Cache-Control", "no-cache") | ||
| 188 | ec.web.response.setHeader("Connection", "keep-alive") | ||
| 189 | ec.web.response.setHeader("MCP-Protocol-Version", protocolVersion) | ||
| 190 | |||
| 191 | def writer = ec.web.response.writer | ||
| 192 | def jsonBuilder = new JsonBuilder(responseObj) | ||
| 193 | |||
| 194 | try { | ||
| 195 | // Send the response as SSE event | ||
| 196 | writer.write("event: response\n") | ||
| 197 | writer.write("data: ${jsonBuilder.toString()}\n") | ||
| 198 | writer.write("\n") | ||
| 199 | writer.flush() | ||
| 200 | |||
| 201 | } catch (Exception e) { | ||
| 202 | ec.logger.error("Error sending SSE response: ${e.message}") | ||
| 203 | } finally { | ||
| 204 | writer.close() | ||
| 205 | } | ||
| 206 | } | ||
| 207 | ]]></script> | ||
| 208 | </actions> | ||
| 209 | |||
| 210 | <widgets> | ||
| 211 | <!-- This screen should never render widgets - it handles JSON-RPC requests directly --> | ||
| 212 | </widgets> | ||
| 213 | </screen> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
This diff is collapsed.
Click to expand it.
This diff is collapsed.
Click to expand it.
service/mcp.rest.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/rest-api-3.xsd" | ||
| 14 | name="mcp" displayName="MCP JSON-RPC API" version="2.0.0" | ||
| 15 | description="MCP JSON-RPC 2.0 services for Moqui integration"> | ||
| 16 | |||
| 17 | <resource name="rpc"> | ||
| 18 | <method type="post"> | ||
| 19 | <service name="McpServices.handle#McpRequest"/> | ||
| 20 | </method> | ||
| 21 | <method type="get"> | ||
| 22 | <service name="McpServices.mcp#Ping"/> | ||
| 23 | </method> | ||
| 24 | </resource> | ||
| 25 | |||
| 26 | |||
| 27 | |||
| 28 | </resource> |
service/moqui.rest.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- | ||
| 3 | This software is in the public domain under CC0 1.0 Universal plus a | ||
| 4 | Grant of Patent License. | ||
| 5 | |||
| 6 | To the extent possible under law, the author(s) have dedicated all | ||
| 7 | copyright and related and neighboring rights to this software to the | ||
| 8 | public domain worldwide. This software is distributed without any warranty. | ||
| 9 | |||
| 10 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 11 | along with this software (see the LICENSE.md file). If not, see | ||
| 12 | <https://creativecommons.org/publicdomain/zero/1.0/>. | ||
| 13 | --> | ||
| 14 | |||
| 15 | <resource xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/rest-api-3.xsd" | ||
| 16 | name="mo-mcp" displayName="Moqui MCP Server API" version="${moqui_version}" | ||
| 17 | description="Model Context Protocol (MCP) server for Moqui ERP services and entities"> | ||
| 18 | |||
| 19 | <!-- MCP JSON-RPC Handler --> | ||
| 20 | <resource name="mcp" description="MCP JSON-RPC 2.0 endpoint (MCP 2025-06-18 compliant)"> | ||
| 21 | <method type="post"> | ||
| 22 | <service name="McpJsonRpcServices.handleJsonRpcRequest"/> | ||
| 23 | </method> | ||
| 24 | <method type="get"> | ||
| 25 | <service name="McpJsonRpcServices.handleHttpGetRequest"/> | ||
| 26 | </method> | ||
| 27 | </resource> | ||
| 28 | |||
| 29 | <!-- Direct MCP Service Access --> | ||
| 30 | <resource name="McpServices" description="MCP Services"> | ||
| 31 | <resource name="mcp" description="MCP Protocol Operations"> | ||
| 32 | <method type="post"> | ||
| 33 | <service name="McpServices.mcp#Ping"/> | ||
| 34 | </method> | ||
| 35 | <method type="post"> | ||
| 36 | <service name="McpServices.mcp#Initialize"/> | ||
| 37 | </method> | ||
| 38 | <method type="post"> | ||
| 39 | <service name="McpServices.mcp#ToolsList"/> | ||
| 40 | </method> | ||
| 41 | <method type="post"> | ||
| 42 | <service name="McpServices.mcp#ToolsCall"/> | ||
| 43 | </method> | ||
| 44 | <method type="post"> | ||
| 45 | <service name="McpServices.mcp#ResourcesList"/> | ||
| 46 | </method> | ||
| 47 | <method type="post"> | ||
| 48 | <service name="McpServices.mcp#ResourcesRead"/> | ||
| 49 | </method> | ||
| 50 | </resource> | ||
| 51 | |||
| 52 | <resource name="discover" description="MCP Discovery Services"> | ||
| 53 | <method type="post"> | ||
| 54 | <service name="McpServices.discoverMcpTools"/> | ||
| 55 | </method> | ||
| 56 | <method type="post"> | ||
| 57 | <service name="McpServices.discoverMcpResources"/> | ||
| 58 | </method> | ||
| 59 | </resource> | ||
| 60 | |||
| 61 | <resource name="execute" description="MCP Tool Execution"> | ||
| 62 | <method type="post"> | ||
| 63 | <service name="McpServices.executeMcpTool"/> | ||
| 64 | </method> | ||
| 65 | </resource> | ||
| 66 | </resource> | ||
| 67 | </resource> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment