2dc86743 by Ean Schuessler

Implement clean Moqui-centric MCP v2.0 using built-in JSON-RPC

- Replace custom REST API with Moqui's native /rpc/json endpoint
- Implement MCP methods as standard Moqui services with allow-remote='true'
- Remove unnecessary custom layers (webapp, screens, custom JSON-RPC handler)
- Direct service-to-tool mapping for maximum simplicity
- Leverage Moqui's built-in authentication, permissions, and audit logging
- Comprehensive client examples for Python, JavaScript, and cURL
- Complete documentation with architecture overview and usage patterns

Key Changes:
- service/McpServices.xml: MCP methods as standard Moqui services
- component.xml: Minimal configuration, no custom webapp
- AGENTS.md: Updated for Moqui-centric approach
- entity/, data/: Minimal extensions, leverage built-in entities
- Removed: mcp.rest.xml, screen/ directory (unnecessary complexity)

This demonstrates the power of Moqui's built-in JSON-RPC support
for clean, maintainable MCP integration.
1 parent 8ad686a8
1 # Moqui MCP v2.0 Server Guide
2
3 This guide explains how to interact with Moqui MCP v2.0 server for AI-Moqui integration using Model Context Protocol (MCP) and Moqui's built-in JSON-RPC support.
4
5 ## Repository Status
6
7 **moqui-mcp-2** is a **standalone component with its own git repository**. It uses Moqui's native JSON-RPC framework for maximum compatibility and minimal complexity.
8
9 - **Repository**: Independent git repository
10 - **Integration**: Included as plain directory in moqui-opencode
11 - **Version**: v2.0.0 (clean MCP implementation)
12 - **Approach**: Moqui-centric using built-in JSON-RPC support
13
14 ## Architecture Overview
15
16 ```
17 moqui-mcp-2/
18 ├── AGENTS.md # This file - MCP server guide
19 ├── component.xml # Component configuration
20 ├── service/
21 │ └── McpServices.xml # MCP services with allow-remote="true"
22 └── data/
23 └── McpSecuritySeedData.xml # Security permissions
24 ```
25
26 ## MCP Implementation Strategy
27
28 ### **Moqui-Centric Design**
29 -**Built-in JSON-RPC**: Uses Moqui's `/rpc/json` endpoint
30 -**Standard Services**: MCP methods as regular Moqui services
31 -**Native Authentication**: Leverages Moqui's user authentication
32 -**Direct Integration**: No custom layers or abstractions
33 -**Audit Logging**: Uses Moqui's ArtifactHit framework
34
35 ### **MCP Method Mapping**
36 ```
37 MCP Method → Moqui Service
38 initialize → org.moqui.mcp.McpServices.mcp#Initialize
39 tools/list → org.moqui.mcp.McpServices.mcp#ToolsList
40 tools/call → org.moqui.mcp.McpServices.mcp#ToolsCall
41 resources/list → org.moqui.mcp.McpServices.mcp#ResourcesList
42 resources/read → org.moqui.mcp.McpServices.mcp#ResourcesRead
43 ping → org.moqui.mcp.McpServices.mcp#Ping
44 ```
45
46 ## JSON-RPC Endpoint
47
48 ### **Moqui Built-in Endpoint**
49 ```
50 POST /rpc/json
51 - Moqui's native JSON-RPC 2.0 endpoint
52 - Handles all services with allow-remote="true"
53 - Built-in authentication and audit logging
54 ```
55
56 ### **JSON-RPC Request Format**
57 ```json
58 {
59 "jsonrpc": "2.0",
60 "id": "unique_request_id",
61 "method": "org.moqui.mcp.McpServices.mcp#Initialize",
62 "params": {
63 "protocolVersion": "2025-06-18",
64 "capabilities": {
65 "roots": {},
66 "sampling": {}
67 },
68 "clientInfo": {
69 "name": "AI Client",
70 "version": "1.0.0"
71 }
72 }
73 }
74 ```
75
76 ## MCP Client Integration
77
78 ### **Python Client Example**
79 ```python
80 import requests
81 import json
82 import uuid
83
84 class MoquiMCPClient:
85 def __init__(self, base_url, username, password):
86 self.base_url = base_url
87 self.request_id = 0
88 self.authenticate(username, password)
89
90 def _make_request(self, method, params=None):
91 self.request_id += 1
92 payload = {
93 "jsonrpc": "2.0",
94 "id": f"req_{self.request_id}",
95 "method": method,
96 "params": params or {}
97 }
98
99 response = requests.post(f"{self.base_url}/rpc/json", json=payload)
100 return response.json()
101
102 def initialize(self):
103 return self._make_request("org.moqui.mcp.McpServices.mcp#Initialize", {
104 "protocolVersion": "2025-06-18",
105 "capabilities": {
106 "roots": {},
107 "sampling": {}
108 },
109 "clientInfo": {
110 "name": "Python MCP Client",
111 "version": "1.0.0"
112 }
113 })
114
115 def list_tools(self):
116 return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsList")
117
118 def call_tool(self, tool_name, arguments):
119 return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsCall", {
120 "name": tool_name,
121 "arguments": arguments
122 })
123
124 def list_resources(self):
125 return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesList")
126
127 def read_resource(self, uri):
128 return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesRead", {
129 "uri": uri
130 })
131
132 def ping(self):
133 return self._make_request("org.moqui.mcp.McpServices.mcp#Ping")
134
135 # Usage
136 client = MoquiMCPClient("http://localhost:8080", "admin", "moqui")
137 init_result = client.initialize()
138 tools = client.list_tools()
139 result = client.call_tool("org.moqui.example.Services.create#Example", {
140 "exampleName": "Test Example"
141 })
142 ```
143
144 ### **JavaScript Client Example**
145 ```javascript
146 class MoquiMCPClient {
147 constructor(baseUrl, username, password) {
148 this.baseUrl = baseUrl;
149 this.requestId = 0;
150 this.authenticate(username, password);
151 }
152
153 async makeRequest(method, params = {}) {
154 this.requestId++;
155 const payload = {
156 jsonrpc: "2.0",
157 id: `req_${this.requestId}`,
158 method,
159 params
160 };
161
162 const response = await fetch(`${this.baseUrl}/rpc/json`, {
163 method: 'POST',
164 headers: { 'Content-Type': 'application/json' },
165 body: JSON.stringify(payload)
166 });
167
168 return response.json();
169 }
170
171 async initialize() {
172 return this.makeRequest('org.moqui.mcp.McpServices.mcp#Initialize', {
173 protocolVersion: '2025-06-18',
174 capabilities: {
175 roots: {},
176 sampling: {}
177 },
178 clientInfo: {
179 name: 'JavaScript MCP Client',
180 version: '1.0.0'
181 }
182 });
183 }
184
185 async listTools() {
186 return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsList');
187 }
188
189 async callTool(toolName, arguments) {
190 return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsCall', {
191 name: toolName,
192 arguments
193 });
194 }
195
196 async listResources() {
197 return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesList');
198 }
199
200 async readResource(uri) {
201 return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesRead', {
202 uri
203 });
204 }
205
206 async ping() {
207 return this.makeRequest('org.moqui.mcp.McpServices.mcp#Ping');
208 }
209 }
210
211 // Usage
212 const client = new MoquiMCPClient('http://localhost:8080', 'admin', 'moqui');
213 await client.initialize();
214 const tools = await client.listTools();
215 const result = await client.callTool('org.moqui.example.Services.create#Example', {
216 exampleName: 'Test Example'
217 });
218 ```
219
220 ### **cURL Examples**
221
222 #### **Initialize MCP Session**
223 ```bash
224 curl -X POST -H "Content-Type: application/json" \
225 --data '{
226 "jsonrpc": "2.0",
227 "id": "init_001",
228 "method": "org.moqui.mcp.McpServices.mcp#Initialize",
229 "params": {
230 "protocolVersion": "2025-06-18",
231 "capabilities": {
232 "roots": {},
233 "sampling": {}
234 },
235 "clientInfo": {
236 "name": "cURL Client",
237 "version": "1.0.0"
238 }
239 }
240 }' \
241 http://localhost:8080/rpc/json
242 ```
243
244 #### **List Available Tools**
245 ```bash
246 curl -X POST -H "Content-Type: application/json" \
247 --data '{
248 "jsonrpc": "2.0",
249 "id": "tools_001",
250 "method": "org.moqui.mcp.McpServices.mcp#ToolsList",
251 "params": {}
252 }' \
253 http://localhost:8080/rpc/json
254 ```
255
256 #### **Execute Tool**
257 ```bash
258 curl -X POST -H "Content-Type: application/json" \
259 --data '{
260 "jsonrpc": "2.0",
261 "id": "call_001",
262 "method": "org.moqui.mcp.McpServices.mcp#ToolsCall",
263 "params": {
264 "name": "org.moqui.example.Services.create#Example",
265 "arguments": {
266 "exampleName": "JSON-RPC Test",
267 "statusId": "EXST_ACTIVE"
268 }
269 }
270 }' \
271 http://localhost:8080/rpc/json
272 ```
273
274 #### **Read Entity Resource**
275 ```bash
276 curl -X POST -H "Content-Type: application/json" \
277 --data '{
278 "jsonrpc": "2.0",
279 "id": "resource_001",
280 "method": "org.moqui.mcp.McpServices.mcp#ResourcesRead",
281 "params": {
282 "uri": "entity://Example"
283 }
284 }' \
285 http://localhost:8080/rpc/json
286 ```
287
288 #### **Health Check**
289 ```bash
290 curl -X POST -H "Content-Type: application/json" \
291 --data '{
292 "jsonrpc": "2.0",
293 "id": "ping_001",
294 "method": "org.moqui.mcp.McpServices.mcp#Ping",
295 "params": {}
296 }' \
297 http://localhost:8080/rpc/json
298 ```
299
300 ## Security and Permissions
301
302 ### **Authentication**
303 - Uses Moqui's built-in user authentication
304 - Pass credentials via standard JSON-RPC `authUsername` and `authPassword` parameters
305 - Or use existing session via Moqui's web session
306
307 ### **Authorization**
308 - All MCP services require `authenticate="true"`
309 - Tool access controlled by Moqui's service permissions
310 - Entity access controlled by Moqui's entity permissions
311 - Audit logging via Moqui's ArtifactHit framework
312
313 ### **Permission Setup**
314 ```xml
315 <!-- MCP User Group -->
316 <moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/>
317
318 <!-- MCP Service Permissions -->
319 <moqui.security.ArtifactAuthz userGroupId="McpUser"
320 artifactGroupId="McpServices"
321 authzTypeEnumId="AUTHZT_ALLOW"
322 authzActionEnumId="AUTHZA_ALL"/>
323 ```
324
325 ## Development and Testing
326
327 ### **Local Development**
328 1. Start Moqui with moqui-mcp-2 component
329 2. Test with JSON-RPC client examples above
330 3. Check Moqui logs for debugging
331 4. Monitor ArtifactHit records for audit trail
332
333 ### **Service Discovery**
334 - All Moqui services automatically available as MCP tools
335 - Filtered by user permissions
336 - Service parameters converted to JSON Schema
337 - Service descriptions used for tool descriptions
338
339 ### **Entity Access**
340 - All Moqui entities automatically available as MCP resources
341 - Format: `entity://EntityName`
342 - Filtered by entity VIEW permissions
343 - Limited to 100 records per request
344
345 ## Error Handling
346
347 ### **JSON-RPC Error Responses**
348 ```json
349 {
350 "jsonrpc": "2.0",
351 "id": "req_001",
352 "error": {
353 "code": -32601,
354 "message": "Method not found"
355 }
356 }
357 ```
358
359 ### **Common Error Codes**
360 - `-32600`: Invalid Request (malformed JSON-RPC)
361 - `-32601`: Method not found
362 - `-32602`: Invalid params
363 - `-32603`: Internal error (service execution failed)
364
365 ### **Service-Level Errors**
366 - Permission denied throws Exception with message
367 - Invalid service name throws Exception
368 - Entity access violations throw Exception
369 - All errors logged to Moqui error system
370
371 ## Monitoring and Debugging
372
373 ### **Audit Trail**
374 - All MCP calls logged to `moqui.server.ArtifactHit`
375 - Track user, service, parameters, execution time
376 - Monitor success/failure rates
377 - Debug via Moqui's built-in monitoring tools
378
379 ### **Performance Metrics**
380 - Service execution time tracked
381 - Response size monitored
382 - Error rates captured
383 - User activity patterns available
384
385 ---
386
387 ## Key Advantages
388
389 ### **Moqui-Centric Benefits**
390 1. **Simplicity**: No custom JSON-RPC handling needed
391 2. **Reliability**: Uses battle-tested Moqui framework
392 3. **Security**: Leverages Moqui's mature security system
393 4. **Audit**: Built-in logging and monitoring
394 5. **Compatibility**: Standard JSON-RPC 2.0 compliance
395 6. **Maintenance**: Minimal custom code to maintain
396
397 ### **Clean Architecture**
398 - No custom webapps or screens needed
399 - No custom authentication layers
400 - No custom session management
401 - Direct service-to-tool mapping
402 - Standard Moqui patterns throughout
403
404 **This implementation demonstrates the power of Moqui's built-in JSON-RPC support for MCP integration.**
...\ 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 <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd">
15
16 <!-- No dependencies - uses only core framework -->
17
18 <entity-factory load-path="entity/" />
19 <service-factory load-path="service/" />
20
21 <!-- Load seed data -->
22 <entity-factory load-data="data/McpSecuritySeedData.xml" />
23
24 </component>
...\ 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 <entity-facade-xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-facade-3.xsd">
15
16 <!-- MCP User Group -->
17 <moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/>
18
19 <!-- MCP Artifact Groups -->
20 <moqui.security.ArtifactGroup artifactGroupId="McpRestPaths" description="MCP REST Paths"/>
21 <moqui.security.ArtifactGroup artifactGroupId="McpServices" description="MCP Services"/>
22
23 <!-- MCP Artifact Group Members -->
24 <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/rest/s1/mcp-2" artifactTypeEnumId="AT_REST_PATH"/>
25 <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.mcp.*" artifactTypeEnumId="AT_SERVICE"/>
26
27 <!-- 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"/>
30
31 <!-- Add admin user to MCP user group for testing -->
32 <moqui.security.UserGroupMember userGroupId="McpUser" userId="ADMIN" fromDate="2025-01-01 00:00:00.000"/>
33
34 </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 <entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-definition-3.xsd">
15
16 <!-- No custom entities needed - use Moqui's built-in entities directly -->
17 <!-- MCP uses standard Moqui authentication, audit logging, and permissions -->
18
19 </entities>
...\ 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 <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
15
16 <!-- MCP JSON-RPC 2.0 Handler -->
17
18 <service verb="handle" noun="JsonRpcRequest" authenticate="false" transaction-timeout="300">
19 <description>Handle MCP JSON-RPC 2.0 requests with direct Moqui integration</description>
20 <in-parameters>
21 <parameter name="jsonrpc" type="text-short" required="true"/>
22 <parameter name="id" type="text-medium"/>
23 <parameter name="method" type="text-medium" required="true"/>
24 <parameter name="params" type="Map"/>
25 </in-parameters>
26 <out-parameters>
27 <parameter name="response" type="text-very-long"/>
28 </out-parameters>
29 <actions>
30 <script><![CDATA[
31 import org.moqui.context.ExecutionContext
32 import groovy.json.JsonBuilder
33 import groovy.json.JsonSlurper
34 import java.util.UUID
35
36 ExecutionContext ec = context.ec
37
38 // Validate JSON-RPC version
39 if (jsonrpc != "2.0") {
40 response = new JsonBuilder([
41 jsonrpc: "2.0",
42 error: [
43 code: -32600,
44 message: "Invalid Request: Only JSON-RPC 2.0 supported"
45 ],
46 id: id
47 ]).toString()
48 return
49 }
50
51 def result = null
52 def error = null
53
54 try {
55 // Route to appropriate MCP method handler
56 switch (method) {
57 case "initialize":
58 result = handleInitialize(params, ec)
59 break
60 case "tools/list":
61 result = handleToolsList(params, ec)
62 break
63 case "tools/call":
64 result = handleToolsCall(params, ec)
65 break
66 case "resources/list":
67 result = handleResourcesList(params, ec)
68 break
69 case "resources/read":
70 result = handleResourcesRead(params, ec)
71 break
72 case "ping":
73 result = handlePing(params, ec)
74 break
75 default:
76 error = [
77 code: -32601,
78 message: "Method not found: ${method}"
79 ]
80 }
81 } catch (Exception e) {
82 ec.logger.error("MCP JSON-RPC error for method ${method}", e)
83 error = [
84 code: -32603,
85 message: "Internal error: ${e.message}"
86 ]
87 }
88
89 // Build JSON-RPC response
90 def responseObj = [
91 jsonrpc: "2.0",
92 id: id
93 ]
94
95 if (error) {
96 responseObj.error = error
97 } else {
98 responseObj.result = result
99 }
100
101 response = new JsonBuilder(responseObj).toString()
102
103 // Log request for audit
104 ec.message.addMessage("MCP ${method} request processed", "info")
105 ]]></script>
106 </actions>
107 </service>
108
109 <!-- MCP Method Implementations -->
110
111 <service verb="handle" noun="Initialize" authenticate="false" transaction-timeout="30">
112 <description>Handle MCP initialize request with Moqui authentication</description>
113 <in-parameters>
114 <parameter name="protocolVersion" type="text-medium" required="true"/>
115 <parameter name="capabilities" type="Map"/>
116 <parameter name="clientInfo" type="Map"/>
117 </in-parameters>
118 <out-parameters>
119 <parameter name="result" type="Map"/>
120 </out-parameters>
121 <actions>
122 <script><![CDATA[
123 import org.moqui.context.ExecutionContext
124 import groovy.json.JsonBuilder
125
126 ExecutionContext ec = context.ec
127
128 // Validate protocol version
129 if (protocolVersion != "2025-06-18") {
130 throw new Exception("Unsupported protocol version: ${protocolVersion}")
131 }
132
133 // Get current user context (if authenticated)
134 def userId = ec.user.userId
135 def userAccountId = userId ? userId : null
136
137 // Build server capabilities
138 def serverCapabilities = [
139 tools: [:],
140 resources: [:],
141 logging: [:]
142 ]
143
144 // Build server info
145 def serverInfo = [
146 name: "Moqui MCP Server",
147 version: "2.0.0"
148 ]
149
150 result = [
151 protocolVersion: "2025-06-18",
152 capabilities: serverCapabilities,
153 serverInfo: serverInfo,
154 instructions: "This server provides access to Moqui ERP services and entities through MCP. Use tools/list to discover available operations."
155 ]
156 ]]></script>
157 </actions>
158 </service>
159
160 <service verb="handle" noun="ToolsList" authenticate="false" transaction-timeout="60">
161 <description>Handle MCP tools/list request with direct Moqui service discovery</description>
162 <in-parameters>
163 <parameter name="cursor" type="text-medium"/>
164 </in-parameters>
165 <out-parameters>
166 <parameter name="result" type="Map"/>
167 </out-parameters>
168 <actions>
169 <script><![CDATA[
170 import org.moqui.context.ExecutionContext
171 import groovy.json.JsonBuilder
172
173 ExecutionContext ec = context.ec
174
175 // Get all service names from Moqui service engine
176 def allServiceNames = ec.service.getServiceNames()
177 def availableTools = []
178
179 // Convert services to MCP tools
180 for (serviceName in allServiceNames) {
181 try {
182 // Check if user has permission
183 if (!ec.service.hasPermission(serviceName)) {
184 continue
185 }
186
187 def serviceInfo = ec.service.getServiceInfo(serviceName)
188 if (!serviceInfo) continue
189
190 // Convert service to MCP tool format
191 def tool = [
192 name: serviceName,
193 description: serviceInfo.description ?: "Moqui service: ${serviceName}",
194 inputSchema: [
195 type: "object",
196 properties: [:],
197 required: []
198 ]
199 ]
200 ]
201
202 // Convert service parameters to JSON Schema
203 def inParamNames = serviceInfo.getInParameterNames()
204 for (paramName in inParamNames) {
205 def paramInfo = serviceInfo.getInParameter(paramName)
206 tool.inputSchema.properties[paramName] = [
207 type: convertMoquiTypeToJsonSchemaType(paramInfo.type),
208 description: paramInfo.description ?: ""
209 ]
210
211 if (paramInfo.required) {
212 tool.inputSchema.required << paramName
213 }
214 }
215
216 availableTools << tool
217
218 } catch (Exception e) {
219 ec.logger.warn("Error processing service ${serviceName}: ${e.message}")
220 }
221 }
222
223 result = [
224 tools: availableTools
225 ]
226
227 // Add pagination if needed
228 if (availableTools.size() >= 100) {
229 result.nextCursor = UUID.randomUUID().toString()
230 }
231 ]]></script>
232 </actions>
233 </service>
234
235 <service verb="handle" noun="ToolsCall" authenticate="false" transaction-timeout="300">
236 <description>Handle MCP tools/call request with direct Moqui service execution</description>
237 <in-parameters>
238 <parameter name="name" type="text-medium" required="true"/>
239 <parameter name="arguments" type="Map"/>
240 </in-parameters>
241 <out-parameters>
242 <parameter name="result" type="Map"/>
243 </out-parameters>
244 <actions>
245 <script><![CDATA[
246 import org.moqui.context.ExecutionContext
247 import groovy.json.JsonBuilder
248
249 ExecutionContext ec = context.ec
250
251 // Validate service exists
252 if (!ec.service.isServiceDefined(name)) {
253 throw new Exception("Tool not found: ${name}")
254 }
255
256 // Check permission
257 if (!ec.service.hasPermission(name)) {
258 throw new Exception("Permission denied for tool: ${name}")
259 }
260
261 // Create audit record
262 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
263 artifactHit.setSequencedIdPrimary()
264 artifactHit.visitId = ec.web?.visitId
265 artifactHit.userId = ec.user.userId
266 artifactHit.artifactType = "MCP"
267 artifactHit.artifactSubType = "Tool"
268 artifactHit.artifactName = name
269 artifactHit.parameterString = new JsonBuilder(arguments ?: [:]).toString()
270 artifactHit.startDateTime = ec.user.now
271 artifactHit.create()
272
273 def startTime = System.currentTimeMillis()
274 try {
275 // Execute service directly
276 def serviceResult = ec.service.sync(name, arguments ?: [:])
277 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
278
279 // Convert result to MCP format
280 def content = []
281 if (serviceResult) {
282 content << [
283 type: "text",
284 text: new JsonBuilder(serviceResult).toString()
285 ]
286 }
287
288 result = [
289 content: content,
290 isError: false
291 ]
292
293 // Update audit record
294 artifactHit.runningTimeMillis = executionTime
295 artifactHit.wasError = "N"
296 artifactHit.outputSize = new JsonBuilder(result).toString().length()
297 artifactHit.update()
298
299 } catch (Exception e) {
300 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
301
302 // Update audit record with error
303 artifactHit.runningTimeMillis = executionTime
304 artifactHit.wasError = "Y"
305 artifactHit.errorMessage = e.message
306 artifactHit.update()
307
308 result = [
309 content: [
310 [
311 type: "text",
312 text: "Error executing tool ${name}: ${e.message}"
313 ]
314 ],
315 isError: true
316 ]
317
318 ec.logger.error("MCP tool execution error", e)
319 }
320 ]]></script>
321 </actions>
322 </service>
323
324 <service verb="handle" noun="ResourcesList" authenticate="false" transaction-timeout="60">
325 <description>Handle MCP resources/list request with Moqui entity discovery</description>
326 <in-parameters>
327 <parameter name="cursor" type="text-medium"/>
328 </in-parameters>
329 <out-parameters>
330 <parameter name="result" type="Map"/>
331 </out-parameters>
332 <actions>
333 <script><![CDATA[
334 import org.moqui.context.ExecutionContext
335 import groovy.json.JsonBuilder
336
337 ExecutionContext ec = context.ec
338
339 // Get all entity names from Moqui entity engine
340 def allEntityNames = ec.entity.getEntityNames()
341 def availableResources = []
342
343 // Convert entities to MCP resources
344 for (entityName in allEntityNames) {
345 try {
346 // Check if user has permission
347 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
348 continue
349 }
350
351 def entityInfo = ec.entity.getEntityInfo(entityName)
352 if (!entityInfo) continue
353
354 // Convert entity to MCP resource format
355 def resource = [
356 uri: "entity://${entityName}",
357 name: entityName,
358 description: "Moqui entity: ${entityName}",
359 mimeType: "application/json"
360 ]
361
362 availableResources << resource
363
364 } catch (Exception e) {
365 ec.logger.warn("Error processing entity ${entityName}: ${e.message}")
366 }
367 }
368
369 result = [
370 resources: availableResources
371 ]
372 ]]></script>
373 </actions>
374 </service>
375
376 <service verb="handle" noun="ResourcesRead" authenticate="false" transaction-timeout="120">
377 <description>Handle MCP resources/read request with Moqui entity queries</description>
378 <in-parameters>
379 <parameter name="uri" type="text-medium" required="true"/>
380 </in-parameters>
381 <out-parameters>
382 <parameter name="result" type="Map"/>
383 </out-parameters>
384 <actions>
385 <script><![CDATA[
386 import org.moqui.context.ExecutionContext
387 import groovy.json.JsonBuilder
388
389 ExecutionContext ec = context.ec
390
391 // Parse entity URI (format: entity://EntityName)
392 if (!uri.startsWith("entity://")) {
393 throw new Exception("Invalid resource URI: ${uri}")
394 }
395
396 def entityName = uri.substring(9) // Remove "entity://" prefix
397
398 // Validate entity exists
399 if (!ec.entity.isEntityDefined(entityName)) {
400 throw new Exception("Entity not found: ${entityName}")
401 }
402
403 // Check permission
404 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
405 throw new Exception("Permission denied for entity: ${entityName}")
406 }
407
408 // Create audit record
409 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
410 artifactHit.setSequencedIdPrimary()
411 artifactHit.visitId = ec.web?.visitId
412 artifactHit.userId = ec.user.userId
413 artifactHit.artifactType = "MCP"
414 artifactHit.artifactSubType = "Resource"
415 artifactHit.artifactName = "resources/read"
416 artifactHit.parameterString = uri
417 artifactHit.startDateTime = ec.user.now
418 artifactHit.create()
419
420 def startTime = System.currentTimeMillis()
421 try {
422 // Query entity data (limited to prevent large responses)
423 def entityList = ec.entity.find(entityName)
424 .limit(100)
425 .list()
426
427 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
428
429 // Convert to MCP resource content
430 def contents = [
431 [
432 uri: uri,
433 mimeType: "application/json",
434 text: new JsonBuilder([
435 entityName: entityName,
436 recordCount: entityList.size(),
437 data: entityList
438 ]).toString()
439 ]
440 ]
441
442 result = [
443 contents: contents
444 ]
445
446 // Update audit record
447 artifactHit.runningTimeMillis = executionTime
448 artifactHit.wasError = "N"
449 artifactHit.outputSize = new JsonBuilder(result).toString().length()
450 artifactHit.update()
451
452 } catch (Exception e) {
453 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
454
455 // Update audit record with error
456 artifactHit.runningTimeMillis = executionTime
457 artifactHit.wasError = "Y"
458 artifactHit.errorMessage = e.message
459 artifactHit.update()
460
461 throw new Exception("Error reading resource ${uri}: ${e.message}")
462 }
463 ]]></script>
464 </actions>
465 </service>
466
467 <service verb="handle" noun="Ping" authenticate="false" transaction-timeout="10">
468 <description>Handle MCP ping request for health check</description>
469 <in-parameters/>
470 <out-parameters>
471 <parameter name="result" type="Map"/>
472 </out-parameters>
473 <actions>
474 <script><![CDATA[
475 result = [
476 timestamp: ec.user.now,
477 status: "healthy",
478 version: "2.0.0"
479 ]
480 ]]></script>
481 </actions>
482 </service>
483
484 <!-- Helper Functions -->
485
486 <service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false">
487 <description>Convert Moqui data types to JSON Schema types</description>
488 <in-parameters>
489 <parameter name="moquiType" type="text-medium" required="true"/>
490 </in-parameters>
491 <out-parameters>
492 <parameter name="jsonSchemaType" type="text-medium"/>
493 </out-parameters>
494 <actions>
495 <script><![CDATA[
496 // Simple type mapping - can be expanded as needed
497 def typeMap = [
498 "text-short": "string",
499 "text-medium": "string",
500 "text-long": "string",
501 "text-very-long": "string",
502 "id": "string",
503 "id-long": "string",
504 "number-integer": "integer",
505 "number-decimal": "number",
506 "number-float": "number",
507 "date": "string",
508 "date-time": "string",
509 "date-time-nano": "string",
510 "boolean": "boolean",
511 "text-indicator": "boolean"
512 ]
513
514 jsonSchemaType = typeMap[moquiType] ?: "string"
515 ]]></script>
516 </actions>
517 </service>
518
519 </services>
...\ 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 <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
15
16 <!-- MCP Services using Moqui's built-in JSON-RPC support -->
17
18 <service verb="mcp" noun="Initialize" authenticate="true" allow-remote="true" transaction-timeout="30">
19 <description>Handle MCP initialize request using Moqui authentication</description>
20 <in-parameters>
21 <parameter name="protocolVersion" type="text-medium" required="true"/>
22 <parameter name="capabilities" type="Map"/>
23 <parameter name="clientInfo" type="Map"/>
24 </in-parameters>
25 <out-parameters>
26 <parameter name="protocolVersion" type="text-medium"/>
27 <parameter name="capabilities" type="Map"/>
28 <parameter name="serverInfo" type="Map"/>
29 <parameter name="instructions" type="text-long"/>
30 </out-parameters>
31 <actions>
32 <script><![CDATA[
33 import org.moqui.context.ExecutionContext
34 import groovy.json.JsonBuilder
35
36 ExecutionContext ec = context.ec
37
38 // Validate protocol version
39 if (protocolVersion != "2025-06-18") {
40 throw new Exception("Unsupported protocol version: ${protocolVersion}")
41 }
42
43 // Build server capabilities
44 def serverCapabilities = [
45 tools: [:],
46 resources: [:],
47 logging: [:]
48 ]
49
50 // Build server info
51 def serverInfo = [
52 name: "Moqui MCP Server",
53 version: "2.0.0"
54 ]
55
56 protocolVersion = "2025-06-18"
57 capabilities = serverCapabilities
58 instructions = "This server provides access to Moqui ERP services and entities through MCP. Use mcp#ToolsList to discover available operations."
59 ]]></script>
60 </actions>
61 </service>
62
63 <service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60">
64 <description>Handle MCP tools/list request with direct Moqui service discovery</description>
65 <in-parameters>
66 <parameter name="cursor" type="text-medium"/>
67 </in-parameters>
68 <out-parameters>
69 <parameter name="tools" type="List"/>
70 <parameter name="nextCursor" type="text-medium"/>
71 </out-parameters>
72 <actions>
73 <script><![CDATA[
74 import org.moqui.context.ExecutionContext
75 import groovy.json.JsonBuilder
76 import java.util.UUID
77
78 ExecutionContext ec = context.ec
79
80 // Get all service names from Moqui service engine
81 def allServiceNames = ec.service.getServiceNames()
82 def availableTools = []
83
84 // Convert services to MCP tools
85 for (serviceName in allServiceNames) {
86 try {
87 // Check if user has permission
88 if (!ec.service.hasPermission(serviceName)) {
89 continue
90 }
91
92 def serviceInfo = ec.service.getServiceInfo(serviceName)
93 if (!serviceInfo) continue
94
95 // Convert service to MCP tool format
96 def tool = [
97 name: serviceName,
98 description: serviceInfo.description ?: "Moqui service: ${serviceName}",
99 inputSchema: [
100 type: "object",
101 properties: [:],
102 required: []
103 ]
104 ]
105 ]
106
107 // Convert service parameters to JSON Schema
108 def inParamNames = serviceInfo.getInParameterNames()
109 for (paramName in inParamNames) {
110 def paramInfo = serviceInfo.getInParameter(paramName)
111 tool.inputSchema.properties[paramName] = [
112 type: convertMoquiTypeToJsonSchemaType(paramInfo.type),
113 description: paramInfo.description ?: ""
114 ]
115
116 if (paramInfo.required) {
117 tool.inputSchema.required << paramName
118 }
119 }
120
121 availableTools << tool
122
123 } catch (Exception e) {
124 ec.logger.warn("Error processing service ${serviceName}: ${e.message}")
125 }
126 }
127
128 tools = availableTools
129
130 // Add pagination if needed
131 if (availableTools.size() >= 100) {
132 nextCursor = UUID.randomUUID().toString()
133 }
134 ]]></script>
135 </actions>
136 </service>
137
138 <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300">
139 <description>Handle MCP tools/call request with direct Moqui service execution</description>
140 <in-parameters>
141 <parameter name="name" type="text-medium" required="true"/>
142 <parameter name="arguments" type="Map"/>
143 </in-parameters>
144 <out-parameters>
145 <parameter name="content" type="List"/>
146 <parameter name="isError" type="text-indicator"/>
147 </out-parameters>
148 <actions>
149 <script><![CDATA[
150 import org.moqui.context.ExecutionContext
151 import groovy.json.JsonBuilder
152
153 ExecutionContext ec = context.ec
154
155 // Validate service exists
156 if (!ec.service.isServiceDefined(name)) {
157 throw new Exception("Tool not found: ${name}")
158 }
159
160 // Check permission
161 if (!ec.service.hasPermission(name)) {
162 throw new Exception("Permission denied for tool: ${name}")
163 }
164
165 // Create audit record
166 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
167 artifactHit.setSequencedIdPrimary()
168 artifactHit.visitId = ec.web?.visitId
169 artifactHit.userId = ec.user.userId
170 artifactHit.artifactType = "MCP"
171 artifactHit.artifactSubType = "Tool"
172 artifactHit.artifactName = name
173 artifactHit.parameterString = new JsonBuilder(arguments ?: [:]).toString()
174 artifactHit.startDateTime = ec.user.now
175 artifactHit.create()
176
177 def startTime = System.currentTimeMillis()
178 try {
179 // Execute service directly
180 def serviceResult = ec.service.sync(name, arguments ?: [:])
181 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
182
183 // Convert result to MCP format
184 content = []
185 if (serviceResult) {
186 content << [
187 type: "text",
188 text: new JsonBuilder(serviceResult).toString()
189 ]
190 }
191
192 isError = "N"
193
194 // Update audit record
195 artifactHit.runningTimeMillis = executionTime
196 artifactHit.wasError = "N"
197 artifactHit.outputSize = new JsonBuilder([content: content, isError: isError]).toString().length()
198 artifactHit.update()
199
200 } catch (Exception e) {
201 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
202
203 // Update audit record with error
204 artifactHit.runningTimeMillis = executionTime
205 artifactHit.wasError = "Y"
206 artifactHit.errorMessage = e.message
207 artifactHit.update()
208
209 content = [
210 [
211 type: "text",
212 text: "Error executing tool ${name}: ${e.message}"
213 ]
214 ]
215
216 isError = "Y"
217 ec.logger.error("MCP tool execution error", e)
218 }
219 ]]></script>
220 </actions>
221 </service>
222
223 <service verb="mcp" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60">
224 <description>Handle MCP resources/list request with Moqui entity discovery</description>
225 <in-parameters>
226 <parameter name="cursor" type="text-medium"/>
227 </in-parameters>
228 <out-parameters>
229 <parameter name="resources" type="List"/>
230 </out-parameters>
231 <actions>
232 <script><![CDATA[
233 import org.moqui.context.ExecutionContext
234 import groovy.json.JsonBuilder
235
236 ExecutionContext ec = context.ec
237
238 // Get all entity names from Moqui entity engine
239 def allEntityNames = ec.entity.getEntityNames()
240 def availableResources = []
241
242 // Convert entities to MCP resources
243 for (entityName in allEntityNames) {
244 try {
245 // Check if user has permission
246 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
247 continue
248 }
249
250 def entityInfo = ec.entity.getEntityInfo(entityName)
251 if (!entityInfo) continue
252
253 // Convert entity to MCP resource format
254 def resource = [
255 uri: "entity://${entityName}",
256 name: entityName,
257 description: "Moqui entity: ${entityName}",
258 mimeType: "application/json"
259 ]
260
261 availableResources << resource
262
263 } catch (Exception e) {
264 ec.logger.warn("Error processing entity ${entityName}: ${e.message}")
265 }
266 }
267
268 resources = availableResources
269 ]]></script>
270 </actions>
271 </service>
272
273 <service verb="mcp" noun="ResourcesRead" authenticate="true" allow-remote="true" transaction-timeout="120">
274 <description>Handle MCP resources/read request with Moqui entity queries</description>
275 <in-parameters>
276 <parameter name="uri" type="text-medium" required="true"/>
277 </in-parameters>
278 <out-parameters>
279 <parameter name="contents" type="List"/>
280 </out-parameters>
281 <actions>
282 <script><![CDATA[
283 import org.moqui.context.ExecutionContext
284 import groovy.json.JsonBuilder
285
286 ExecutionContext ec = context.ec
287
288 // Parse entity URI (format: entity://EntityName)
289 if (!uri.startsWith("entity://")) {
290 throw new Exception("Invalid resource URI: ${uri}")
291 }
292
293 def entityName = uri.substring(9) // Remove "entity://" prefix
294
295 // Validate entity exists
296 if (!ec.entity.isEntityDefined(entityName)) {
297 throw new Exception("Entity not found: ${entityName}")
298 }
299
300 // Check permission
301 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
302 throw new Exception("Permission denied for entity: ${entityName}")
303 }
304
305 // Create audit record
306 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
307 artifactHit.setSequencedIdPrimary()
308 artifactHit.visitId = ec.web?.visitId
309 artifactHit.userId = ec.user.userId
310 artifactHit.artifactType = "MCP"
311 artifactHit.artifactSubType = "Resource"
312 artifactHit.artifactName = "resources/read"
313 artifactHit.parameterString = uri
314 artifactHit.startDateTime = ec.user.now
315 artifactHit.create()
316
317 def startTime = System.currentTimeMillis()
318 try {
319 // Query entity data (limited to prevent large responses)
320 def entityList = ec.entity.find(entityName)
321 .limit(100)
322 .list()
323
324 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
325
326 // Convert to MCP resource content
327 contents = [
328 [
329 uri: uri,
330 mimeType: "application/json",
331 text: new JsonBuilder([
332 entityName: entityName,
333 recordCount: entityList.size(),
334 data: entityList
335 ]).toString()
336 ]
337 ]
338
339 // Update audit record
340 artifactHit.runningTimeMillis = executionTime
341 artifactHit.wasError = "N"
342 artifactHit.outputSize = new JsonBuilder(contents).toString().length()
343 artifactHit.update()
344
345 } catch (Exception e) {
346 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
347
348 // Update audit record with error
349 artifactHit.runningTimeMillis = executionTime
350 artifactHit.wasError = "Y"
351 artifactHit.errorMessage = e.message
352 artifactHit.update()
353
354 throw new Exception("Error reading resource ${uri}: ${e.message}")
355 }
356 ]]></script>
357 </actions>
358 </service>
359
360 <service verb="mcp" noun="Ping" authenticate="true" allow-remote="true" transaction-timeout="10">
361 <description>Handle MCP ping request for health check</description>
362 <in-parameters/>
363 <out-parameters>
364 <parameter name="timestamp" type="date-time"/>
365 <parameter name="status" type="text-short"/>
366 <parameter name="version" type="text-medium"/>
367 </out-parameters>
368 <actions>
369 <script><![CDATA[
370 timestamp = ec.user.now
371 status = "healthy"
372 version = "2.0.0"
373 ]]></script>
374 </actions>
375 </service>
376
377 <!-- Helper Functions -->
378
379 <service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false">
380 <description>Convert Moqui data types to JSON Schema types</description>
381 <in-parameters>
382 <parameter name="moquiType" type="text-medium" required="true"/>
383 </in-parameters>
384 <out-parameters>
385 <parameter name="jsonSchemaType" type="text-medium"/>
386 </out-parameters>
387 <actions>
388 <script><![CDATA[
389 // Simple type mapping - can be expanded as needed
390 def typeMap = [
391 "text-short": "string",
392 "text-medium": "string",
393 "text-long": "string",
394 "text-very-long": "string",
395 "id": "string",
396 "id-long": "string",
397 "number-integer": "integer",
398 "number-decimal": "number",
399 "number-float": "number",
400 "date": "string",
401 "date-time": "string",
402 "date-time-nano": "string",
403 "boolean": "boolean",
404 "text-indicator": "boolean"
405 ]
406
407 jsonSchemaType = typeMap[moquiType] ?: "string"
408 ]]></script>
409 </actions>
410 </service>
411
412 </services>
...\ No newline at end of file ...\ No newline at end of file