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
# Moqui MCP v2.0 Server Guide
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.
## Repository Status
**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.
- **Repository**: Independent git repository
- **Integration**: Included as plain directory in moqui-opencode
- **Version**: v2.0.0 (clean MCP implementation)
- **Approach**: Moqui-centric using built-in JSON-RPC support
## Architecture Overview
```
moqui-mcp-2/
├── AGENTS.md # This file - MCP server guide
├── component.xml # Component configuration
├── service/
│ └── McpServices.xml # MCP services with allow-remote="true"
└── data/
└── McpSecuritySeedData.xml # Security permissions
```
## MCP Implementation Strategy
### **Moqui-Centric Design**
-**Built-in JSON-RPC**: Uses Moqui's `/rpc/json` endpoint
-**Standard Services**: MCP methods as regular Moqui services
-**Native Authentication**: Leverages Moqui's user authentication
-**Direct Integration**: No custom layers or abstractions
-**Audit Logging**: Uses Moqui's ArtifactHit framework
### **MCP Method Mapping**
```
MCP Method → Moqui Service
initialize → org.moqui.mcp.McpServices.mcp#Initialize
tools/list → org.moqui.mcp.McpServices.mcp#ToolsList
tools/call → org.moqui.mcp.McpServices.mcp#ToolsCall
resources/list → org.moqui.mcp.McpServices.mcp#ResourcesList
resources/read → org.moqui.mcp.McpServices.mcp#ResourcesRead
ping → org.moqui.mcp.McpServices.mcp#Ping
```
## JSON-RPC Endpoint
### **Moqui Built-in Endpoint**
```
POST /rpc/json
- Moqui's native JSON-RPC 2.0 endpoint
- Handles all services with allow-remote="true"
- Built-in authentication and audit logging
```
### **JSON-RPC Request Format**
```json
{
"jsonrpc": "2.0",
"id": "unique_request_id",
"method": "org.moqui.mcp.McpServices.mcp#Initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": {},
"sampling": {}
},
"clientInfo": {
"name": "AI Client",
"version": "1.0.0"
}
}
}
```
## MCP Client Integration
### **Python Client Example**
```python
import requests
import json
import uuid
class MoquiMCPClient:
def __init__(self, base_url, username, password):
self.base_url = base_url
self.request_id = 0
self.authenticate(username, password)
def _make_request(self, method, params=None):
self.request_id += 1
payload = {
"jsonrpc": "2.0",
"id": f"req_{self.request_id}",
"method": method,
"params": params or {}
}
response = requests.post(f"{self.base_url}/rpc/json", json=payload)
return response.json()
def initialize(self):
return self._make_request("org.moqui.mcp.McpServices.mcp#Initialize", {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": {},
"sampling": {}
},
"clientInfo": {
"name": "Python MCP Client",
"version": "1.0.0"
}
})
def list_tools(self):
return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsList")
def call_tool(self, tool_name, arguments):
return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsCall", {
"name": tool_name,
"arguments": arguments
})
def list_resources(self):
return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesList")
def read_resource(self, uri):
return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesRead", {
"uri": uri
})
def ping(self):
return self._make_request("org.moqui.mcp.McpServices.mcp#Ping")
# Usage
client = MoquiMCPClient("http://localhost:8080", "admin", "moqui")
init_result = client.initialize()
tools = client.list_tools()
result = client.call_tool("org.moqui.example.Services.create#Example", {
"exampleName": "Test Example"
})
```
### **JavaScript Client Example**
```javascript
class MoquiMCPClient {
constructor(baseUrl, username, password) {
this.baseUrl = baseUrl;
this.requestId = 0;
this.authenticate(username, password);
}
async makeRequest(method, params = {}) {
this.requestId++;
const payload = {
jsonrpc: "2.0",
id: `req_${this.requestId}`,
method,
params
};
const response = await fetch(`${this.baseUrl}/rpc/json`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
return response.json();
}
async initialize() {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#Initialize', {
protocolVersion: '2025-06-18',
capabilities: {
roots: {},
sampling: {}
},
clientInfo: {
name: 'JavaScript MCP Client',
version: '1.0.0'
}
});
}
async listTools() {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsList');
}
async callTool(toolName, arguments) {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsCall', {
name: toolName,
arguments
});
}
async listResources() {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesList');
}
async readResource(uri) {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesRead', {
uri
});
}
async ping() {
return this.makeRequest('org.moqui.mcp.McpServices.mcp#Ping');
}
}
// Usage
const client = new MoquiMCPClient('http://localhost:8080', 'admin', 'moqui');
await client.initialize();
const tools = await client.listTools();
const result = await client.callTool('org.moqui.example.Services.create#Example', {
exampleName: 'Test Example'
});
```
### **cURL Examples**
#### **Initialize MCP Session**
```bash
curl -X POST -H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": "init_001",
"method": "org.moqui.mcp.McpServices.mcp#Initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {
"roots": {},
"sampling": {}
},
"clientInfo": {
"name": "cURL Client",
"version": "1.0.0"
}
}
}' \
http://localhost:8080/rpc/json
```
#### **List Available Tools**
```bash
curl -X POST -H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": "tools_001",
"method": "org.moqui.mcp.McpServices.mcp#ToolsList",
"params": {}
}' \
http://localhost:8080/rpc/json
```
#### **Execute Tool**
```bash
curl -X POST -H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": "call_001",
"method": "org.moqui.mcp.McpServices.mcp#ToolsCall",
"params": {
"name": "org.moqui.example.Services.create#Example",
"arguments": {
"exampleName": "JSON-RPC Test",
"statusId": "EXST_ACTIVE"
}
}
}' \
http://localhost:8080/rpc/json
```
#### **Read Entity Resource**
```bash
curl -X POST -H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": "resource_001",
"method": "org.moqui.mcp.McpServices.mcp#ResourcesRead",
"params": {
"uri": "entity://Example"
}
}' \
http://localhost:8080/rpc/json
```
#### **Health Check**
```bash
curl -X POST -H "Content-Type: application/json" \
--data '{
"jsonrpc": "2.0",
"id": "ping_001",
"method": "org.moqui.mcp.McpServices.mcp#Ping",
"params": {}
}' \
http://localhost:8080/rpc/json
```
## Security and Permissions
### **Authentication**
- Uses Moqui's built-in user authentication
- Pass credentials via standard JSON-RPC `authUsername` and `authPassword` parameters
- Or use existing session via Moqui's web session
### **Authorization**
- All MCP services require `authenticate="true"`
- Tool access controlled by Moqui's service permissions
- Entity access controlled by Moqui's entity permissions
- Audit logging via Moqui's ArtifactHit framework
### **Permission Setup**
```xml
<!-- MCP User Group -->
<moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/>
<!-- MCP Service Permissions -->
<moqui.security.ArtifactAuthz userGroupId="McpUser"
artifactGroupId="McpServices"
authzTypeEnumId="AUTHZT_ALLOW"
authzActionEnumId="AUTHZA_ALL"/>
```
## Development and Testing
### **Local Development**
1. Start Moqui with moqui-mcp-2 component
2. Test with JSON-RPC client examples above
3. Check Moqui logs for debugging
4. Monitor ArtifactHit records for audit trail
### **Service Discovery**
- All Moqui services automatically available as MCP tools
- Filtered by user permissions
- Service parameters converted to JSON Schema
- Service descriptions used for tool descriptions
### **Entity Access**
- All Moqui entities automatically available as MCP resources
- Format: `entity://EntityName`
- Filtered by entity VIEW permissions
- Limited to 100 records per request
## Error Handling
### **JSON-RPC Error Responses**
```json
{
"jsonrpc": "2.0",
"id": "req_001",
"error": {
"code": -32601,
"message": "Method not found"
}
}
```
### **Common Error Codes**
- `-32600`: Invalid Request (malformed JSON-RPC)
- `-32601`: Method not found
- `-32602`: Invalid params
- `-32603`: Internal error (service execution failed)
### **Service-Level Errors**
- Permission denied throws Exception with message
- Invalid service name throws Exception
- Entity access violations throw Exception
- All errors logged to Moqui error system
## Monitoring and Debugging
### **Audit Trail**
- All MCP calls logged to `moqui.server.ArtifactHit`
- Track user, service, parameters, execution time
- Monitor success/failure rates
- Debug via Moqui's built-in monitoring tools
### **Performance Metrics**
- Service execution time tracked
- Response size monitored
- Error rates captured
- User activity patterns available
---
## Key Advantages
### **Moqui-Centric Benefits**
1. **Simplicity**: No custom JSON-RPC handling needed
2. **Reliability**: Uses battle-tested Moqui framework
3. **Security**: Leverages Moqui's mature security system
4. **Audit**: Built-in logging and monitoring
5. **Compatibility**: Standard JSON-RPC 2.0 compliance
6. **Maintenance**: Minimal custom code to maintain
### **Clean Architecture**
- No custom webapps or screens needed
- No custom authentication layers
- No custom session management
- Direct service-to-tool mapping
- Standard Moqui patterns throughout
**This implementation demonstrates the power of Moqui's built-in JSON-RPC support for MCP integration.**
\ No newline at end of file
<?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/>. -->
<component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd">
<!-- No dependencies - uses only core framework -->
<entity-factory load-path="entity/" />
<service-factory load-path="service/" />
<!-- Load seed data -->
<entity-factory load-data="data/McpSecuritySeedData.xml" />
</component>
\ No newline at end of file
<?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/>. -->
<entity-facade-xml xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-facade-3.xsd">
<!-- MCP User Group -->
<moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/>
<!-- MCP Artifact Groups -->
<moqui.security.ArtifactGroup artifactGroupId="McpRestPaths" description="MCP REST Paths"/>
<moqui.security.ArtifactGroup artifactGroupId="McpServices" description="MCP Services"/>
<!-- MCP Artifact Group Members -->
<moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/rest/s1/mcp-2" artifactTypeEnumId="AT_REST_PATH"/>
<moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="org.moqui.mcp.*" artifactTypeEnumId="AT_SERVICE"/>
<!-- MCP Artifact Authz -->
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpRestPaths" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/>
<!-- Add admin user to MCP user group for testing -->
<moqui.security.UserGroupMember userGroupId="McpUser" userId="ADMIN" fromDate="2025-01-01 00:00:00.000"/>
</entity-facade-xml>
\ No newline at end of file
<?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/>. -->
<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/entity-definition-3.xsd">
<!-- No custom entities needed - use Moqui's built-in entities directly -->
<!-- MCP uses standard Moqui authentication, audit logging, and permissions -->
</entities>
\ No newline at end of file
<?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/>. -->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
<!-- MCP JSON-RPC 2.0 Handler -->
<service verb="handle" noun="JsonRpcRequest" authenticate="false" transaction-timeout="300">
<description>Handle MCP JSON-RPC 2.0 requests with direct Moqui integration</description>
<in-parameters>
<parameter name="jsonrpc" type="text-short" required="true"/>
<parameter name="id" type="text-medium"/>
<parameter name="method" type="text-medium" required="true"/>
<parameter name="params" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="response" type="text-very-long"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
import groovy.json.JsonSlurper
import java.util.UUID
ExecutionContext ec = context.ec
// Validate JSON-RPC version
if (jsonrpc != "2.0") {
response = new JsonBuilder([
jsonrpc: "2.0",
error: [
code: -32600,
message: "Invalid Request: Only JSON-RPC 2.0 supported"
],
id: id
]).toString()
return
}
def result = null
def error = null
try {
// Route to appropriate MCP method handler
switch (method) {
case "initialize":
result = handleInitialize(params, ec)
break
case "tools/list":
result = handleToolsList(params, ec)
break
case "tools/call":
result = handleToolsCall(params, ec)
break
case "resources/list":
result = handleResourcesList(params, ec)
break
case "resources/read":
result = handleResourcesRead(params, ec)
break
case "ping":
result = handlePing(params, ec)
break
default:
error = [
code: -32601,
message: "Method not found: ${method}"
]
}
} 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
}
response = new JsonBuilder(responseObj).toString()
// Log request for audit
ec.message.addMessage("MCP ${method} request processed", "info")
]]></script>
</actions>
</service>
<!-- MCP Method Implementations -->
<service verb="handle" noun="Initialize" authenticate="false" transaction-timeout="30">
<description>Handle MCP initialize request with Moqui authentication</description>
<in-parameters>
<parameter name="protocolVersion" type="text-medium" required="true"/>
<parameter name="capabilities" type="Map"/>
<parameter name="clientInfo" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Validate protocol version
if (protocolVersion != "2025-06-18") {
throw new Exception("Unsupported protocol version: ${protocolVersion}")
}
// Get current user context (if authenticated)
def userId = ec.user.userId
def userAccountId = userId ? userId : null
// Build server capabilities
def serverCapabilities = [
tools: [:],
resources: [:],
logging: [:]
]
// Build server info
def serverInfo = [
name: "Moqui MCP Server",
version: "2.0.0"
]
result = [
protocolVersion: "2025-06-18",
capabilities: serverCapabilities,
serverInfo: serverInfo,
instructions: "This server provides access to Moqui ERP services and entities through MCP. Use tools/list to discover available operations."
]
]]></script>
</actions>
</service>
<service verb="handle" noun="ToolsList" authenticate="false" transaction-timeout="60">
<description>Handle MCP tools/list request with direct Moqui service discovery</description>
<in-parameters>
<parameter name="cursor" type="text-medium"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Get all service names from Moqui service engine
def allServiceNames = ec.service.getServiceNames()
def availableTools = []
// Convert services to MCP tools
for (serviceName in allServiceNames) {
try {
// Check if user has permission
if (!ec.service.hasPermission(serviceName)) {
continue
}
def serviceInfo = ec.service.getServiceInfo(serviceName)
if (!serviceInfo) continue
// Convert service to MCP tool format
def tool = [
name: serviceName,
description: serviceInfo.description ?: "Moqui service: ${serviceName}",
inputSchema: [
type: "object",
properties: [:],
required: []
]
]
]
// Convert service parameters to JSON Schema
def inParamNames = serviceInfo.getInParameterNames()
for (paramName in inParamNames) {
def paramInfo = serviceInfo.getInParameter(paramName)
tool.inputSchema.properties[paramName] = [
type: convertMoquiTypeToJsonSchemaType(paramInfo.type),
description: paramInfo.description ?: ""
]
if (paramInfo.required) {
tool.inputSchema.required << paramName
}
}
availableTools << tool
} catch (Exception e) {
ec.logger.warn("Error processing service ${serviceName}: ${e.message}")
}
}
result = [
tools: availableTools
]
// Add pagination if needed
if (availableTools.size() >= 100) {
result.nextCursor = UUID.randomUUID().toString()
}
]]></script>
</actions>
</service>
<service verb="handle" noun="ToolsCall" authenticate="false" transaction-timeout="300">
<description>Handle MCP tools/call request with direct Moqui service execution</description>
<in-parameters>
<parameter name="name" type="text-medium" required="true"/>
<parameter name="arguments" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Validate service exists
if (!ec.service.isServiceDefined(name)) {
throw new Exception("Tool not found: ${name}")
}
// Check permission
if (!ec.service.hasPermission(name)) {
throw new Exception("Permission denied for tool: ${name}")
}
// Create audit record
def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
artifactHit.setSequencedIdPrimary()
artifactHit.visitId = ec.web?.visitId
artifactHit.userId = ec.user.userId
artifactHit.artifactType = "MCP"
artifactHit.artifactSubType = "Tool"
artifactHit.artifactName = name
artifactHit.parameterString = new JsonBuilder(arguments ?: [:]).toString()
artifactHit.startDateTime = ec.user.now
artifactHit.create()
def startTime = System.currentTimeMillis()
try {
// Execute service directly
def serviceResult = ec.service.sync(name, arguments ?: [:])
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Convert result to MCP format
def content = []
if (serviceResult) {
content << [
type: "text",
text: new JsonBuilder(serviceResult).toString()
]
}
result = [
content: content,
isError: false
]
// Update audit record
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "N"
artifactHit.outputSize = new JsonBuilder(result).toString().length()
artifactHit.update()
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Update audit record with error
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "Y"
artifactHit.errorMessage = e.message
artifactHit.update()
result = [
content: [
[
type: "text",
text: "Error executing tool ${name}: ${e.message}"
]
],
isError: true
]
ec.logger.error("MCP tool execution error", e)
}
]]></script>
</actions>
</service>
<service verb="handle" noun="ResourcesList" authenticate="false" transaction-timeout="60">
<description>Handle MCP resources/list request with Moqui entity discovery</description>
<in-parameters>
<parameter name="cursor" type="text-medium"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Get all entity names from Moqui entity engine
def allEntityNames = ec.entity.getEntityNames()
def availableResources = []
// Convert entities to MCP resources
for (entityName in allEntityNames) {
try {
// Check if user has permission
if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
continue
}
def entityInfo = ec.entity.getEntityInfo(entityName)
if (!entityInfo) continue
// Convert entity to MCP resource format
def resource = [
uri: "entity://${entityName}",
name: entityName,
description: "Moqui entity: ${entityName}",
mimeType: "application/json"
]
availableResources << resource
} catch (Exception e) {
ec.logger.warn("Error processing entity ${entityName}: ${e.message}")
}
}
result = [
resources: availableResources
]
]]></script>
</actions>
</service>
<service verb="handle" noun="ResourcesRead" authenticate="false" transaction-timeout="120">
<description>Handle MCP resources/read request with Moqui entity queries</description>
<in-parameters>
<parameter name="uri" type="text-medium" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Parse entity URI (format: entity://EntityName)
if (!uri.startsWith("entity://")) {
throw new Exception("Invalid resource URI: ${uri}")
}
def entityName = uri.substring(9) // Remove "entity://" prefix
// Validate entity exists
if (!ec.entity.isEntityDefined(entityName)) {
throw new Exception("Entity not found: ${entityName}")
}
// Check permission
if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
throw new Exception("Permission denied for entity: ${entityName}")
}
// Create audit record
def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
artifactHit.setSequencedIdPrimary()
artifactHit.visitId = ec.web?.visitId
artifactHit.userId = ec.user.userId
artifactHit.artifactType = "MCP"
artifactHit.artifactSubType = "Resource"
artifactHit.artifactName = "resources/read"
artifactHit.parameterString = uri
artifactHit.startDateTime = ec.user.now
artifactHit.create()
def startTime = System.currentTimeMillis()
try {
// Query entity data (limited to prevent large responses)
def entityList = ec.entity.find(entityName)
.limit(100)
.list()
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Convert to MCP resource content
def contents = [
[
uri: uri,
mimeType: "application/json",
text: new JsonBuilder([
entityName: entityName,
recordCount: entityList.size(),
data: entityList
]).toString()
]
]
result = [
contents: contents
]
// Update audit record
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "N"
artifactHit.outputSize = new JsonBuilder(result).toString().length()
artifactHit.update()
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Update audit record with error
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "Y"
artifactHit.errorMessage = e.message
artifactHit.update()
throw new Exception("Error reading resource ${uri}: ${e.message}")
}
]]></script>
</actions>
</service>
<service verb="handle" noun="Ping" authenticate="false" transaction-timeout="10">
<description>Handle MCP ping request for health check</description>
<in-parameters/>
<out-parameters>
<parameter name="result" type="Map"/>
</out-parameters>
<actions>
<script><![CDATA[
result = [
timestamp: ec.user.now,
status: "healthy",
version: "2.0.0"
]
]]></script>
</actions>
</service>
<!-- Helper Functions -->
<service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false">
<description>Convert Moqui data types to JSON Schema types</description>
<in-parameters>
<parameter name="moquiType" type="text-medium" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="jsonSchemaType" type="text-medium"/>
</out-parameters>
<actions>
<script><![CDATA[
// Simple type mapping - can be expanded as needed
def typeMap = [
"text-short": "string",
"text-medium": "string",
"text-long": "string",
"text-very-long": "string",
"id": "string",
"id-long": "string",
"number-integer": "integer",
"number-decimal": "number",
"number-float": "number",
"date": "string",
"date-time": "string",
"date-time-nano": "string",
"boolean": "boolean",
"text-indicator": "boolean"
]
jsonSchemaType = typeMap[moquiType] ?: "string"
]]></script>
</actions>
</service>
</services>
\ No newline at end of file
<?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/>. -->
<services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
<!-- MCP Services using Moqui's built-in JSON-RPC support -->
<service verb="mcp" noun="Initialize" authenticate="true" allow-remote="true" transaction-timeout="30">
<description>Handle MCP initialize request using Moqui authentication</description>
<in-parameters>
<parameter name="protocolVersion" type="text-medium" required="true"/>
<parameter name="capabilities" type="Map"/>
<parameter name="clientInfo" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="protocolVersion" type="text-medium"/>
<parameter name="capabilities" type="Map"/>
<parameter name="serverInfo" type="Map"/>
<parameter name="instructions" type="text-long"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Validate protocol version
if (protocolVersion != "2025-06-18") {
throw new Exception("Unsupported protocol version: ${protocolVersion}")
}
// Build server capabilities
def serverCapabilities = [
tools: [:],
resources: [:],
logging: [:]
]
// Build server info
def serverInfo = [
name: "Moqui MCP Server",
version: "2.0.0"
]
protocolVersion = "2025-06-18"
capabilities = serverCapabilities
instructions = "This server provides access to Moqui ERP services and entities through MCP. Use mcp#ToolsList to discover available operations."
]]></script>
</actions>
</service>
<service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60">
<description>Handle MCP tools/list request with direct Moqui service discovery</description>
<in-parameters>
<parameter name="cursor" type="text-medium"/>
</in-parameters>
<out-parameters>
<parameter name="tools" type="List"/>
<parameter name="nextCursor" type="text-medium"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
import java.util.UUID
ExecutionContext ec = context.ec
// Get all service names from Moqui service engine
def allServiceNames = ec.service.getServiceNames()
def availableTools = []
// Convert services to MCP tools
for (serviceName in allServiceNames) {
try {
// Check if user has permission
if (!ec.service.hasPermission(serviceName)) {
continue
}
def serviceInfo = ec.service.getServiceInfo(serviceName)
if (!serviceInfo) continue
// Convert service to MCP tool format
def tool = [
name: serviceName,
description: serviceInfo.description ?: "Moqui service: ${serviceName}",
inputSchema: [
type: "object",
properties: [:],
required: []
]
]
]
// Convert service parameters to JSON Schema
def inParamNames = serviceInfo.getInParameterNames()
for (paramName in inParamNames) {
def paramInfo = serviceInfo.getInParameter(paramName)
tool.inputSchema.properties[paramName] = [
type: convertMoquiTypeToJsonSchemaType(paramInfo.type),
description: paramInfo.description ?: ""
]
if (paramInfo.required) {
tool.inputSchema.required << paramName
}
}
availableTools << tool
} catch (Exception e) {
ec.logger.warn("Error processing service ${serviceName}: ${e.message}")
}
}
tools = availableTools
// Add pagination if needed
if (availableTools.size() >= 100) {
nextCursor = UUID.randomUUID().toString()
}
]]></script>
</actions>
</service>
<service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300">
<description>Handle MCP tools/call request with direct Moqui service execution</description>
<in-parameters>
<parameter name="name" type="text-medium" required="true"/>
<parameter name="arguments" type="Map"/>
</in-parameters>
<out-parameters>
<parameter name="content" type="List"/>
<parameter name="isError" type="text-indicator"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Validate service exists
if (!ec.service.isServiceDefined(name)) {
throw new Exception("Tool not found: ${name}")
}
// Check permission
if (!ec.service.hasPermission(name)) {
throw new Exception("Permission denied for tool: ${name}")
}
// Create audit record
def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
artifactHit.setSequencedIdPrimary()
artifactHit.visitId = ec.web?.visitId
artifactHit.userId = ec.user.userId
artifactHit.artifactType = "MCP"
artifactHit.artifactSubType = "Tool"
artifactHit.artifactName = name
artifactHit.parameterString = new JsonBuilder(arguments ?: [:]).toString()
artifactHit.startDateTime = ec.user.now
artifactHit.create()
def startTime = System.currentTimeMillis()
try {
// Execute service directly
def serviceResult = ec.service.sync(name, arguments ?: [:])
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Convert result to MCP format
content = []
if (serviceResult) {
content << [
type: "text",
text: new JsonBuilder(serviceResult).toString()
]
}
isError = "N"
// Update audit record
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "N"
artifactHit.outputSize = new JsonBuilder([content: content, isError: isError]).toString().length()
artifactHit.update()
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Update audit record with error
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "Y"
artifactHit.errorMessage = e.message
artifactHit.update()
content = [
[
type: "text",
text: "Error executing tool ${name}: ${e.message}"
]
]
isError = "Y"
ec.logger.error("MCP tool execution error", e)
}
]]></script>
</actions>
</service>
<service verb="mcp" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60">
<description>Handle MCP resources/list request with Moqui entity discovery</description>
<in-parameters>
<parameter name="cursor" type="text-medium"/>
</in-parameters>
<out-parameters>
<parameter name="resources" type="List"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Get all entity names from Moqui entity engine
def allEntityNames = ec.entity.getEntityNames()
def availableResources = []
// Convert entities to MCP resources
for (entityName in allEntityNames) {
try {
// Check if user has permission
if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
continue
}
def entityInfo = ec.entity.getEntityInfo(entityName)
if (!entityInfo) continue
// Convert entity to MCP resource format
def resource = [
uri: "entity://${entityName}",
name: entityName,
description: "Moqui entity: ${entityName}",
mimeType: "application/json"
]
availableResources << resource
} catch (Exception e) {
ec.logger.warn("Error processing entity ${entityName}: ${e.message}")
}
}
resources = availableResources
]]></script>
</actions>
</service>
<service verb="mcp" noun="ResourcesRead" authenticate="true" allow-remote="true" transaction-timeout="120">
<description>Handle MCP resources/read request with Moqui entity queries</description>
<in-parameters>
<parameter name="uri" type="text-medium" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="contents" type="List"/>
</out-parameters>
<actions>
<script><![CDATA[
import org.moqui.context.ExecutionContext
import groovy.json.JsonBuilder
ExecutionContext ec = context.ec
// Parse entity URI (format: entity://EntityName)
if (!uri.startsWith("entity://")) {
throw new Exception("Invalid resource URI: ${uri}")
}
def entityName = uri.substring(9) // Remove "entity://" prefix
// Validate entity exists
if (!ec.entity.isEntityDefined(entityName)) {
throw new Exception("Entity not found: ${entityName}")
}
// Check permission
if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
throw new Exception("Permission denied for entity: ${entityName}")
}
// Create audit record
def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
artifactHit.setSequencedIdPrimary()
artifactHit.visitId = ec.web?.visitId
artifactHit.userId = ec.user.userId
artifactHit.artifactType = "MCP"
artifactHit.artifactSubType = "Resource"
artifactHit.artifactName = "resources/read"
artifactHit.parameterString = uri
artifactHit.startDateTime = ec.user.now
artifactHit.create()
def startTime = System.currentTimeMillis()
try {
// Query entity data (limited to prevent large responses)
def entityList = ec.entity.find(entityName)
.limit(100)
.list()
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Convert to MCP resource content
contents = [
[
uri: uri,
mimeType: "application/json",
text: new JsonBuilder([
entityName: entityName,
recordCount: entityList.size(),
data: entityList
]).toString()
]
]
// Update audit record
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "N"
artifactHit.outputSize = new JsonBuilder(contents).toString().length()
artifactHit.update()
} catch (Exception e) {
def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
// Update audit record with error
artifactHit.runningTimeMillis = executionTime
artifactHit.wasError = "Y"
artifactHit.errorMessage = e.message
artifactHit.update()
throw new Exception("Error reading resource ${uri}: ${e.message}")
}
]]></script>
</actions>
</service>
<service verb="mcp" noun="Ping" authenticate="true" allow-remote="true" transaction-timeout="10">
<description>Handle MCP ping request for health check</description>
<in-parameters/>
<out-parameters>
<parameter name="timestamp" type="date-time"/>
<parameter name="status" type="text-short"/>
<parameter name="version" type="text-medium"/>
</out-parameters>
<actions>
<script><![CDATA[
timestamp = ec.user.now
status = "healthy"
version = "2.0.0"
]]></script>
</actions>
</service>
<!-- Helper Functions -->
<service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false">
<description>Convert Moqui data types to JSON Schema types</description>
<in-parameters>
<parameter name="moquiType" type="text-medium" required="true"/>
</in-parameters>
<out-parameters>
<parameter name="jsonSchemaType" type="text-medium"/>
</out-parameters>
<actions>
<script><![CDATA[
// Simple type mapping - can be expanded as needed
def typeMap = [
"text-short": "string",
"text-medium": "string",
"text-long": "string",
"text-very-long": "string",
"id": "string",
"id-long": "string",
"number-integer": "integer",
"number-decimal": "number",
"number-float": "number",
"date": "string",
"date-time": "string",
"date-time-nano": "string",
"boolean": "boolean",
"text-indicator": "boolean"
]
jsonSchemaType = typeMap[moquiType] ?: "string"
]]></script>
</actions>
</service>
</services>
\ No newline at end of file