720946f4 by Ean Schuessler

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.
1 parent 2dc86743
...@@ -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
......
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
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>
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