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.
Showing
6 changed files
with
1412 additions
and
0 deletions
AGENTS.md
0 → 100644
| 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 |
component.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <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 |
data/McpSecuritySeedData.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <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 |
entity/McpCoreEntities.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <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 |
service/McpJsonRpcServices.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <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 |
service/McpServices.xml
0 → 100644
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!-- This software is in the public domain under CC0 1.0 Universal plus a | ||
| 3 | Grant of Patent License. | ||
| 4 | |||
| 5 | To the extent possible under law, the author(s) have dedicated all | ||
| 6 | copyright and related and neighboring rights to this software to the | ||
| 7 | public domain worldwide. This software is distributed without any warranty. | ||
| 8 | |||
| 9 | You should have received a copy of the CC0 Public Domain Dedication | ||
| 10 | along with this software (see the LICENSE.md file). If not, see | ||
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | ||
| 12 | |||
| 13 | <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 |
-
Please register or sign in to post a comment