WIP Servlet 4 MCP
Showing
11 changed files
with
196 additions
and
2200 deletions
AGENTS.md
deleted
100644 → 0
| 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 unified screen-based implementation. | ||
| 4 | |||
| 5 | ## Repository Status | ||
| 6 | |||
| 7 | **moqui-mcp-2** is a **standalone component with its own git repository**. It uses a unified screen-based approach that handles both JSON-RPC and Server-Sent Events (SSE) for maximum MCP client compatibility. | ||
| 8 | |||
| 9 | - **Repository**: Independent git repository | ||
| 10 | - **Integration**: Included as plain directory in moqui-opencode | ||
| 11 | - **Version**: v2.0.0 (unified screen implementation) | ||
| 12 | - **Approach**: Moqui screen-based with dual JSON-RPC/SSE support | ||
| 13 | - **Status**: ✅ **PRODUCTION READY** - All tests passing | ||
| 14 | |||
| 15 | ## Architecture Overview | ||
| 16 | |||
| 17 | ``` | ||
| 18 | moqui-mcp-2/ | ||
| 19 | ├── AGENTS.md # This file - MCP server guide | ||
| 20 | ├── component.xml # Component configuration | ||
| 21 | ├── screen/ | ||
| 22 | │ └── webroot/ | ||
| 23 | │ └── mcp.xml # Unified MCP screen (JSON-RPC + SSE) | ||
| 24 | ├── service/ | ||
| 25 | │ ├── McpServices.xml # MCP services implementation | ||
| 26 | │ └── mcp.rest.xml # Basic REST endpoints | ||
| 27 | └── data/ | ||
| 28 | └── McpSecuritySeedData.xml # Security permissions | ||
| 29 | ``` | ||
| 30 | |||
| 31 | ## Implementation Status | ||
| 32 | |||
| 33 | ### **✅ COMPLETED FEATURES** | ||
| 34 | - **Unified Screen Architecture**: Single endpoint handles both JSON-RPC and SSE | ||
| 35 | - **Content-Type Negotiation**: Correctly prioritizes `application/json` over `text/event-stream` | ||
| 36 | - **Session Management**: MCP session IDs generated and validated | ||
| 37 | - **Security Integration**: Full Moqui security framework integration | ||
| 38 | - **Method Mapping**: All MCP methods properly mapped to Moqui services | ||
| 39 | - **Error Handling**: Comprehensive error responses for both formats | ||
| 40 | - **Protocol Headers**: MCP protocol version headers set correctly | ||
| 41 | |||
| 42 | ### **✅ ENDPOINT CONFIGURATION** | ||
| 43 | - **Primary Endpoint**: `http://localhost:8080/mcp/rpc` | ||
| 44 | - **JSON-RPC Support**: `Accept: application/json` → JSON responses | ||
| 45 | - **SSE Support**: `Accept: text/event-stream` → Server-Sent Events | ||
| 46 | - **Authentication**: Basic auth with `mcp-user:moqui` credentials | ||
| 47 | - **Session Headers**: `Mcp-Session-Id` for session persistence | ||
| 48 | |||
| 49 | ## MCP Implementation Strategy | ||
| 50 | |||
| 51 | ### **Unified Screen Design** | ||
| 52 | - ✅ **Single Endpoint**: `/mcp/rpc` handles both JSON-RPC and SSE | ||
| 53 | - ✅ **Content-Type Negotiation**: Automatic response format selection | ||
| 54 | - ✅ **Standard Services**: MCP methods as regular Moqui services | ||
| 55 | - ✅ **Native Authentication**: Leverages Moqui's user authentication | ||
| 56 | - ✅ **Direct Integration**: No custom layers or abstractions | ||
| 57 | - ✅ **Audit Logging**: Uses Moqui's ArtifactHit framework | ||
| 58 | |||
| 59 | ### **MCP Method Mapping** | ||
| 60 | ``` | ||
| 61 | MCP Method → Moqui Service | ||
| 62 | initialize → McpServices.mcp#Initialize | ||
| 63 | tools/list → McpServices.mcp#ToolsList | ||
| 64 | tools/call → McpServices.mcp#ToolsCall | ||
| 65 | resources/list → McpServices.mcp#ResourcesList | ||
| 66 | resources/read → McpServices.mcp#ResourcesRead | ||
| 67 | ping → McpServices.mcp#Ping | ||
| 68 | ``` | ||
| 69 | |||
| 70 | ### **Response Format Handling** | ||
| 71 | ``` | ||
| 72 | Accept Header → Response Format | ||
| 73 | application/json → JSON-RPC 2.0 response | ||
| 74 | text/event-stream → Server-Sent Events | ||
| 75 | application/json, text/event-stream → JSON-RPC 2.0 (prioritized) | ||
| 76 | ``` | ||
| 77 | |||
| 78 | ## MCP Endpoint | ||
| 79 | |||
| 80 | ### **Unified Screen Endpoint** | ||
| 81 | ``` | ||
| 82 | POST /mcp/rpc | ||
| 83 | - Single screen handles both JSON-RPC and SSE | ||
| 84 | - Content-Type negotiation determines response format | ||
| 85 | - Built-in authentication and audit logging | ||
| 86 | - MCP protocol version headers included | ||
| 87 | ``` | ||
| 88 | |||
| 89 | ### **Request Flow** | ||
| 90 | 1. **Authentication**: Basic auth validates user credentials | ||
| 91 | 2. **Session Management**: Initialize generates session ID, subsequent requests require it | ||
| 92 | 3. **Content-Type Negotiation**: Accept header determines response format | ||
| 93 | 4. **Method Routing**: MCP methods mapped to Moqui services | ||
| 94 | 5. **Response Generation**: JSON-RPC or SSE based on client preference | ||
| 95 | |||
| 96 | ### **JSON-RPC Request Format** | ||
| 97 | ```json | ||
| 98 | { | ||
| 99 | "jsonrpc": "2.0", | ||
| 100 | "id": "unique_request_id", | ||
| 101 | "method": "initialize", | ||
| 102 | "params": { | ||
| 103 | "protocolVersion": "2025-06-18", | ||
| 104 | "capabilities": { | ||
| 105 | "roots": {}, | ||
| 106 | "sampling": {} | ||
| 107 | }, | ||
| 108 | "clientInfo": { | ||
| 109 | "name": "AI Client", | ||
| 110 | "version": "1.0.0" | ||
| 111 | } | ||
| 112 | } | ||
| 113 | } | ||
| 114 | ``` | ||
| 115 | |||
| 116 | ### **SSE Request Format** | ||
| 117 | ```bash | ||
| 118 | curl -X POST http://localhost:8080/mcp/rpc \ | ||
| 119 | -H "Content-Type: application/json" \ | ||
| 120 | -H "Accept: text/event-stream" \ | ||
| 121 | -H "Authorization: Basic bWNwLXVzZXI6bW9xdWk=" \ | ||
| 122 | -H "Mcp-Session-Id: <session-id>" \ | ||
| 123 | -d '{"jsonrpc":"2.0","id":"ping-1","method":"ping","params":{}}' | ||
| 124 | ``` | ||
| 125 | |||
| 126 | ### **SSE Response Format** | ||
| 127 | ``` | ||
| 128 | event: response | ||
| 129 | data: {"jsonrpc":"2.0","id":"ping-1","result":{"result":{"timestamp":"2025-11-14T23:16:14+0000","status":"healthy","version":"2.0.0"}}} | ||
| 130 | ``` | ||
| 131 | |||
| 132 | ## MCP Client Integration | ||
| 133 | |||
| 134 | ### **Python Client Example** | ||
| 135 | ```python | ||
| 136 | import requests | ||
| 137 | import json | ||
| 138 | import uuid | ||
| 139 | |||
| 140 | class MoquiMCPClient: | ||
| 141 | def __init__(self, base_url, username, password): | ||
| 142 | self.base_url = base_url | ||
| 143 | self.request_id = 0 | ||
| 144 | self.authenticate(username, password) | ||
| 145 | |||
| 146 | def _make_request(self, method, params=None): | ||
| 147 | self.request_id += 1 | ||
| 148 | payload = { | ||
| 149 | "jsonrpc": "2.0", | ||
| 150 | "id": f"req_{self.request_id}", | ||
| 151 | "method": method, | ||
| 152 | "params": params or {} | ||
| 153 | } | ||
| 154 | |||
| 155 | response = requests.post(f"{self.base_url}/mcp/rpc", json=payload) | ||
| 156 | return response.json() | ||
| 157 | |||
| 158 | def initialize(self): | ||
| 159 | return self._make_request("org.moqui.mcp.McpServices.mcp#Initialize", { | ||
| 160 | "protocolVersion": "2025-06-18", | ||
| 161 | "capabilities": { | ||
| 162 | "roots": {}, | ||
| 163 | "sampling": {} | ||
| 164 | }, | ||
| 165 | "clientInfo": { | ||
| 166 | "name": "Python MCP Client", | ||
| 167 | "version": "1.0.0" | ||
| 168 | } | ||
| 169 | }) | ||
| 170 | |||
| 171 | def list_tools(self): | ||
| 172 | return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsList") | ||
| 173 | |||
| 174 | def call_tool(self, tool_name, arguments): | ||
| 175 | return self._make_request("org.moqui.mcp.McpServices.mcp#ToolsCall", { | ||
| 176 | "name": tool_name, | ||
| 177 | "arguments": arguments | ||
| 178 | }) | ||
| 179 | |||
| 180 | def list_resources(self): | ||
| 181 | return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesList") | ||
| 182 | |||
| 183 | def read_resource(self, uri): | ||
| 184 | return self._make_request("org.moqui.mcp.McpServices.mcp#ResourcesRead", { | ||
| 185 | "uri": uri | ||
| 186 | }) | ||
| 187 | |||
| 188 | def ping(self): | ||
| 189 | return self._make_request("org.moqui.mcp.McpServices.mcp#Ping") | ||
| 190 | |||
| 191 | # Usage | ||
| 192 | client = MoquiMCPClient("http://localhost:8080", "admin", "moqui") | ||
| 193 | init_result = client.initialize() | ||
| 194 | tools = client.list_tools() | ||
| 195 | result = client.call_tool("org.moqui.example.Services.create#Example", { | ||
| 196 | "exampleName": "Test Example" | ||
| 197 | }) | ||
| 198 | ``` | ||
| 199 | |||
| 200 | ### **JavaScript Client Example** | ||
| 201 | ```javascript | ||
| 202 | class MoquiMCPClient { | ||
| 203 | constructor(baseUrl, username, password) { | ||
| 204 | this.baseUrl = baseUrl; | ||
| 205 | this.requestId = 0; | ||
| 206 | this.authenticate(username, password); | ||
| 207 | } | ||
| 208 | |||
| 209 | async makeRequest(method, params = {}) { | ||
| 210 | this.requestId++; | ||
| 211 | const payload = { | ||
| 212 | jsonrpc: "2.0", | ||
| 213 | id: `req_${this.requestId}`, | ||
| 214 | method, | ||
| 215 | params | ||
| 216 | }; | ||
| 217 | |||
| 218 | const response = await fetch(`${this.baseUrl}/mcp/rpc`, { | ||
| 219 | method: 'POST', | ||
| 220 | headers: { 'Content-Type': 'application/json' }, | ||
| 221 | body: JSON.stringify(payload) | ||
| 222 | }); | ||
| 223 | |||
| 224 | return response.json(); | ||
| 225 | } | ||
| 226 | |||
| 227 | async initialize() { | ||
| 228 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#Initialize', { | ||
| 229 | protocolVersion: '2025-06-18', | ||
| 230 | capabilities: { | ||
| 231 | roots: {}, | ||
| 232 | sampling: {} | ||
| 233 | }, | ||
| 234 | clientInfo: { | ||
| 235 | name: 'JavaScript MCP Client', | ||
| 236 | version: '1.0.0' | ||
| 237 | } | ||
| 238 | }); | ||
| 239 | } | ||
| 240 | |||
| 241 | async listTools() { | ||
| 242 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsList'); | ||
| 243 | } | ||
| 244 | |||
| 245 | async callTool(toolName, arguments) { | ||
| 246 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#ToolsCall', { | ||
| 247 | name: toolName, | ||
| 248 | arguments | ||
| 249 | }); | ||
| 250 | } | ||
| 251 | |||
| 252 | async listResources() { | ||
| 253 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesList'); | ||
| 254 | } | ||
| 255 | |||
| 256 | async readResource(uri) { | ||
| 257 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#ResourcesRead', { | ||
| 258 | uri | ||
| 259 | }); | ||
| 260 | } | ||
| 261 | |||
| 262 | async ping() { | ||
| 263 | return this.makeRequest('org.moqui.mcp.McpServices.mcp#Ping'); | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | // Usage | ||
| 268 | const client = new MoquiMCPClient('http://localhost:8080', 'admin', 'moqui'); | ||
| 269 | await client.initialize(); | ||
| 270 | const tools = await client.listTools(); | ||
| 271 | const result = await client.callTool('org.moqui.example.Services.create#Example', { | ||
| 272 | exampleName: 'Test Example' | ||
| 273 | }); | ||
| 274 | ``` | ||
| 275 | |||
| 276 | ### **cURL Examples** | ||
| 277 | |||
| 278 | #### **Initialize MCP Session** | ||
| 279 | ```bash | ||
| 280 | curl -X POST -H "Content-Type: application/json" \ | ||
| 281 | --data '{ | ||
| 282 | "jsonrpc": "2.0", | ||
| 283 | "id": "init_001", | ||
| 284 | "method": "org.moqui.mcp.McpServices.mcp#Initialize", | ||
| 285 | "params": { | ||
| 286 | "protocolVersion": "2025-06-18", | ||
| 287 | "capabilities": { | ||
| 288 | "roots": {}, | ||
| 289 | "sampling": {} | ||
| 290 | }, | ||
| 291 | "clientInfo": { | ||
| 292 | "name": "cURL Client", | ||
| 293 | "version": "1.0.0" | ||
| 294 | } | ||
| 295 | } | ||
| 296 | }' \ | ||
| 297 | http://localhost:8080/mcp/rpc | ||
| 298 | ``` | ||
| 299 | |||
| 300 | #### **List Available Tools** | ||
| 301 | ```bash | ||
| 302 | curl -X POST -H "Content-Type: application/json" \ | ||
| 303 | --data '{ | ||
| 304 | "jsonrpc": "2.0", | ||
| 305 | "id": "tools_001", | ||
| 306 | "method": "org.moqui.mcp.McpServices.mcp#ToolsList", | ||
| 307 | "params": {} | ||
| 308 | }' \ | ||
| 309 | http://localhost:8080/mcp/rpc | ||
| 310 | ``` | ||
| 311 | |||
| 312 | #### **Execute Tool** | ||
| 313 | ```bash | ||
| 314 | curl -X POST -H "Content-Type: application/json" \ | ||
| 315 | --data '{ | ||
| 316 | "jsonrpc": "2.0", | ||
| 317 | "id": "call_001", | ||
| 318 | "method": "org.moqui.mcp.McpServices.mcp#ToolsCall", | ||
| 319 | "params": { | ||
| 320 | "name": "org.moqui.example.Services.create#Example", | ||
| 321 | "arguments": { | ||
| 322 | "exampleName": "JSON-RPC Test", | ||
| 323 | "statusId": "EXST_ACTIVE" | ||
| 324 | } | ||
| 325 | } | ||
| 326 | }' \ | ||
| 327 | http://localhost:8080/mcp/rpc | ||
| 328 | ``` | ||
| 329 | |||
| 330 | #### **Read Entity Resource** | ||
| 331 | ```bash | ||
| 332 | curl -X POST -H "Content-Type: application/json" \ | ||
| 333 | --data '{ | ||
| 334 | "jsonrpc": "2.0", | ||
| 335 | "id": "resource_001", | ||
| 336 | "method": "org.moqui.mcp.McpServices.mcp#ResourcesRead", | ||
| 337 | "params": { | ||
| 338 | "uri": "entity://Example" | ||
| 339 | } | ||
| 340 | }' \ | ||
| 341 | http://localhost:8080/mcp/rpc | ||
| 342 | ``` | ||
| 343 | |||
| 344 | #### **Health Check** | ||
| 345 | ```bash | ||
| 346 | curl -X POST -H "Content-Type: application/json" \ | ||
| 347 | --data '{ | ||
| 348 | "jsonrpc": "2.0", | ||
| 349 | "id": "ping_001", | ||
| 350 | "method": "org.moqui.mcp.McpServices.mcp#Ping", | ||
| 351 | "params": {} | ||
| 352 | }' \ | ||
| 353 | http://localhost:8080/mcp/rpc | ||
| 354 | ``` | ||
| 355 | |||
| 356 | ## Security and Permissions | ||
| 357 | |||
| 358 | ### **Authentication** | ||
| 359 | - Uses Moqui's built-in user authentication | ||
| 360 | - Pass credentials via standard JSON-RPC `authUsername` and `authPassword` parameters | ||
| 361 | - Or use existing session via Moqui's web session | ||
| 362 | |||
| 363 | ### **Authorization** | ||
| 364 | - All MCP services require `authenticate="true"` | ||
| 365 | - Tool access controlled by Moqui's service permissions | ||
| 366 | - Entity access controlled by Moqui's entity permissions | ||
| 367 | - Audit logging via Moqui's ArtifactHit framework | ||
| 368 | |||
| 369 | ### **Permission Setup** | ||
| 370 | ```xml | ||
| 371 | <!-- MCP User Group --> | ||
| 372 | <moqui.security.UserGroup userGroupId="McpUser" description="MCP Server Users"/> | ||
| 373 | |||
| 374 | <!-- MCP Artifact Groups --> | ||
| 375 | <moqui.security.ArtifactGroup artifactGroupId="McpServices" description="MCP JSON-RPC Services"/> | ||
| 376 | <moqui.security.ArtifactGroup artifactGroupId="McpRestPaths" description="MCP REST API Paths"/> | ||
| 377 | <moqui.security.ArtifactGroup artifactGroupId="McpScreenTransitions" description="MCP Screen Transitions"/> | ||
| 378 | |||
| 379 | <!-- MCP Service Permissions --> | ||
| 380 | <moqui.security.ArtifactAuthz userGroupId="McpUser" | ||
| 381 | artifactGroupId="McpServices" | ||
| 382 | authzTypeEnumId="AUTHZT_ALLOW" | ||
| 383 | authzActionEnumId="AUTHZA_ALL"/> | ||
| 384 | <moqui.security.ArtifactAuthz userGroupId="McpUser" | ||
| 385 | artifactGroupId="McpRestPaths" | ||
| 386 | authzTypeEnumId="AUTHZT_ALLOW" | ||
| 387 | authzActionEnumId="AUTHZA_ALL"/> | ||
| 388 | <moqui.security.ArtifactAuthz userGroupId="McpUser" | ||
| 389 | artifactGroupId="McpScreenTransitions" | ||
| 390 | authzTypeEnumId="AUTHZT_ALLOW" | ||
| 391 | authzActionEnumId="AUTHZA_ALL"/> | ||
| 392 | ``` | ||
| 393 | |||
| 394 | ## Development and Testing | ||
| 395 | |||
| 396 | ### **Local Development** | ||
| 397 | 1. Start Moqui with moqui-mcp-2 component | ||
| 398 | 2. Test with JSON-RPC client examples above | ||
| 399 | 3. Check Moqui logs for debugging | ||
| 400 | 4. Monitor ArtifactHit records for audit trail | ||
| 401 | |||
| 402 | ### **Service Discovery** | ||
| 403 | - All Moqui services automatically available as MCP tools | ||
| 404 | - Filtered by user permissions | ||
| 405 | - Service parameters converted to JSON Schema | ||
| 406 | - Service descriptions used for tool descriptions | ||
| 407 | |||
| 408 | ### **Entity Access** | ||
| 409 | - All Moqui entities automatically available as MCP resources | ||
| 410 | - Format: `entity://EntityName` | ||
| 411 | - Filtered by entity VIEW permissions | ||
| 412 | - Limited to 100 records per request | ||
| 413 | |||
| 414 | ## Error Handling | ||
| 415 | |||
| 416 | ### **JSON-RPC Error Responses** | ||
| 417 | ```json | ||
| 418 | { | ||
| 419 | "jsonrpc": "2.0", | ||
| 420 | "id": "req_001", | ||
| 421 | "error": { | ||
| 422 | "code": -32601, | ||
| 423 | "message": "Method not found" | ||
| 424 | } | ||
| 425 | } | ||
| 426 | ``` | ||
| 427 | |||
| 428 | ### **Common Error Codes** | ||
| 429 | - `-32600`: Invalid Request (malformed JSON-RPC) | ||
| 430 | - `-32601`: Method not found | ||
| 431 | - `-32602`: Invalid params | ||
| 432 | - `-32603`: Internal error (service execution failed) | ||
| 433 | |||
| 434 | ### **Service-Level Errors** | ||
| 435 | - Permission denied throws Exception with message | ||
| 436 | - Invalid service name throws Exception | ||
| 437 | - Entity access violations throw Exception | ||
| 438 | - All errors logged to Moqui error system | ||
| 439 | |||
| 440 | ## Monitoring and Debugging | ||
| 441 | |||
| 442 | ### **Audit Trail** | ||
| 443 | - All MCP calls logged to `moqui.server.ArtifactHit` | ||
| 444 | - Track user, service, parameters, execution time | ||
| 445 | - Monitor success/failure rates | ||
| 446 | - Debug via Moqui's built-in monitoring tools | ||
| 447 | |||
| 448 | ### **Performance Metrics** | ||
| 449 | - Service execution time tracked | ||
| 450 | - Response size monitored | ||
| 451 | - Error rates captured | ||
| 452 | - User activity patterns available | ||
| 453 | |||
| 454 | --- | ||
| 455 | |||
| 456 | ## Testing Results | ||
| 457 | |||
| 458 | ### **✅ VERIFIED FUNCTIONALITY** | ||
| 459 | - **JSON-RPC Initialize**: Session creation and protocol negotiation | ||
| 460 | - **JSON-RPC Ping**: Health check with proper response format | ||
| 461 | - **JSON-RPC Tools List**: Service discovery working correctly | ||
| 462 | - **SSE Responses**: Server-sent events with proper event formatting | ||
| 463 | - **Session Management**: Session ID generation and validation | ||
| 464 | - **Content-Type Negotiation**: Correct prioritization of JSON over SSE | ||
| 465 | - **Security Integration**: All three authorization layers working | ||
| 466 | - **Error Handling**: Proper error responses for both formats | ||
| 467 | |||
| 468 | ### **✅ CLIENT COMPATIBILITY** | ||
| 469 | - **opencode Client**: Ready for production use | ||
| 470 | - **Standard MCP Clients**: Full protocol compliance | ||
| 471 | - **Custom Integrations**: Easy integration patterns documented | ||
| 472 | |||
| 473 | ## Key Advantages | ||
| 474 | |||
| 475 | ### **Unified Screen Benefits** | ||
| 476 | 1. **Single Endpoint**: One URL handles all MCP protocol variations | ||
| 477 | 2. **Content-Type Negotiation**: Automatic response format selection | ||
| 478 | 3. **Client Compatibility**: Works with both JSON-RPC and SSE clients | ||
| 479 | 4. **Simplicity**: No custom webapps or complex routing needed | ||
| 480 | 5. **Reliability**: Uses battle-tested Moqui screen framework | ||
| 481 | 6. **Security**: Leverages Moqui's mature security system | ||
| 482 | 7. **Audit**: Built-in logging and monitoring | ||
| 483 | 8. **Maintenance**: Minimal custom code to maintain | ||
| 484 | |||
| 485 | ### **Clean Architecture** | ||
| 486 | - Single screen handles all MCP protocol logic | ||
| 487 | - No custom authentication layers | ||
| 488 | - Standard Moqui session management | ||
| 489 | - Direct service-to-tool mapping | ||
| 490 | - Standard Moqui patterns throughout | ||
| 491 | - Full MCP protocol compliance | ||
| 492 | |||
| 493 | **This implementation demonstrates the power of Moqui's screen framework for unified MCP protocol handling.** | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README-SDK-Services.md
deleted
100644 → 0
| 1 | # Moqui MCP SDK Services Servlet | ||
| 2 | |||
| 3 | This document describes the MCP servlet implementation that combines the official Java MCP SDK with existing Moqui services for optimal architecture. | ||
| 4 | |||
| 5 | ## Overview | ||
| 6 | |||
| 7 | The `McpSdkServicesServlet` provides the best of both worlds: | ||
| 8 | |||
| 9 | - **MCP SDK** for standards-compliant protocol handling and SSE transport | ||
| 10 | - **McpServices.xml** for proven business logic and Moqui integration | ||
| 11 | - **Delegation pattern** for clean separation of concerns | ||
| 12 | - **Service reuse** across different MCP transport implementations | ||
| 13 | |||
| 14 | ## Architecture | ||
| 15 | |||
| 16 | ### Component Interaction | ||
| 17 | |||
| 18 | ``` | ||
| 19 | ┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ | ||
| 20 | │ MCP Client │───▶│ MCP SDK Servlet │───▶│ McpServices.xml│ | ||
| 21 | │ │ │ │ │ │ | ||
| 22 | │ - JSON-RPC 2.0 │ │ - Transport │ │ - Business │ | ||
| 23 | │ - SSE Events │ │ - Protocol │ │ Logic │ | ||
| 24 | │ - Tool Calls │ │ - Validation │ │ - Security │ | ||
| 25 | └─────────────────┘ └──────────────────┘ └─────────────────┘ | ||
| 26 | │ | ||
| 27 | ▼ | ||
| 28 | ┌─────────────────┐ | ||
| 29 | │ Moqui Framework│ | ||
| 30 | │ │ | ||
| 31 | │ - Entity Engine │ | ||
| 32 | │ - Service Engine│ | ||
| 33 | │ - Security │ | ||
| 34 | └─────────────────┘ | ||
| 35 | ``` | ||
| 36 | |||
| 37 | ### Service Delegation Pattern | ||
| 38 | |||
| 39 | | MCP Method | SDK Servlet | Moqui Service | Description | | ||
| 40 | |------------|-------------|---------------|-------------| | ||
| 41 | | `initialize` | `initialize` tool | `mcp#Initialize` | Session initialization | | ||
| 42 | | `ping` | `ping` tool | `mcp#Ping` | Health check | | ||
| 43 | | `tools/list` | `tools/list` tool | `mcp#ToolsList` | Discover available tools | | ||
| 44 | | `tools/call` | `tools/call` tool | `mcp#ToolsCall` | Execute tools/services | | ||
| 45 | | `resources/list` | `resources/list` tool | `mcp#ResourcesList` | Discover entities | | ||
| 46 | | `resources/read` | `resources/read` tool | `mcp#ResourcesRead` | Read entity data | | ||
| 47 | |||
| 48 | ## Benefits | ||
| 49 | |||
| 50 | ### 1. Separation of Concerns | ||
| 51 | |||
| 52 | - **SDK Servlet**: Protocol handling, transport, validation | ||
| 53 | - **McpServices.xml**: Business logic, security, data access | ||
| 54 | - **Clean interfaces**: Well-defined service contracts | ||
| 55 | |||
| 56 | ### 2. Code Reuse | ||
| 57 | |||
| 58 | - **Single source of truth**: All MCP logic in McpServices.xml | ||
| 59 | - **Multiple transports**: Same services work with different servlets | ||
| 60 | - **Testable services**: Can test business logic independently | ||
| 61 | |||
| 62 | ### 3. Standards Compliance | ||
| 63 | |||
| 64 | - **MCP SDK**: Guaranteed protocol compliance | ||
| 65 | - **JSON Schema**: Automatic validation | ||
| 66 | - **Error handling**: Standardized responses | ||
| 67 | |||
| 68 | ### 4. Moqui Integration | ||
| 69 | |||
| 70 | - **Security**: Leverages Moqui authentication/authorization | ||
| 71 | - **Auditing**: Built-in artifact hit tracking | ||
| 72 | - **Transactions**: Proper transaction management | ||
| 73 | - **Error handling**: Moqui exception handling | ||
| 74 | |||
| 75 | ## Configuration | ||
| 76 | |||
| 77 | ### Dependencies | ||
| 78 | |||
| 79 | ```gradle | ||
| 80 | dependencies { | ||
| 81 | // MCP Java SDK for transport and protocol | ||
| 82 | implementation 'io.modelcontextprotocol.sdk:mcp-sdk:1.0.0' | ||
| 83 | |||
| 84 | // Jackson for JSON handling (required by MCP SDK) | ||
| 85 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' | ||
| 86 | implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2' | ||
| 87 | |||
| 88 | // Servlet API | ||
| 89 | compileOnly 'javax.servlet:javax.servlet-api:4.0.1' | ||
| 90 | } | ||
| 91 | ``` | ||
| 92 | |||
| 93 | ### Web.xml Configuration | ||
| 94 | |||
| 95 | ```xml | ||
| 96 | <servlet> | ||
| 97 | <servlet-name>McpSdkServicesServlet</servlet-name> | ||
| 98 | <servlet-class>org.moqui.mcp.McpSdkServicesServlet</servlet-class> | ||
| 99 | |||
| 100 | <init-param> | ||
| 101 | <param-name>moqui-name</param-name> | ||
| 102 | <param-value>moqui-mcp-2</param-value> | ||
| 103 | </init-param> | ||
| 104 | |||
| 105 | <async-supported>true</async-supported> | ||
| 106 | <load-on-startup>1</load-on-startup> | ||
| 107 | </servlet> | ||
| 108 | |||
| 109 | <servlet-mapping> | ||
| 110 | <servlet-name>McpSdkServicesServlet</servlet-name> | ||
| 111 | <url-pattern>/mcp/*</url-pattern> | ||
| 112 | </servlet-mapping> | ||
| 113 | ``` | ||
| 114 | |||
| 115 | ## Available Tools | ||
| 116 | |||
| 117 | ### Core MCP Tools | ||
| 118 | |||
| 119 | | Tool | Service | Description | | ||
| 120 | |------|---------|-------------| | ||
| 121 | | `initialize` | `mcp#Initialize` | Initialize MCP session with capabilities negotiation | | ||
| 122 | | `ping` | `mcp#Ping` | Health check and server status | | ||
| 123 | | `tools/list` | `mcp#ToolsList` | List all available Moqui services as tools | | ||
| 124 | | `tools/call` | `mcp#ToolsCall` | Execute any Moqui service | | ||
| 125 | | `resources/list` | `mcp#ResourcesList` | List all accessible entities as resources | | ||
| 126 | | `resources/read` | `mcp#ResourcesRead` | Read entity data and metadata | | ||
| 127 | | `debug/component` | `debug#ComponentStatus` | Debug component status and configuration | | ||
| 128 | |||
| 129 | ### Tool Examples | ||
| 130 | |||
| 131 | #### Initialize Session | ||
| 132 | |||
| 133 | ```bash | ||
| 134 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 135 | -H "Content-Type: application/json" \ | ||
| 136 | -d '{ | ||
| 137 | "jsonrpc": "2.0", | ||
| 138 | "id": "init-1", | ||
| 139 | "method": "tools/call", | ||
| 140 | "params": { | ||
| 141 | "name": "initialize", | ||
| 142 | "arguments": { | ||
| 143 | "protocolVersion": "2025-06-18", | ||
| 144 | "clientInfo": { | ||
| 145 | "name": "Test Client", | ||
| 146 | "version": "1.0.0" | ||
| 147 | } | ||
| 148 | } | ||
| 149 | } | ||
| 150 | }' | ||
| 151 | ``` | ||
| 152 | |||
| 153 | #### List Available Tools | ||
| 154 | |||
| 155 | ```bash | ||
| 156 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 157 | -H "Content-Type: application/json" \ | ||
| 158 | -d '{ | ||
| 159 | "jsonrpc": "2.0", | ||
| 160 | "id": "tools-1", | ||
| 161 | "method": "tools/call", | ||
| 162 | "params": { | ||
| 163 | "name": "tools/list", | ||
| 164 | "arguments": {} | ||
| 165 | } | ||
| 166 | }' | ||
| 167 | ``` | ||
| 168 | |||
| 169 | #### Execute Moqui Service | ||
| 170 | |||
| 171 | ```bash | ||
| 172 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 173 | -H "Content-Type: application/json" \ | ||
| 174 | -d '{ | ||
| 175 | "jsonrpc": "2.0", | ||
| 176 | "id": "call-1", | ||
| 177 | "method": "tools/call", | ||
| 178 | "params": { | ||
| 179 | "name": "tools/call", | ||
| 180 | "arguments": { | ||
| 181 | "name": "org.moqui.entity.EntityServices.find#List", | ||
| 182 | "arguments": { | ||
| 183 | "entityName": "moqui.security.UserAccount", | ||
| 184 | "fields": ["username", "emailAddress"], | ||
| 185 | "limit": 10 | ||
| 186 | } | ||
| 187 | } | ||
| 188 | } | ||
| 189 | }' | ||
| 190 | ``` | ||
| 191 | |||
| 192 | ## Available Resources | ||
| 193 | |||
| 194 | ### Entity Resources | ||
| 195 | |||
| 196 | - **`entity://`** - List all entities | ||
| 197 | - **`entity://EntityName`** - Read specific entity data | ||
| 198 | |||
| 199 | #### Resource Examples | ||
| 200 | |||
| 201 | ```bash | ||
| 202 | # List all entities | ||
| 203 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 204 | -H "Content-Type: application/json" \ | ||
| 205 | -d '{ | ||
| 206 | "jsonrpc": "2.0", | ||
| 207 | "id": "res-1", | ||
| 208 | "method": "tools/call", | ||
| 209 | "params": { | ||
| 210 | "name": "resources/list", | ||
| 211 | "arguments": {} | ||
| 212 | } | ||
| 213 | }' | ||
| 214 | |||
| 215 | # Read specific entity | ||
| 216 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 217 | -H "Content-Type: application/json" \ | ||
| 218 | -d '{ | ||
| 219 | "jsonrpc": "2.0", | ||
| 220 | "id": "res-2", | ||
| 221 | "method": "tools/call", | ||
| 222 | "params": { | ||
| 223 | "name": "resources/read", | ||
| 224 | "arguments": { | ||
| 225 | "uri": "entity://moqui.security.UserAccount" | ||
| 226 | } | ||
| 227 | } | ||
| 228 | }' | ||
| 229 | ``` | ||
| 230 | |||
| 231 | ## Available Prompts | ||
| 232 | |||
| 233 | ### Entity Query Prompt | ||
| 234 | |||
| 235 | Generate Moqui entity queries: | ||
| 236 | |||
| 237 | ```bash | ||
| 238 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 239 | -H "Content-Type: application/json" \ | ||
| 240 | -d '{ | ||
| 241 | "jsonrpc": "2.0", | ||
| 242 | "id": "prompt-1", | ||
| 243 | "method": "prompts/get", | ||
| 244 | "params": { | ||
| 245 | "name": "entity-query", | ||
| 246 | "arguments": { | ||
| 247 | "entity": "mantle.order.OrderHeader", | ||
| 248 | "purpose": "Find recent orders", | ||
| 249 | "fields": "orderId, orderDate, grandTotal" | ||
| 250 | } | ||
| 251 | } | ||
| 252 | }' | ||
| 253 | ``` | ||
| 254 | |||
| 255 | ### Service Execution Prompt | ||
| 256 | |||
| 257 | Generate Moqui service execution plans: | ||
| 258 | |||
| 259 | ```bash | ||
| 260 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 261 | -H "Content-Type: application/json" \ | ||
| 262 | -d '{ | ||
| 263 | "jsonrpc": "2.0", | ||
| 264 | "id": "prompt-2", | ||
| 265 | "method": "prompts/get", | ||
| 266 | "params": { | ||
| 267 | "name": "service-execution", | ||
| 268 | "arguments": { | ||
| 269 | "serviceName": "create#OrderItem", | ||
| 270 | "description": "Create a new order item", | ||
| 271 | "parameters": "{\"orderId\":\"string\",\"productId\":\"string\",\"quantity\":\"number\"}" | ||
| 272 | } | ||
| 273 | } | ||
| 274 | }' | ||
| 275 | ``` | ||
| 276 | |||
| 277 | ## Service Implementation Details | ||
| 278 | |||
| 279 | ### McpServices.xml Features | ||
| 280 | |||
| 281 | The `McpServices.xml` provides comprehensive MCP functionality: | ||
| 282 | |||
| 283 | #### 1. Service Discovery (`mcp#ToolsList`) | ||
| 284 | |||
| 285 | ```xml | ||
| 286 | <service verb="mcp" noun="ToolsList" authenticate="true" allow-remote="true"> | ||
| 287 | <description>Handle MCP tools/list request with direct Moqui service discovery</description> | ||
| 288 | <actions> | ||
| 289 | <script><![CDATA[ | ||
| 290 | // Get all service names from Moqui service engine | ||
| 291 | def allServiceNames = ec.service.getKnownServiceNames() | ||
| 292 | def availableTools = [] | ||
| 293 | |||
| 294 | // Convert services to MCP tools | ||
| 295 | for (serviceName in allServiceNames) { | ||
| 296 | if (ec.service.hasPermission(serviceName)) { | ||
| 297 | def serviceInfo = ec.service.getServiceInfo(serviceName) | ||
| 298 | // Convert to MCP tool format... | ||
| 299 | } | ||
| 300 | } | ||
| 301 | ]]></script> | ||
| 302 | </actions> | ||
| 303 | </service> | ||
| 304 | ``` | ||
| 305 | |||
| 306 | #### 2. Service Execution (`mcp#ToolsCall`) | ||
| 307 | |||
| 308 | ```xml | ||
| 309 | <service verb="mcp" noun="ToolsCall" authenticate="true" allow-remote="true"> | ||
| 310 | <description>Handle MCP tools/call request with direct Moqui service execution</description> | ||
| 311 | <actions> | ||
| 312 | <script><![CDATA[ | ||
| 313 | // Validate service exists and user has permission | ||
| 314 | if (!ec.service.hasPermission(name)) { | ||
| 315 | throw new Exception("Permission denied for tool: ${name}") | ||
| 316 | } | ||
| 317 | |||
| 318 | // Create audit record | ||
| 319 | def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit") | ||
| 320 | // ... audit setup ... | ||
| 321 | |||
| 322 | // Execute service | ||
| 323 | def serviceResult = ec.service.sync().name(name).parameters(arguments).call() | ||
| 324 | |||
| 325 | // Convert result to MCP format | ||
| 326 | result = [content: content, isError: false] | ||
| 327 | ]]></script> | ||
| 328 | </actions> | ||
| 329 | </service> | ||
| 330 | ``` | ||
| 331 | |||
| 332 | #### 3. Entity Access (`mcp#ResourcesRead`) | ||
| 333 | |||
| 334 | ```xml | ||
| 335 | <service verb="mcp" noun="ResourcesRead" authenticate="true" allow-remote="true"> | ||
| 336 | <description>Handle MCP resources/read request with Moqui entity queries</description> | ||
| 337 | <actions> | ||
| 338 | <script><![CDATA[ | ||
| 339 | // Parse entity URI and validate permissions | ||
| 340 | if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) { | ||
| 341 | throw new Exception("Permission denied for entity: ${entityName}") | ||
| 342 | } | ||
| 343 | |||
| 344 | // Get entity definition and query data | ||
| 345 | def entityDef = ec.entity.getEntityDefinition(entityName) | ||
| 346 | def entityList = ec.entity.find(entityName).limit(100).list() | ||
| 347 | |||
| 348 | // Build comprehensive response | ||
| 349 | result = [contents: [[ | ||
| 350 | uri: uri, | ||
| 351 | mimeType: "application/json", | ||
| 352 | text: entityInfo + data | ||
| 353 | ]]] | ||
| 354 | ]]></script> | ||
| 355 | </actions> | ||
| 356 | </service> | ||
| 357 | ``` | ||
| 358 | |||
| 359 | ## Security Model | ||
| 360 | |||
| 361 | ### Authentication Integration | ||
| 362 | |||
| 363 | - **Moqui Authentication**: Uses existing Moqui user accounts | ||
| 364 | - **Session Management**: Leverages Moqui web sessions | ||
| 365 | - **Permission Checking**: Validates service and entity permissions | ||
| 366 | - **Audit Trail**: Records all MCP operations in ArtifactHit | ||
| 367 | |||
| 368 | ### Permission Model | ||
| 369 | |||
| 370 | | Operation | Permission Check | Description | | ||
| 371 | |------------|------------------|-------------| | ||
| 372 | | Service Execution | `ec.service.hasPermission(serviceName)` | Check service-level permissions | | ||
| 373 | | Entity Access | `ec.user.hasPermission("entity:EntityName", "VIEW")` | Check entity-level permissions | | ||
| 374 | | Resource Access | `ec.user.hasPermission("entity:EntityName", "VIEW")` | Check entity read permissions | | ||
| 375 | |||
| 376 | ### Audit Logging | ||
| 377 | |||
| 378 | All MCP operations are automatically logged: | ||
| 379 | |||
| 380 | ```groovy | ||
| 381 | def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit") | ||
| 382 | artifactHit.artifactType = "MCP" | ||
| 383 | artifactHit.artifactSubType = "Tool" // or "Resource" | ||
| 384 | artifactHit.artifactName = serviceName | ||
| 385 | artifactHit.parameterString = new JsonBuilder(arguments).toString() | ||
| 386 | artifactHit.userId = ec.user.userId | ||
| 387 | artifactHit.create() | ||
| 388 | ``` | ||
| 389 | |||
| 390 | ## Error Handling | ||
| 391 | |||
| 392 | ### Service-Level Errors | ||
| 393 | |||
| 394 | The McpServices.xml provides comprehensive error handling: | ||
| 395 | |||
| 396 | ```groovy | ||
| 397 | try { | ||
| 398 | def serviceResult = ec.service.sync().name(name).parameters(arguments).call() | ||
| 399 | result = [content: content, isError: false] | ||
| 400 | } catch (Exception e) { | ||
| 401 | // Update audit record with error | ||
| 402 | artifactHit.wasError = "Y" | ||
| 403 | artifactHit.errorMessage = e.message | ||
| 404 | artifactHit.update() | ||
| 405 | |||
| 406 | result = [ | ||
| 407 | content: [[type: "text", text: "Error executing tool ${name}: ${e.message}"]], | ||
| 408 | isError: true | ||
| 409 | ] | ||
| 410 | } | ||
| 411 | ``` | ||
| 412 | |||
| 413 | ### SDK-Level Errors | ||
| 414 | |||
| 415 | The MCP SDK handles protocol-level errors: | ||
| 416 | |||
| 417 | - **JSON-RPC validation** - Invalid requests | ||
| 418 | - **Schema validation** - Parameter validation | ||
| 419 | - **Transport errors** - Connection issues | ||
| 420 | - **Timeout handling** - Long-running operations | ||
| 421 | |||
| 422 | ## Performance Considerations | ||
| 423 | |||
| 424 | ### Service Caching | ||
| 425 | |||
| 426 | - **Service definitions** cached by Moqui service engine | ||
| 427 | - **Entity metadata** cached by entity engine | ||
| 428 | - **Permission checks** cached for performance | ||
| 429 | - **Audit records** batched for efficiency | ||
| 430 | |||
| 431 | ### Connection Management | ||
| 432 | |||
| 433 | - **SSE connections** managed by MCP SDK | ||
| 434 | - **Async processing** for non-blocking operations | ||
| 435 | - **Connection limits** prevent resource exhaustion | ||
| 436 | - **Graceful shutdown** on servlet destroy | ||
| 437 | |||
| 438 | ### Query Optimization | ||
| 439 | |||
| 440 | - **Entity queries** limited to 100 records by default | ||
| 441 | - **Field selection** reduces data transfer | ||
| 442 | - **Pagination support** for large result sets | ||
| 443 | - **Index utilization** through proper query construction | ||
| 444 | |||
| 445 | ## Monitoring and Debugging | ||
| 446 | |||
| 447 | ### Component Status Tool | ||
| 448 | |||
| 449 | Use the debug tool to check component status: | ||
| 450 | |||
| 451 | ```bash | ||
| 452 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 453 | -H "Content-Type: application/json" \ | ||
| 454 | -d '{ | ||
| 455 | "jsonrpc": "2.0", | ||
| 456 | "id": "debug-1", | ||
| 457 | "method": "tools/call", | ||
| 458 | "params": { | ||
| 459 | "name": "debug/component", | ||
| 460 | "arguments": {} | ||
| 461 | } | ||
| 462 | }' | ||
| 463 | ``` | ||
| 464 | |||
| 465 | ### Log Categories | ||
| 466 | |||
| 467 | - `org.moqui.mcp.McpSdkServicesServlet` - Servlet operations | ||
| 468 | - `org.moqui.mcp.McpServices` - Service operations | ||
| 469 | - `io.modelcontextprotocol.sdk` - MCP SDK operations | ||
| 470 | |||
| 471 | ### Audit Reports | ||
| 472 | |||
| 473 | Query audit records: | ||
| 474 | |||
| 475 | ```sql | ||
| 476 | SELECT artifactName, COUNT(*) as callCount, | ||
| 477 | AVG(runningTimeMillis) as avgTime, | ||
| 478 | COUNT(CASE WHEN wasError = 'Y' THEN 1 END) as errorCount | ||
| 479 | FROM moqui.server.ArtifactHit | ||
| 480 | WHERE artifactType = 'MCP' | ||
| 481 | GROUP BY artifactName | ||
| 482 | ORDER BY callCount DESC; | ||
| 483 | ``` | ||
| 484 | |||
| 485 | ## Comparison: Approaches | ||
| 486 | |||
| 487 | | Feature | Custom Servlet | SDK + Custom Logic | SDK + Services | | ||
| 488 | |---------|----------------|-------------------|-----------------| | ||
| 489 | | **Protocol Compliance** | Manual | SDK | SDK | | ||
| 490 | | **Business Logic** | Custom | Custom | Services | | ||
| 491 | | **Code Reuse** | Low | Medium | High | | ||
| 492 | | **Testing** | Complex | Medium | Simple | | ||
| 493 | | **Maintenance** | High | Medium | Low | | ||
| 494 | | **Security** | Custom | Custom | Moqui | | ||
| 495 | | **Auditing** | Custom | Custom | Built-in | | ||
| 496 | | **Performance** | Unknown | Good | Optimized | | ||
| 497 | |||
| 498 | ## Migration Guide | ||
| 499 | |||
| 500 | ### From Custom Implementation | ||
| 501 | |||
| 502 | 1. **Add MCP SDK dependency** to build.gradle | ||
| 503 | 2. **Deploy McpServices.xml** (if not already present) | ||
| 504 | 3. **Replace servlet** with `McpSdkServicesServlet` | ||
| 505 | 4. **Update web.xml** configuration | ||
| 506 | 5. **Test existing clients** for compatibility | ||
| 507 | 6. **Update documentation** with new tool names | ||
| 508 | |||
| 509 | ### Client Compatibility | ||
| 510 | |||
| 511 | The SDK + Services approach maintains compatibility: | ||
| 512 | |||
| 513 | - **Same JSON-RPC 2.0 protocol** | ||
| 514 | - **Enhanced tool interfaces** (more services available) | ||
| 515 | - **Better error responses** (standardized) | ||
| 516 | - **Improved security** (Moqui integration) | ||
| 517 | |||
| 518 | ## Best Practices | ||
| 519 | |||
| 520 | ### 1. Service Design | ||
| 521 | |||
| 522 | - **Keep services focused** on single responsibilities | ||
| 523 | - **Use proper error handling** with meaningful messages | ||
| 524 | - **Document parameters** with descriptions and types | ||
| 525 | - **Validate inputs** before processing | ||
| 526 | |||
| 527 | ### 2. Security | ||
| 528 | |||
| 529 | - **Always check permissions** before operations | ||
| 530 | - **Use parameterized queries** to prevent injection | ||
| 531 | - **Log all access** for audit trails | ||
| 532 | - **Sanitize outputs** to prevent data leakage | ||
| 533 | |||
| 534 | ### 3. Performance | ||
| 535 | |||
| 536 | - **Limit result sets** to prevent memory issues | ||
| 537 | - **Use appropriate indexes** for entity queries | ||
| 538 | - **Cache frequently accessed data** | ||
| 539 | - **Monitor execution times** for optimization | ||
| 540 | |||
| 541 | ### 4. Error Handling | ||
| 542 | |||
| 543 | - **Provide meaningful error messages** | ||
| 544 | - **Include context** for debugging | ||
| 545 | - **Log errors appropriately** | ||
| 546 | - **Graceful degradation** for failures | ||
| 547 | |||
| 548 | ## Future Enhancements | ||
| 549 | |||
| 550 | ### Planned Features | ||
| 551 | |||
| 552 | 1. **Async Services** - Support for long-running operations | ||
| 553 | 2. **Streaming Resources** - Large dataset streaming | ||
| 554 | 3. **Tool Categories** - Organize services by domain | ||
| 555 | 4. **Dynamic Registration** - Runtime service addition | ||
| 556 | 5. **Enhanced Prompts** - More sophisticated prompt templates | ||
| 557 | |||
| 558 | ### Advanced Integration | ||
| 559 | |||
| 560 | 1. **Event-Driven Updates** - Real-time entity change notifications | ||
| 561 | 2. **Workflow Integration** - MCP-triggered Moqui workflows | ||
| 562 | 3. **Multi-tenancy** - Tenant-aware MCP operations | ||
| 563 | 4. **GraphQL Support** - GraphQL as alternative to entity access | ||
| 564 | |||
| 565 | ## References | ||
| 566 | |||
| 567 | - [MCP Java SDK Documentation](https://modelcontextprotocol.io/sdk/java/mcp-server) | ||
| 568 | - [MCP Protocol Specification](https://modelcontextprotocol.io/specification/) | ||
| 569 | - [Moqui Framework Documentation](https://moqui.org/docs/) | ||
| 570 | - [Moqui Service Engine](https://moqui.org/docs/docs/moqui-service-engine) | ||
| 571 | - [Moqui Entity Engine](https://moqui.org/docs/docs/moqui-entity-engine) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README-SDK-Servlet.md
deleted
100644 → 0
| 1 | # Moqui MCP SDK Servlet | ||
| 2 | |||
| 3 | This document describes the MCP servlet implementation using the official Java MCP SDK library (`io.modelcontextprotocol.sdk`). | ||
| 4 | |||
| 5 | ## Overview | ||
| 6 | |||
| 7 | The `McpSdkSseServlet` uses the official MCP Java SDK to provide standards-compliant MCP server functionality with: | ||
| 8 | |||
| 9 | - **Full MCP protocol compliance** using official SDK | ||
| 10 | - **SSE transport** with proper session management | ||
| 11 | - **Built-in tool/resource/prompt registration** | ||
| 12 | - **JSON schema validation** for all inputs | ||
| 13 | - **Standard error handling** and response formatting | ||
| 14 | - **Async support** for scalable connections | ||
| 15 | |||
| 16 | ## Architecture | ||
| 17 | |||
| 18 | ### MCP SDK Integration | ||
| 19 | |||
| 20 | The servlet uses these key MCP SDK components: | ||
| 21 | |||
| 22 | - `HttpServletSseServerTransportProvider` - SSE transport implementation | ||
| 23 | - `McpSyncServer` - Synchronous MCP server API | ||
| 24 | - `McpServerFeatures.SyncToolSpecification` - Tool definitions | ||
| 25 | - `McpServerFeatures.SyncResourceSpecification` - Resource definitions | ||
| 26 | - `McpServerFeatures.SyncPromptSpecification` - Prompt definitions | ||
| 27 | |||
| 28 | ### Endpoint Structure | ||
| 29 | |||
| 30 | ``` | ||
| 31 | /mcp/sse - SSE endpoint for server-to-client events (handled by SDK) | ||
| 32 | /mcp/message - Message endpoint for client-to-server requests (handled by SDK) | ||
| 33 | ``` | ||
| 34 | |||
| 35 | ## Dependencies | ||
| 36 | |||
| 37 | ### Build Configuration | ||
| 38 | |||
| 39 | Add to `build.gradle`: | ||
| 40 | |||
| 41 | ```gradle | ||
| 42 | dependencies { | ||
| 43 | // MCP Java SDK | ||
| 44 | implementation 'io.modelcontextprotocol.sdk:mcp-sdk:1.0.0' | ||
| 45 | |||
| 46 | // Jackson for JSON handling (required by MCP SDK) | ||
| 47 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2' | ||
| 48 | implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.2' | ||
| 49 | |||
| 50 | // Servlet API | ||
| 51 | compileOnly 'javax.servlet:javax.servlet-api:4.0.1' | ||
| 52 | } | ||
| 53 | ``` | ||
| 54 | |||
| 55 | ### Maven Dependencies | ||
| 56 | |||
| 57 | ```xml | ||
| 58 | <dependencies> | ||
| 59 | <dependency> | ||
| 60 | <groupId>io.modelcontextprotocol.sdk</groupId> | ||
| 61 | <artifactId>mcp-sdk</artifactId> | ||
| 62 | <version>1.0.0</version> | ||
| 63 | </dependency> | ||
| 64 | |||
| 65 | <dependency> | ||
| 66 | <groupId>com.fasterxml.jackson.core</groupId> | ||
| 67 | <artifactId>jackson-databind</artifactId> | ||
| 68 | <version>2.15.2</version> | ||
| 69 | </dependency> | ||
| 70 | |||
| 71 | <dependency> | ||
| 72 | <groupId>com.fasterxml.jackson.datatype</groupId> | ||
| 73 | <artifactId>jackson-datatype-jsr310</artifactId> | ||
| 74 | <version>2.15.2</version> | ||
| 75 | </dependency> | ||
| 76 | </dependencies> | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ## Configuration | ||
| 80 | |||
| 81 | ### Web.xml Configuration | ||
| 82 | |||
| 83 | ```xml | ||
| 84 | <servlet> | ||
| 85 | <servlet-name>McpSdkSseServlet</servlet-name> | ||
| 86 | <servlet-class>org.moqui.mcp.McpSdkSseServlet</servlet-class> | ||
| 87 | |||
| 88 | <init-param> | ||
| 89 | <param-name>moqui-name</param-name> | ||
| 90 | <param-value>moqui-mcp-2</param-value> | ||
| 91 | </init-param> | ||
| 92 | |||
| 93 | <async-supported>true</async-supported> | ||
| 94 | <load-on-startup>1</load-on-startup> | ||
| 95 | </servlet> | ||
| 96 | |||
| 97 | <servlet-mapping> | ||
| 98 | <servlet-name>McpSdkSseServlet</servlet-name> | ||
| 99 | <url-pattern>/mcp/*</url-pattern> | ||
| 100 | </servlet-mapping> | ||
| 101 | ``` | ||
| 102 | |||
| 103 | ## Available Tools | ||
| 104 | |||
| 105 | ### Entity Tools | ||
| 106 | |||
| 107 | | Tool | Description | Parameters | | ||
| 108 | |------|-------------|------------| | ||
| 109 | | `EntityFind` | Query entities | `entity` (required), `fields`, `constraint`, `limit`, `offset` | | ||
| 110 | | `EntityCreate` | Create entity records | `entity` (required), `fields` (required) | | ||
| 111 | | `EntityUpdate` | Update entity records | `entity` (required), `fields` (required), `constraint` | | ||
| 112 | | `EntityDelete` | Delete entity records | `entity` (required), `constraint` | | ||
| 113 | |||
| 114 | ### Service Tools | ||
| 115 | |||
| 116 | | Tool | Description | Parameters | | ||
| 117 | |------|-------------|------------| | ||
| 118 | | `ServiceCall` | Execute Moqui services | `service` (required), `parameters` | | ||
| 119 | | `SystemStatus` | Get system statistics | `includeMetrics`, `includeCache` | | ||
| 120 | |||
| 121 | ## Available Resources | ||
| 122 | |||
| 123 | ### Entity Resources | ||
| 124 | |||
| 125 | - `entity://` - List all entities | ||
| 126 | - `entity://EntityName` - Get specific entity definition | ||
| 127 | |||
| 128 | **Example:** | ||
| 129 | ```bash | ||
| 130 | # List all entities | ||
| 131 | curl -H "Accept: application/json" \ | ||
| 132 | http://localhost:8080/mcp/resources?uri=entity:// | ||
| 133 | |||
| 134 | # Get specific entity | ||
| 135 | curl -H "Accept: application/json" \ | ||
| 136 | http://localhost:8080/mcp/resources?uri=entity://moqui.security.UserAccount | ||
| 137 | ``` | ||
| 138 | |||
| 139 | ## Available Prompts | ||
| 140 | |||
| 141 | ### Entity Query Prompt | ||
| 142 | |||
| 143 | Generate Moqui entity queries: | ||
| 144 | |||
| 145 | ```json | ||
| 146 | { | ||
| 147 | "name": "entity-query", | ||
| 148 | "arguments": { | ||
| 149 | "entity": "mantle.order.OrderHeader", | ||
| 150 | "purpose": "Find recent orders", | ||
| 151 | "fields": "orderId, orderDate, grandTotal" | ||
| 152 | } | ||
| 153 | } | ||
| 154 | ``` | ||
| 155 | |||
| 156 | ### Service Definition Prompt | ||
| 157 | |||
| 158 | Generate Moqui service definitions: | ||
| 159 | |||
| 160 | ```json | ||
| 161 | { | ||
| 162 | "name": "service-definition", | ||
| 163 | "arguments": { | ||
| 164 | "serviceName": "create#OrderItem", | ||
| 165 | "description": "Create a new order item", | ||
| 166 | "parameters": "{\"orderId\":\"string\",\"productId\":\"string\",\"quantity\":\"number\"}" | ||
| 167 | } | ||
| 168 | } | ||
| 169 | ``` | ||
| 170 | |||
| 171 | ## Usage Examples | ||
| 172 | |||
| 173 | ### Client Connection | ||
| 174 | |||
| 175 | ```javascript | ||
| 176 | // Using MCP SDK client | ||
| 177 | import { McpClient } from '@modelcontextprotocol/sdk/client/index.js'; | ||
| 178 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; | ||
| 179 | |||
| 180 | const transport = new StdioClientTransport({ | ||
| 181 | command: 'curl', | ||
| 182 | args: ['-X', 'POST', 'http://localhost:8080/mcp/message'] | ||
| 183 | }); | ||
| 184 | |||
| 185 | const client = new McpClient( | ||
| 186 | transport, | ||
| 187 | { | ||
| 188 | name: "Moqui Client", | ||
| 189 | version: "1.0.0" | ||
| 190 | } | ||
| 191 | ); | ||
| 192 | |||
| 193 | await client.connect(); | ||
| 194 | |||
| 195 | // List tools | ||
| 196 | const tools = await client.listTools(); | ||
| 197 | console.log('Available tools:', tools.tools); | ||
| 198 | |||
| 199 | // Call a tool | ||
| 200 | const result = await client.callTool({ | ||
| 201 | name: "EntityFind", | ||
| 202 | arguments: { | ||
| 203 | entity: "moqui.security.UserAccount", | ||
| 204 | limit: 10 | ||
| 205 | } | ||
| 206 | }); | ||
| 207 | console.log('Tool result:', result); | ||
| 208 | ``` | ||
| 209 | |||
| 210 | ### Raw HTTP Client | ||
| 211 | |||
| 212 | ```bash | ||
| 213 | # Initialize connection | ||
| 214 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 215 | -H "Content-Type: application/json" \ | ||
| 216 | -d '{ | ||
| 217 | "jsonrpc": "2.0", | ||
| 218 | "id": "init-1", | ||
| 219 | "method": "initialize", | ||
| 220 | "params": { | ||
| 221 | "clientInfo": { | ||
| 222 | "name": "Test Client", | ||
| 223 | "version": "1.0.0" | ||
| 224 | } | ||
| 225 | } | ||
| 226 | }' | ||
| 227 | |||
| 228 | # List tools | ||
| 229 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 230 | -H "Content-Type: application/json" \ | ||
| 231 | -d '{ | ||
| 232 | "jsonrpc": "2.0", | ||
| 233 | "id": "tools-1", | ||
| 234 | "method": "tools/list", | ||
| 235 | "params": {} | ||
| 236 | }' | ||
| 237 | |||
| 238 | # Call EntityFind tool | ||
| 239 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 240 | -H "Content-Type: application/json" \ | ||
| 241 | -d '{ | ||
| 242 | "jsonrpc": "2.0", | ||
| 243 | "id": "call-1", | ||
| 244 | "method": "tools/call", | ||
| 245 | "params": { | ||
| 246 | "name": "EntityFind", | ||
| 247 | "arguments": { | ||
| 248 | "entity": "moqui.security.UserAccount", | ||
| 249 | "fields": ["username", "emailAddress"], | ||
| 250 | "limit": 5 | ||
| 251 | } | ||
| 252 | } | ||
| 253 | }' | ||
| 254 | ``` | ||
| 255 | |||
| 256 | ## Tool Implementation Details | ||
| 257 | |||
| 258 | ### EntityFind Tool | ||
| 259 | |||
| 260 | ```groovy | ||
| 261 | def entityFindTool = new McpServerFeatures.SyncToolSpecification( | ||
| 262 | new Tool("EntityFind", "Find entities in Moqui using entity queries", """ | ||
| 263 | { | ||
| 264 | "type": "object", | ||
| 265 | "properties": { | ||
| 266 | "entity": {"type": "string", "description": "Entity name"}, | ||
| 267 | "fields": {"type": "array", "items": {"type": "string"}, "description": "Fields to select"}, | ||
| 268 | "constraint": {"type": "string", "description": "Constraint expression"}, | ||
| 269 | "limit": {"type": "number", "description": "Maximum results"}, | ||
| 270 | "offset": {"type": "number", "description": "Results offset"} | ||
| 271 | }, | ||
| 272 | "required": ["entity"] | ||
| 273 | } | ||
| 274 | """), | ||
| 275 | { exchange, arguments -> | ||
| 276 | return executeWithEc { ec -> | ||
| 277 | String entityName = arguments.get("entity") as String | ||
| 278 | List<String> fields = arguments.get("fields") as List<String> | ||
| 279 | String constraint = arguments.get("constraint") as String | ||
| 280 | Integer limit = arguments.get("limit") as Integer | ||
| 281 | Integer offset = arguments.get("offset") as Integer | ||
| 282 | |||
| 283 | def finder = ec.entity.find(entityName).selectFields(fields ?: ["*"]) | ||
| 284 | if (constraint) { | ||
| 285 | finder.condition(constraint) | ||
| 286 | } | ||
| 287 | if (offset) { | ||
| 288 | finder.offset(offset) | ||
| 289 | } | ||
| 290 | if (limit) { | ||
| 291 | finder.limit(limit) | ||
| 292 | } | ||
| 293 | def result = finder.list() | ||
| 294 | |||
| 295 | new CallToolResult([ | ||
| 296 | new TextContent("Found ${result.size()} records in ${entityName}: ${result}") | ||
| 297 | ], false) | ||
| 298 | } | ||
| 299 | } | ||
| 300 | ) | ||
| 301 | ``` | ||
| 302 | |||
| 303 | ## Error Handling | ||
| 304 | |||
| 305 | The MCP SDK provides standardized error handling: | ||
| 306 | |||
| 307 | ### JSON-RPC Errors | ||
| 308 | |||
| 309 | | Code | Description | Example | | ||
| 310 | |------|-------------|---------| | ||
| 311 | | `-32600` | Invalid Request | Malformed JSON-RPC | | ||
| 312 | | `-32601` | Method Not Found | Unknown method name | | ||
| 313 | | `-32602` | Invalid Params | Missing required parameters | | ||
| 314 | | `-32603` | Internal Error | Server-side exception | | ||
| 315 | |||
| 316 | ### Tool Execution Errors | ||
| 317 | |||
| 318 | ```groovy | ||
| 319 | return new CallToolResult([ | ||
| 320 | new TextContent("Error: " + e.message) | ||
| 321 | ], true) // isError = true | ||
| 322 | ``` | ||
| 323 | |||
| 324 | ### Resource Errors | ||
| 325 | |||
| 326 | ```groovy | ||
| 327 | throw new IllegalArgumentException("Entity not found: ${entityName}") | ||
| 328 | ``` | ||
| 329 | |||
| 330 | ## Security | ||
| 331 | |||
| 332 | ### Authentication Integration | ||
| 333 | |||
| 334 | The servlet integrates with Moqui's security framework: | ||
| 335 | |||
| 336 | ```groovy | ||
| 337 | // Authenticate as admin for MCP operations | ||
| 338 | if (!ec.user?.userId) { | ||
| 339 | try { | ||
| 340 | ec.user.loginUser("admin", "admin") | ||
| 341 | } catch (Exception e) { | ||
| 342 | logger.warn("MCP Admin login failed: ${e.message}") | ||
| 343 | } | ||
| 344 | } | ||
| 345 | ``` | ||
| 346 | |||
| 347 | ### CORS Support | ||
| 348 | |||
| 349 | ```groovy | ||
| 350 | private static boolean handleCors(HttpServletRequest request, HttpServletResponse response, String webappName) { | ||
| 351 | String originHeader = request.getHeader("Origin") | ||
| 352 | if (originHeader) { | ||
| 353 | response.setHeader("Access-Control-Allow-Origin", originHeader) | ||
| 354 | response.setHeader("Access-Control-Allow-Credentials", "true") | ||
| 355 | } | ||
| 356 | |||
| 357 | String methodHeader = request.getHeader("Access-Control-Request-Method") | ||
| 358 | if (methodHeader) { | ||
| 359 | response.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") | ||
| 360 | response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id, MCP-Protocol-Version, Accept") | ||
| 361 | response.setHeader("Access-Control-Max-Age", "3600") | ||
| 362 | return true | ||
| 363 | } | ||
| 364 | return false | ||
| 365 | } | ||
| 366 | ``` | ||
| 367 | |||
| 368 | ## Monitoring and Logging | ||
| 369 | |||
| 370 | ### Log Categories | ||
| 371 | |||
| 372 | - `org.moqui.mcp.McpSdkSseServlet` - Servlet operations | ||
| 373 | - `io.modelcontextprotocol.sdk` - MCP SDK operations | ||
| 374 | |||
| 375 | ### Key Log Messages | ||
| 376 | |||
| 377 | ``` | ||
| 378 | INFO - McpSdkSseServlet initialized for webapp moqui-mcp-2 | ||
| 379 | INFO - MCP Server started with SSE transport | ||
| 380 | INFO - Registered 6 Moqui tools | ||
| 381 | INFO - Registered Moqui resources | ||
| 382 | INFO - Registered Moqui prompts | ||
| 383 | ``` | ||
| 384 | |||
| 385 | ## Performance Considerations | ||
| 386 | |||
| 387 | ### Connection Management | ||
| 388 | |||
| 389 | - **Async processing** for non-blocking operations | ||
| 390 | - **Connection pooling** handled by servlet container | ||
| 391 | - **Session management** provided by MCP SDK | ||
| 392 | - **Graceful shutdown** on servlet destroy | ||
| 393 | |||
| 394 | ### Memory Usage | ||
| 395 | |||
| 396 | - **Tool definitions** loaded once at startup | ||
| 397 | - **ExecutionContext** created per request | ||
| 398 | - **JSON parsing** handled by Jackson | ||
| 399 | - **SSE connections** managed efficiently by SDK | ||
| 400 | |||
| 401 | ## Comparison: Custom vs SDK Implementation | ||
| 402 | |||
| 403 | | Feature | Custom Implementation | SDK Implementation | | ||
| 404 | |---------|---------------------|-------------------| | ||
| 405 | | **Protocol Compliance** | Manual implementation | Guaranteed by SDK | | ||
| 406 | | **Error Handling** | Custom code | Standardized | | ||
| 407 | | **JSON Schema** | Manual validation | Automatic | | ||
| 408 | | **SSE Transport** | Custom implementation | Built-in | | ||
| 409 | | **Session Management** | Manual code | Built-in | | ||
| 410 | | **Testing** | Custom tests | SDK tested | | ||
| 411 | | **Maintenance** | High effort | Low effort | | ||
| 412 | | **Standards Updates** | Manual updates | Automatic with SDK | | ||
| 413 | |||
| 414 | ## Migration from Custom Servlet | ||
| 415 | |||
| 416 | ### Benefits of SDK Migration | ||
| 417 | |||
| 418 | 1. **Standards Compliance** - Guaranteed MCP protocol compliance | ||
| 419 | 2. **Reduced Maintenance** - Less custom code to maintain | ||
| 420 | 3. **Better Error Handling** - Standardized error responses | ||
| 421 | 4. **Future Compatibility** - Automatic updates with SDK releases | ||
| 422 | 5. **Testing** - SDK includes comprehensive test suite | ||
| 423 | |||
| 424 | ### Migration Steps | ||
| 425 | |||
| 426 | 1. **Add MCP SDK dependency** to build.gradle | ||
| 427 | 2. **Replace servlet class** with `McpSdkSseServlet` | ||
| 428 | 3. **Update web.xml** configuration | ||
| 429 | 4. **Test existing clients** for compatibility | ||
| 430 | 5. **Update documentation** with new endpoints | ||
| 431 | |||
| 432 | ### Client Compatibility | ||
| 433 | |||
| 434 | The SDK implementation maintains compatibility with existing MCP clients: | ||
| 435 | |||
| 436 | - Same JSON-RPC 2.0 protocol | ||
| 437 | - Same tool/resource/prompt interfaces | ||
| 438 | - Same authentication patterns | ||
| 439 | - Better error responses and validation | ||
| 440 | |||
| 441 | ## Troubleshooting | ||
| 442 | |||
| 443 | ### Common Issues | ||
| 444 | |||
| 445 | 1. **Dependency Conflicts** | ||
| 446 | ```bash | ||
| 447 | # Check for Jackson version conflicts | ||
| 448 | ./gradlew dependencies | grep jackson | ||
| 449 | ``` | ||
| 450 | |||
| 451 | 2. **Async Support Issues** | ||
| 452 | ```xml | ||
| 453 | <!-- Ensure async is enabled in web.xml --> | ||
| 454 | <async-supported>true</async-supported> | ||
| 455 | ``` | ||
| 456 | |||
| 457 | 3. **Servlet Container Compatibility** | ||
| 458 | - Requires Servlet 3.0+ for async support | ||
| 459 | - Test with Tomcat 8.5+, Jetty 9.4+, or similar | ||
| 460 | |||
| 461 | 4. **MCP SDK Version** | ||
| 462 | ```gradle | ||
| 463 | // Use latest stable version | ||
| 464 | implementation 'io.modelcontextprotocol.sdk:mcp-sdk:1.0.0' | ||
| 465 | ``` | ||
| 466 | |||
| 467 | ### Debug Mode | ||
| 468 | |||
| 469 | Enable debug logging: | ||
| 470 | |||
| 471 | ```xml | ||
| 472 | <Logger name="org.moqui.mcp.McpSdkSseServlet" level="DEBUG"/> | ||
| 473 | <Logger name="io.modelcontextprotocol.sdk" level="DEBUG"/> | ||
| 474 | ``` | ||
| 475 | |||
| 476 | ## Future Enhancements | ||
| 477 | |||
| 478 | ### Planned Features | ||
| 479 | |||
| 480 | 1. **Async Tools** - Use `McpServerFeatures.AsyncToolSpecification` | ||
| 481 | 2. **Streaming Resources** - Large data set streaming | ||
| 482 | 3. **Tool Categories** - Organize tools by domain | ||
| 483 | 4. **Dynamic Registration** - Runtime tool/resource addition | ||
| 484 | 5. **Metrics Collection** - Performance and usage metrics | ||
| 485 | |||
| 486 | ### Advanced Integration | ||
| 487 | |||
| 488 | 1. **Spring Integration** - Use MCP SDK Spring starters | ||
| 489 | 2. **WebFlux Support** - Reactive transport providers | ||
| 490 | 3. **WebSocket Transport** - Alternative to SSE | ||
| 491 | 4. **Load Balancing** - Multi-instance deployment | ||
| 492 | |||
| 493 | ## References | ||
| 494 | |||
| 495 | - [MCP Java SDK Documentation](https://modelcontextprotocol.io/sdk/java/mcp-server) | ||
| 496 | - [MCP Protocol Specification](https://modelcontextprotocol.io/specification/) | ||
| 497 | - [Moqui Framework Documentation](https://moqui.org/docs/) | ||
| 498 | - [Servlet 3.0 Specification](https://jakarta.ee/specifications/servlet/3.0/) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
README-SSE-Servlet.md
deleted
100644 → 0
| 1 | # Moqui MCP SSE Servlet | ||
| 2 | |||
| 3 | This document describes the new MCP SSE (Server-Sent Events) servlet implementation based on the official Java MCP SDK SSE Servlet approach. | ||
| 4 | |||
| 5 | ## Overview | ||
| 6 | |||
| 7 | The `McpSseServlet` implements the MCP SSE transport specification using the traditional Servlet API, providing: | ||
| 8 | |||
| 9 | - **Asynchronous message handling** using Servlet async support | ||
| 10 | - **Session management** for multiple client connections | ||
| 11 | - **Two types of endpoints**: | ||
| 12 | - SSE endpoint (`/sse`) for server-to-client events | ||
| 13 | - Message endpoint (`/mcp/message`) for client-to-server requests | ||
| 14 | - **Error handling** and response formatting | ||
| 15 | - **Graceful shutdown** support | ||
| 16 | - **Keep-alive pings** to maintain connections | ||
| 17 | - **Connection limits** to prevent resource exhaustion | ||
| 18 | |||
| 19 | ## Architecture | ||
| 20 | |||
| 21 | ### Endpoint Structure | ||
| 22 | |||
| 23 | ``` | ||
| 24 | /sse - SSE endpoint for server-to-client events | ||
| 25 | /mcp/message - Message endpoint for client-to-server requests | ||
| 26 | ``` | ||
| 27 | |||
| 28 | ### Connection Flow | ||
| 29 | |||
| 30 | 1. **Client connects** to `/sse` endpoint | ||
| 31 | 2. **Server establishes** SSE connection with unique session ID | ||
| 32 | 3. **Client sends** JSON-RPC messages to `/mcp/message` | ||
| 33 | 4. **Server processes** messages and sends responses via SSE | ||
| 34 | 5. **Keep-alive pings** maintain connection health | ||
| 35 | |||
| 36 | ## Configuration | ||
| 37 | |||
| 38 | ### Servlet Configuration Parameters | ||
| 39 | |||
| 40 | | Parameter | Default | Description | | ||
| 41 | |-----------|---------|-------------| | ||
| 42 | | `moqui-name` | - | Moqui webapp name (required) | | ||
| 43 | | `sseEndpoint` | `/sse` | SSE endpoint path | | ||
| 44 | | `messageEndpoint` | `/mcp/message` | Message endpoint path | | ||
| 45 | | `keepAliveIntervalSeconds` | `30` | Keep-alive ping interval | | ||
| 46 | | `maxConnections` | `100` | Maximum concurrent SSE connections | | ||
| 47 | |||
| 48 | ### Web.xml Configuration | ||
| 49 | |||
| 50 | ```xml | ||
| 51 | <servlet> | ||
| 52 | <servlet-name>McpSseServlet</servlet-name> | ||
| 53 | <servlet-class>org.moqui.mcp.McpSseServlet</servlet-class> | ||
| 54 | |||
| 55 | <init-param> | ||
| 56 | <param-name>moqui-name</param-name> | ||
| 57 | <param-value>moqui-mcp-2</param-value> | ||
| 58 | </init-param> | ||
| 59 | |||
| 60 | <init-param> | ||
| 61 | <param-name>keepAliveIntervalSeconds</param-name> | ||
| 62 | <param-value>30</param-value> | ||
| 63 | </init-param> | ||
| 64 | |||
| 65 | <async-supported>true</async-supported> | ||
| 66 | </servlet> | ||
| 67 | |||
| 68 | <servlet-mapping> | ||
| 69 | <servlet-name>McpSseServlet</servlet-name> | ||
| 70 | <url-pattern>/sse/*</url-pattern> | ||
| 71 | </servlet-mapping> | ||
| 72 | |||
| 73 | <servlet-mapping> | ||
| 74 | <servlet-name>McpSseServlet</servlet-name> | ||
| 75 | <url-pattern>/mcp/message/*</url-pattern> | ||
| 76 | </servlet-mapping> | ||
| 77 | ``` | ||
| 78 | |||
| 79 | ## Usage Examples | ||
| 80 | |||
| 81 | ### Client Connection Flow | ||
| 82 | |||
| 83 | #### 1. Establish SSE Connection | ||
| 84 | |||
| 85 | ```bash | ||
| 86 | curl -N -H "Accept: text/event-stream" \ | ||
| 87 | http://localhost:8080/sse | ||
| 88 | ``` | ||
| 89 | |||
| 90 | **Response:** | ||
| 91 | ``` | ||
| 92 | event: connect | ||
| 93 | data: {"type":"connected","sessionId":"uuid-123","timestamp":1234567890,"serverInfo":{"name":"Moqui MCP SSE Server","version":"2.0.0","protocolVersion":"2025-06-18"}} | ||
| 94 | |||
| 95 | event: ping | ||
| 96 | data: {"type":"ping","timestamp":1234567890,"connections":1} | ||
| 97 | ``` | ||
| 98 | |||
| 99 | #### 2. Initialize MCP Session | ||
| 100 | |||
| 101 | ```bash | ||
| 102 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 103 | -H "Content-Type: application/json" \ | ||
| 104 | -d '{ | ||
| 105 | "jsonrpc": "2.0", | ||
| 106 | "id": "init-1", | ||
| 107 | "method": "initialize", | ||
| 108 | "params": { | ||
| 109 | "clientInfo": { | ||
| 110 | "name": "Test Client", | ||
| 111 | "version": "1.0.0" | ||
| 112 | } | ||
| 113 | } | ||
| 114 | }' | ||
| 115 | ``` | ||
| 116 | |||
| 117 | **Response:** | ||
| 118 | ```json | ||
| 119 | { | ||
| 120 | "jsonrpc": "2.0", | ||
| 121 | "id": "init-1", | ||
| 122 | "result": { | ||
| 123 | "protocolVersion": "2025-06-18", | ||
| 124 | "capabilities": { | ||
| 125 | "tools": {"listChanged": true}, | ||
| 126 | "resources": {"subscribe": true, "listChanged": true}, | ||
| 127 | "logging": {}, | ||
| 128 | "notifications": {} | ||
| 129 | }, | ||
| 130 | "serverInfo": { | ||
| 131 | "name": "Moqui MCP SSE Server", | ||
| 132 | "version": "2.0.0" | ||
| 133 | }, | ||
| 134 | "sessionId": "uuid-123" | ||
| 135 | } | ||
| 136 | } | ||
| 137 | ``` | ||
| 138 | |||
| 139 | #### 3. List Available Tools | ||
| 140 | |||
| 141 | ```bash | ||
| 142 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 143 | -H "Content-Type: application/json" \ | ||
| 144 | -d '{ | ||
| 145 | "jsonrpc": "2.0", | ||
| 146 | "id": "tools-1", | ||
| 147 | "method": "tools/list", | ||
| 148 | "params": {} | ||
| 149 | }' | ||
| 150 | ``` | ||
| 151 | |||
| 152 | #### 4. Call a Tool | ||
| 153 | |||
| 154 | ```bash | ||
| 155 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 156 | -H "Content-Type: application/json" \ | ||
| 157 | -d '{ | ||
| 158 | "jsonrpc": "2.0", | ||
| 159 | "id": "call-1", | ||
| 160 | "method": "tools/call", | ||
| 161 | "params": { | ||
| 162 | "name": "EntityFind", | ||
| 163 | "arguments": { | ||
| 164 | "entity": "moqui.security.UserAccount", | ||
| 165 | "fields": ["username", "emailAddress"], | ||
| 166 | "limit": 10 | ||
| 167 | } | ||
| 168 | } | ||
| 169 | }' | ||
| 170 | ``` | ||
| 171 | |||
| 172 | ### JavaScript Client Example | ||
| 173 | |||
| 174 | ```javascript | ||
| 175 | class McpSseClient { | ||
| 176 | constructor(baseUrl) { | ||
| 177 | this.baseUrl = baseUrl; | ||
| 178 | this.sessionId = null; | ||
| 179 | this.eventSource = null; | ||
| 180 | this.messageId = 0; | ||
| 181 | } | ||
| 182 | |||
| 183 | async connect() { | ||
| 184 | // Establish SSE connection | ||
| 185 | this.eventSource = new EventSource(`${this.baseUrl}/sse`); | ||
| 186 | |||
| 187 | this.eventSource.addEventListener('connect', (event) => { | ||
| 188 | const data = JSON.parse(event.data); | ||
| 189 | console.log('Connected:', data); | ||
| 190 | }); | ||
| 191 | |||
| 192 | this.eventSource.addEventListener('ping', (event) => { | ||
| 193 | const data = JSON.parse(event.data); | ||
| 194 | console.log('Ping:', data); | ||
| 195 | }); | ||
| 196 | |||
| 197 | this.eventSource.addEventListener('initialized', (event) => { | ||
| 198 | const data = JSON.parse(event.data); | ||
| 199 | console.log('Server initialized:', data); | ||
| 200 | }); | ||
| 201 | |||
| 202 | // Initialize MCP session | ||
| 203 | const initResponse = await this.sendMessage('initialize', { | ||
| 204 | clientInfo: { | ||
| 205 | name: 'JavaScript Client', | ||
| 206 | version: '1.0.0' | ||
| 207 | } | ||
| 208 | }); | ||
| 209 | |||
| 210 | this.sessionId = initResponse.sessionId; | ||
| 211 | return initResponse; | ||
| 212 | } | ||
| 213 | |||
| 214 | async sendMessage(method, params = {}) { | ||
| 215 | const id = `msg-${++this.messageId}`; | ||
| 216 | const payload = { | ||
| 217 | jsonrpc: "2.0", | ||
| 218 | id: id, | ||
| 219 | method: method, | ||
| 220 | params: params | ||
| 221 | }; | ||
| 222 | |||
| 223 | const response = await fetch(`${this.baseUrl}/mcp/message`, { | ||
| 224 | method: 'POST', | ||
| 225 | headers: { | ||
| 226 | 'Content-Type': 'application/json' | ||
| 227 | }, | ||
| 228 | body: JSON.stringify(payload) | ||
| 229 | }); | ||
| 230 | |||
| 231 | return await response.json(); | ||
| 232 | } | ||
| 233 | |||
| 234 | async listTools() { | ||
| 235 | return await this.sendMessage('tools/list'); | ||
| 236 | } | ||
| 237 | |||
| 238 | async callTool(name, arguments) { | ||
| 239 | return await this.sendMessage('tools/call', { | ||
| 240 | name: name, | ||
| 241 | arguments: arguments | ||
| 242 | }); | ||
| 243 | } | ||
| 244 | |||
| 245 | disconnect() { | ||
| 246 | if (this.eventSource) { | ||
| 247 | this.eventSource.close(); | ||
| 248 | } | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | // Usage | ||
| 253 | const client = new McpSseClient('http://localhost:8080'); | ||
| 254 | |||
| 255 | client.connect().then(() => { | ||
| 256 | console.log('MCP client connected'); | ||
| 257 | |||
| 258 | // List tools | ||
| 259 | return client.listTools(); | ||
| 260 | }).then(tools => { | ||
| 261 | console.log('Available tools:', tools); | ||
| 262 | |||
| 263 | // Call a tool | ||
| 264 | return client.callTool('EntityFind', { | ||
| 265 | entity: 'moqui.security.UserAccount', | ||
| 266 | limit: 5 | ||
| 267 | }); | ||
| 268 | }).then(result => { | ||
| 269 | console.log('Tool result:', result); | ||
| 270 | }).catch(error => { | ||
| 271 | console.error('MCP client error:', error); | ||
| 272 | }); | ||
| 273 | ``` | ||
| 274 | |||
| 275 | ## Features | ||
| 276 | |||
| 277 | ### Session Management | ||
| 278 | |||
| 279 | - **Unique session IDs** generated for each connection | ||
| 280 | - **Connection tracking** with metadata (user agent, timestamps) | ||
| 281 | - **Automatic cleanup** on connection close/error | ||
| 282 | - **Connection limits** to prevent resource exhaustion | ||
| 283 | |||
| 284 | ### Event Types | ||
| 285 | |||
| 286 | | Event Type | Description | Data | | ||
| 287 | |------------|-------------|------| | ||
| 288 | | `connect` | Initial connection established | Session info, server details | | ||
| 289 | | `ping` | Keep-alive ping | Timestamp, connection count | | ||
| 290 | | `initialized` | Server initialization notification | Session info, client details | | ||
| 291 | | `subscribed` | Subscription confirmation | Session, event type | | ||
| 292 | | `tool_result` | Tool execution result | Tool name, result data | | ||
| 293 | | `resource_update` | Resource change notification | Resource URI, change data | | ||
| 294 | |||
| 295 | ### Error Handling | ||
| 296 | |||
| 297 | - **JSON-RPC 2.0 compliant** error responses | ||
| 298 | - **Connection error recovery** with automatic cleanup | ||
| 299 | - **Resource exhaustion protection** with connection limits | ||
| 300 | - **Graceful degradation** on service failures | ||
| 301 | |||
| 302 | ### Security | ||
| 303 | |||
| 304 | - **CORS support** for cross-origin requests | ||
| 305 | - **Moqui authentication integration** with admin fallback | ||
| 306 | - **Session isolation** between clients | ||
| 307 | - **Input validation** for all requests | ||
| 308 | |||
| 309 | ## Monitoring | ||
| 310 | |||
| 311 | ### Connection Status | ||
| 312 | |||
| 313 | Get current server status: | ||
| 314 | |||
| 315 | ```bash | ||
| 316 | curl http://localhost:8080/mcp/message | ||
| 317 | ``` | ||
| 318 | |||
| 319 | **Response:** | ||
| 320 | ```json | ||
| 321 | { | ||
| 322 | "serverInfo": { | ||
| 323 | "name": "Moqui MCP SSE Server", | ||
| 324 | "version": "2.0.0", | ||
| 325 | "protocolVersion": "2025-06-18" | ||
| 326 | }, | ||
| 327 | "connections": { | ||
| 328 | "active": 3, | ||
| 329 | "max": 100 | ||
| 330 | }, | ||
| 331 | "endpoints": { | ||
| 332 | "sse": "/sse", | ||
| 333 | "message": "/mcp/message" | ||
| 334 | } | ||
| 335 | } | ||
| 336 | ``` | ||
| 337 | |||
| 338 | ### Logging | ||
| 339 | |||
| 340 | The servlet provides detailed logging for: | ||
| 341 | |||
| 342 | - Connection establishment/cleanup | ||
| 343 | - Message processing | ||
| 344 | - Error conditions | ||
| 345 | - Performance metrics | ||
| 346 | |||
| 347 | Log levels: | ||
| 348 | - `INFO`: Connection events, message processing | ||
| 349 | - `WARN`: Connection errors, authentication issues | ||
| 350 | - `ERROR`: System errors, service failures | ||
| 351 | - `DEBUG`: Detailed request/response data | ||
| 352 | |||
| 353 | ## Comparison with Existing Servlet | ||
| 354 | |||
| 355 | | Feature | MoquiMcpServlet | McpSseServlet | | ||
| 356 | |---------|------------------|---------------| | ||
| 357 | | **Transport** | HTTP POST/GET | SSE + HTTP | | ||
| 358 | | **Async Support** | Limited | Full async | | ||
| 359 | | **Session Management** | Basic | Advanced | | ||
| 360 | | **Real-time Events** | No | Yes | | ||
| 361 | | **Connection Limits** | No | Yes | | ||
| 362 | | **Keep-alive** | No | Yes | | ||
| 363 | | **Standards Compliance** | Custom | MCP SSE Spec | | ||
| 364 | |||
| 365 | ## Migration Guide | ||
| 366 | |||
| 367 | ### From MoquiMcpServlet to McpSseServlet | ||
| 368 | |||
| 369 | 1. **Update web.xml** to use the new servlet | ||
| 370 | 2. **Update client code** to handle SSE connections | ||
| 371 | 3. **Modify endpoints** from `/mcp/rpc` to `/mcp/message` | ||
| 372 | 4. **Add SSE handling** for real-time events | ||
| 373 | 5. **Update authentication** if using custom auth | ||
| 374 | |||
| 375 | ### Client Migration | ||
| 376 | |||
| 377 | **Old approach:** | ||
| 378 | ```bash | ||
| 379 | curl -X POST http://localhost:8080/mcp/rpc \ | ||
| 380 | -H "Content-Type: application/json" \ | ||
| 381 | -d '{"jsonrpc":"2.0","method":"ping","id":1}' | ||
| 382 | ``` | ||
| 383 | |||
| 384 | **New approach:** | ||
| 385 | ```bash | ||
| 386 | # 1. Establish SSE connection | ||
| 387 | curl -N -H "Accept: text/event-stream" http://localhost:8080/sse & | ||
| 388 | |||
| 389 | # 2. Send messages | ||
| 390 | curl -X POST http://localhost:8080/mcp/message \ | ||
| 391 | -H "Content-Type: application/json" \ | ||
| 392 | -d '{"jsonrpc":"2.0","method":"ping","id":1}' | ||
| 393 | ``` | ||
| 394 | |||
| 395 | ## Troubleshooting | ||
| 396 | |||
| 397 | ### Common Issues | ||
| 398 | |||
| 399 | 1. **SSE Connection Fails** | ||
| 400 | - Check async support is enabled in web.xml | ||
| 401 | - Verify servlet container supports Servlet 3.0+ | ||
| 402 | - Check firewall/proxy settings | ||
| 403 | |||
| 404 | 2. **Messages Not Received** | ||
| 405 | - Verify session ID is valid | ||
| 406 | - Check connection is still active | ||
| 407 | - Review server logs for errors | ||
| 408 | |||
| 409 | 3. **High Memory Usage** | ||
| 410 | - Reduce `maxConnections` parameter | ||
| 411 | - Check for connection leaks | ||
| 412 | - Monitor session cleanup | ||
| 413 | |||
| 414 | 4. **Authentication Issues** | ||
| 415 | - Verify Moqui security configuration | ||
| 416 | - Check admin user credentials | ||
| 417 | - Review webapp security settings | ||
| 418 | |||
| 419 | ### Debug Mode | ||
| 420 | |||
| 421 | Enable debug logging by adding to log4j2.xml: | ||
| 422 | |||
| 423 | ```xml | ||
| 424 | <Logger name="org.moqui.mcp.McpSseServlet" level="DEBUG" additivity="false"> | ||
| 425 | <AppenderRef ref="Console"/> | ||
| 426 | </Logger> | ||
| 427 | ``` | ||
| 428 | |||
| 429 | ## Performance Considerations | ||
| 430 | |||
| 431 | ### Connection Scaling | ||
| 432 | |||
| 433 | - **Default limit**: 100 concurrent connections | ||
| 434 | - **Memory usage**: ~1KB per connection | ||
| 435 | - **CPU overhead**: Minimal for idle connections | ||
| 436 | - **Network bandwidth**: Low (keep-alive pings only) | ||
| 437 | |||
| 438 | ### Optimization Tips | ||
| 439 | |||
| 440 | 1. **Adjust keep-alive interval** based on network conditions | ||
| 441 | 2. **Monitor connection counts** and adjust limits | ||
| 442 | 3. **Use connection pooling** for high-frequency clients | ||
| 443 | 4. **Implement backpressure** for high-volume scenarios | ||
| 444 | |||
| 445 | ## Future Enhancements | ||
| 446 | |||
| 447 | Planned improvements: | ||
| 448 | |||
| 449 | 1. **WebSocket support** as alternative to SSE | ||
| 450 | 2. **Message queuing** for offline clients | ||
| 451 | 3. **Load balancing** support for multiple instances | ||
| 452 | 4. **Advanced authentication** with token-based auth | ||
| 453 | 5. **Metrics and monitoring** integration | ||
| 454 | 6. **Message compression** for large payloads | ||
| 455 | |||
| 456 | ## References | ||
| 457 | |||
| 458 | - [MCP Specification](https://modelcontextprotocol.io/) | ||
| 459 | - [Java MCP SDK](https://modelcontextprotocol.io/sdk/java/mcp-server) | ||
| 460 | - [Servlet 3.0 Specification](https://jakarta.ee/specifications/servlet/3.0/) | ||
| 461 | - [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -11,52 +11,13 @@ | ... | @@ -11,52 +11,13 @@ |
| 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> | 11 | <https://creativecommons.org/publicdomain/zero/1.0/>. --> |
| 12 | 12 | ||
| 13 | <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | 13 | <component xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
| 14 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/component-definition-3.xsd" | 14 | xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/moqui-conf-3.xsd" |
| 15 | name="moqui-mcp-2"> | 15 | name="moqui-mcp-2" version="1.0.0"> |
| 16 | |||
| 17 | |||
| 18 | |||
| 19 | <!-- MCP SDK dependencies --> | ||
| 20 | <dependency> | ||
| 21 | <groupId>io.modelcontextprotocol.sdk</groupId> | ||
| 22 | <artifactId>mcp</artifactId> | ||
| 23 | <version>0.16.0</version> | ||
| 24 | </dependency> | ||
| 25 | <dependency> | ||
| 26 | <groupId>io.modelcontextprotocol.sdk</groupId> | ||
| 27 | <artifactId>mcp-core</artifactId> | ||
| 28 | <version>0.16.0</version> | ||
| 29 | </dependency> | ||
| 30 | <dependency> | ||
| 31 | <groupId>com.fasterxml.jackson.core</groupId> | ||
| 32 | <artifactId>jackson-databind</artifactId> | ||
| 33 | <version>2.15.2</version> | ||
| 34 | </dependency> | ||
| 35 | <dependency> | ||
| 36 | <groupId>com.fasterxml.jackson.core</groupId> | ||
| 37 | <artifactId>jackson-core</artifactId> | ||
| 38 | <version>2.15.2</version> | ||
| 39 | </dependency> | ||
| 40 | <dependency> | ||
| 41 | <groupId>io.projectreactor</groupId> | ||
| 42 | <artifactId>reactor-core</artifactId> | ||
| 43 | <version>3.5.10</version> | ||
| 44 | </dependency> | ||
| 45 | 16 | ||
| 46 | <entity-factory load-path="entity/" /> | 17 | <entity-factory load-path="entity/" /> |
| 47 | <service-factory load-path="service/" /> | 18 | <service-factory load-path="service/" /> |
| 48 | <!-- <screen-factory load-path="screen/" /> --> | ||
| 49 | 19 | ||
| 50 | <!-- Load seed data --> | 20 | <!-- Load seed data --> |
| 51 | <entity-factory load-data="data/McpSecuritySeedData.xml" /> | 21 | <entity-factory load-data="data/McpSecuritySeedData.xml" /> |
| 52 | 22 | ||
| 53 | <!-- Register MCP filter | ||
| 54 | <webapp-list> | ||
| 55 | <webapp name="webroot"> | ||
| 56 | <filter name="McpFilter" class="org.moqui.mcp.McpFilter"> | ||
| 57 | <url-pattern>/mcpservlet/*</url-pattern> | ||
| 58 | </filter> | ||
| 59 | </webapp> | ||
| 60 | </webapp-list> --> | ||
| 61 | |||
| 62 | </component> | 23 | </component> | ... | ... |
| ... | @@ -22,7 +22,6 @@ | ... | @@ -22,7 +22,6 @@ |
| 22 | <moqui.security.ArtifactGroup artifactGroupId="McpScreenTransitions" description="MCP Screen Transitions"/> | 22 | <moqui.security.ArtifactGroup artifactGroupId="McpScreenTransitions" description="MCP Screen Transitions"/> |
| 23 | 23 | ||
| 24 | <!-- MCP Artifact Group Members --> | 24 | <!-- MCP Artifact Group Members --> |
| 25 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="mo-mcp.mo-mcp.*" artifactTypeEnumId="AT_SERVICE"/> | ||
| 26 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.*" artifactTypeEnumId="AT_SERVICE"/> | 25 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.*" artifactTypeEnumId="AT_SERVICE"/> |
| 27 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#Ping" artifactTypeEnumId="AT_SERVICE"/> | 26 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#Ping" artifactTypeEnumId="AT_SERVICE"/> |
| 28 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.handle#McpRequest" artifactTypeEnumId="AT_SERVICE"/> | 27 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.handle#McpRequest" artifactTypeEnumId="AT_SERVICE"/> |
| ... | @@ -31,10 +30,8 @@ | ... | @@ -31,10 +30,8 @@ |
| 31 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ToolsCall" artifactTypeEnumId="AT_SERVICE"/> | 30 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ToolsCall" artifactTypeEnumId="AT_SERVICE"/> |
| 32 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesList" artifactTypeEnumId="AT_SERVICE"/> | 31 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesList" artifactTypeEnumId="AT_SERVICE"/> |
| 33 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesRead" artifactTypeEnumId="AT_SERVICE"/> | 32 | <moqui.security.ArtifactGroupMember artifactGroupId="McpServices" artifactName="McpServices.mcp#ResourcesRead" artifactTypeEnumId="AT_SERVICE"/> |
| 34 | <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc" artifactTypeEnumId="AT_REST_PATH"/> | 33 | <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp" artifactTypeEnumId="AT_REST_PATH"/> |
| 35 | <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/rpc/*" artifactTypeEnumId="AT_REST_PATH"/> | 34 | <moqui.security.ArtifactGroupMember artifactGroupId="McpRestPaths" artifactName="/mcp/*" artifactTypeEnumId="AT_REST_PATH"/> |
| 36 | <moqui.security.ArtifactGroupMember artifactGroupId="McpScreenTransitions" artifactName="component://moqui-mcp-2/screen/webroot/mcp.xml/rpc" artifactTypeEnumId="AT_XML_SCREEN_TRANS"/> | ||
| 37 | <moqui.security.ArtifactGroupMember artifactGroupId="McpScreenTransitions" artifactName="component://moqui-mcp-2/screen/webroot/mcp.xml" artifactTypeEnumId="AT_XML_SCREEN"/> | ||
| 38 | 35 | ||
| 39 | <!-- MCP Artifact Authz --> | 36 | <!-- MCP Artifact Authz --> |
| 40 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | 37 | <moqui.security.ArtifactAuthz userGroupId="McpUser" artifactGroupId="McpServices" authzTypeEnumId="AUTHZT_ALLOW" authzActionEnumId="AUTHZA_ALL"/> | ... | ... |
| ... | @@ -258,8 +258,8 @@ | ... | @@ -258,8 +258,8 @@ |
| 258 | def userAccountId = userId ? userId : null | 258 | def userAccountId = userId ? userId : null |
| 259 | 259 | ||
| 260 | // Get user-specific tools and resources | 260 | // Get user-specific tools and resources |
| 261 | def toolsResult = ec.service.sync().name("org.moqui.mcp.McpServices.mcp#ToolsList").parameters([:]).call() | 261 | def toolsResult = ec.service.sync().name("McpServices.mcp#ToolsList").parameters([:]).call() |
| 262 | def resourcesResult = ec.service.sync().name("org.moqui.mcp.McpServices.mcp#ResourcesList").parameters([:]).call() | 262 | def resourcesResult = ec.service.sync().name("McpServices.mcp#ResourcesList").parameters([:]).call() |
| 263 | 263 | ||
| 264 | // Build server capabilities based on what user can access | 264 | // Build server capabilities based on what user can access |
| 265 | def serverCapabilities = [ | 265 | def serverCapabilities = [ | ... | ... |
| ... | @@ -82,21 +82,42 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -82,21 +82,42 @@ class EnhancedMcpServlet extends HttpServlet { |
| 82 | 82 | ||
| 83 | ExecutionContextImpl ec = ecfi.getEci() | 83 | ExecutionContextImpl ec = ecfi.getEci() |
| 84 | 84 | ||
| 85 | try { | ||
| 86 | // Handle Basic Authentication directly without triggering screen system | ||
| 87 | String authzHeader = request.getHeader("Authorization") | ||
| 88 | boolean authenticated = false | ||
| 89 | |||
| 90 | if (authzHeader != null && authzHeader.length() > 6 && authzHeader.startsWith("Basic ")) { | ||
| 91 | String basicAuthEncoded = authzHeader.substring(6).trim() | ||
| 92 | String basicAuthAsString = new String(basicAuthEncoded.decodeBase64()) | ||
| 93 | int indexOfColon = basicAuthAsString.indexOf(":") | ||
| 94 | if (indexOfColon > 0) { | ||
| 95 | String username = basicAuthAsString.substring(0, indexOfColon) | ||
| 96 | String password = basicAuthAsString.substring(indexOfColon + 1) | ||
| 85 | try { | 97 | try { |
| 86 | // Initialize web facade for authentication | 98 | ec.user.loginUser(username, password) |
| 87 | ec.initWebFacade(webappName, request, response) | 99 | authenticated = true |
| 88 | 100 | logger.info("Enhanced MCP Basic auth successful for user: ${ec.user?.username}") | |
| 89 | logger.info("Enhanced MCP Request authenticated user: ${ec.user?.username}, userId: ${ec.user?.userId}") | ||
| 90 | |||
| 91 | // If no user authenticated, try to authenticate as admin for MCP requests | ||
| 92 | if (!ec.user?.userId) { | ||
| 93 | logger.info("No user authenticated, attempting admin login for Enhanced MCP") | ||
| 94 | try { | ||
| 95 | ec.user.loginUser("admin", "admin") | ||
| 96 | logger.info("Enhanced MCP Admin login successful, user: ${ec.user?.username}") | ||
| 97 | } catch (Exception e) { | 101 | } catch (Exception e) { |
| 98 | logger.warn("Enhanced MCP Admin login failed: ${e.message}") | 102 | logger.warn("Enhanced MCP Basic auth failed for user ${username}: ${e.message}") |
| 99 | } | 103 | } |
| 104 | } else { | ||
| 105 | logger.warn("Enhanced MCP got bad Basic auth credentials string") | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | // Check if user is authenticated | ||
| 110 | if (!authenticated || !ec.user?.userId) { | ||
| 111 | logger.warn("Enhanced MCP authentication failed - no valid user authenticated") | ||
| 112 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED) | ||
| 113 | response.setContentType("application/json") | ||
| 114 | response.setHeader("WWW-Authenticate", "Basic realm=\"Moqui MCP\"") | ||
| 115 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 116 | jsonrpc: "2.0", | ||
| 117 | error: [code: -32003, message: "Authentication required. Use Basic auth with valid Moqui credentials."], | ||
| 118 | id: null | ||
| 119 | ])) | ||
| 120 | return | ||
| 100 | } | 121 | } |
| 101 | 122 | ||
| 102 | // Route based on request method and path | 123 | // Route based on request method and path |
| ... | @@ -107,6 +128,12 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -107,6 +128,12 @@ class EnhancedMcpServlet extends HttpServlet { |
| 107 | handleSseConnection(request, response, ec) | 128 | handleSseConnection(request, response, ec) |
| 108 | } else if ("POST".equals(method) && requestURI.endsWith("/message")) { | 129 | } else if ("POST".equals(method) && requestURI.endsWith("/message")) { |
| 109 | handleMessage(request, response, ec) | 130 | handleMessage(request, response, ec) |
| 131 | } else if ("POST".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { | ||
| 132 | // Handle POST requests to /mcp for JSON-RPC | ||
| 133 | handleJsonRpc(request, response, ec) | ||
| 134 | } else if ("GET".equals(method) && (requestURI.equals("/mcp") || requestURI.endsWith("/mcp"))) { | ||
| 135 | // Handle GET requests to /mcp - maybe for server info or SSE fallback | ||
| 136 | handleSseConnection(request, response, ec) | ||
| 110 | } else { | 137 | } else { |
| 111 | // Fallback to JSON-RPC handling | 138 | // Fallback to JSON-RPC handling |
| 112 | handleJsonRpc(request, response, ec) | 139 | handleJsonRpc(request, response, ec) |
| ... | @@ -157,31 +184,42 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -157,31 +184,42 @@ class EnhancedMcpServlet extends HttpServlet { |
| 157 | 184 | ||
| 158 | logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") | 185 | logger.info("Handling Enhanced SSE connection from ${request.remoteAddr}") |
| 159 | 186 | ||
| 187 | // Enable async support for SSE | ||
| 188 | if (request.isAsyncSupported()) { | ||
| 189 | request.startAsync() | ||
| 190 | } | ||
| 191 | |||
| 160 | // Set SSE headers | 192 | // Set SSE headers |
| 161 | response.setContentType("text/event-stream") | 193 | response.setContentType("text/event-stream") |
| 162 | response.setCharacterEncoding("UTF-8") | 194 | response.setCharacterEncoding("UTF-8") |
| 163 | response.setHeader("Cache-Control", "no-cache") | 195 | response.setHeader("Cache-Control", "no-cache") |
| 164 | response.setHeader("Connection", "keep-alive") | 196 | response.setHeader("Connection", "keep-alive") |
| 165 | response.setHeader("Access-Control-Allow-Origin", "*") | 197 | response.setHeader("Access-Control-Allow-Origin", "*") |
| 198 | response.setHeader("X-Accel-Buffering", "no") // Disable nginx buffering | ||
| 166 | 199 | ||
| 167 | String sessionId = UUID.randomUUID().toString() | 200 | String sessionId = UUID.randomUUID().toString() |
| 168 | String visitId = ec.web?.visitId | 201 | String visitId = ec.user?.visitId |
| 169 | 202 | ||
| 170 | // Create Visit-based session transport | 203 | // Create Visit-based session transport |
| 171 | VisitBasedMcpSession session = new VisitBasedMcpSession(sessionId, visitId, response.writer, ec) | 204 | VisitBasedMcpSession session = new VisitBasedMcpSession(sessionId, visitId, response.writer, ec) |
| 172 | sessionManager.registerSession(session) | 205 | sessionManager.registerSession(session) |
| 173 | 206 | ||
| 174 | try { | 207 | try { |
| 175 | // Send initial connection event with endpoint info | 208 | // Send initial connection event |
| 176 | sendSseEvent(response.writer, "endpoint", "/mcp-sse/message?sessionId=" + sessionId) | 209 | def connectData = [ |
| 177 | 210 | type: "connected", | |
| 178 | // Send initial resources list | 211 | sessionId: sessionId, |
| 179 | def resourcesResult = processMcpMethod("resources/list", [:], ec) | 212 | timestamp: System.currentTimeMillis(), |
| 180 | sendSseEvent(response.writer, "resources", groovy.json.JsonOutput.toJson(resourcesResult)) | 213 | serverInfo: [ |
| 214 | name: "Moqui MCP SSE Server", | ||
| 215 | version: "2.0.0", | ||
| 216 | protocolVersion: "2025-06-18" | ||
| 217 | ] | ||
| 218 | ] | ||
| 219 | sendSseEvent(response.writer, "connect", groovy.json.JsonOutput.toJson(connectData), 0) | ||
| 181 | 220 | ||
| 182 | // Send initial tools list | 221 | // Send endpoint info for message posting |
| 183 | def toolsResult = processMcpMethod("tools/list", [:], ec) | 222 | sendSseEvent(response.writer, "endpoint", "/mcp/message?sessionId=" + sessionId, 1) |
| 184 | sendSseEvent(response.writer, "tools", groovy.json.JsonOutput.toJson(toolsResult)) | ||
| 185 | 223 | ||
| 186 | // Keep connection alive with periodic pings | 224 | // Keep connection alive with periodic pings |
| 187 | int pingCount = 0 | 225 | int pingCount = 0 |
| ... | @@ -189,30 +227,42 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -189,30 +227,42 @@ class EnhancedMcpServlet extends HttpServlet { |
| 189 | Thread.sleep(5000) // Wait 5 seconds | 227 | Thread.sleep(5000) // Wait 5 seconds |
| 190 | 228 | ||
| 191 | if (!response.isCommitted() && !sessionManager.isShuttingDown()) { | 229 | if (!response.isCommitted() && !sessionManager.isShuttingDown()) { |
| 192 | def pingMessage = new McpSchema.JSONRPCMessage([ | 230 | def pingData = [ |
| 193 | type: "ping", | 231 | type: "ping", |
| 194 | count: pingCount, | 232 | timestamp: System.currentTimeMillis(), |
| 195 | timestamp: System.currentTimeMillis() | 233 | connections: sessionManager.getActiveSessionCount() |
| 196 | ], null) | 234 | ] |
| 197 | session.sendMessage(pingMessage) | 235 | sendSseEvent(response.writer, "ping", groovy.json.JsonOutput.toJson(pingData), pingCount + 2) |
| 198 | pingCount++ | 236 | pingCount++ |
| 199 | } | 237 | } |
| 200 | } | 238 | } |
| 201 | 239 | ||
| 240 | } catch (InterruptedException e) { | ||
| 241 | logger.info("SSE connection interrupted for session ${sessionId}") | ||
| 242 | Thread.currentThread().interrupt() | ||
| 202 | } catch (Exception e) { | 243 | } catch (Exception e) { |
| 203 | logger.warn("Enhanced SSE connection interrupted: ${e.message}") | 244 | logger.warn("Enhanced SSE connection error: ${e.message}", e) |
| 204 | } finally { | 245 | } finally { |
| 205 | // Clean up session | 246 | // Clean up session |
| 206 | sessionManager.unregisterSession(sessionId) | 247 | sessionManager.unregisterSession(sessionId) |
| 207 | try { | 248 | try { |
| 208 | def closeMessage = new McpSchema.JSONRPCMessage([ | 249 | def closeData = [ |
| 209 | type: "disconnected", | 250 | type: "disconnected", |
| 210 | timestamp: System.currentTimeMillis() | 251 | timestamp: System.currentTimeMillis() |
| 211 | ], null) | 252 | ] |
| 212 | session.sendMessage(closeMessage) | 253 | sendSseEvent(response.writer, "disconnect", groovy.json.JsonOutput.toJson(closeData), -1) |
| 213 | } catch (Exception e) { | 254 | } catch (Exception e) { |
| 214 | // Ignore errors during cleanup | 255 | // Ignore errors during cleanup |
| 215 | } | 256 | } |
| 257 | |||
| 258 | // Complete async context if available | ||
| 259 | if (request.isAsyncStarted()) { | ||
| 260 | try { | ||
| 261 | request.getAsyncContext().complete() | ||
| 262 | } catch (Exception e) { | ||
| 263 | logger.debug("Error completing async context: ${e.message}") | ||
| 264 | } | ||
| 265 | } | ||
| 216 | } | 266 | } |
| 217 | } | 267 | } |
| 218 | 268 | ||
| ... | @@ -224,6 +274,19 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -224,6 +274,19 @@ class EnhancedMcpServlet extends HttpServlet { |
| 224 | return | 274 | return |
| 225 | } | 275 | } |
| 226 | 276 | ||
| 277 | // Get sessionId from request parameter or header | ||
| 278 | String sessionId = request.getParameter("sessionId") ?: request.getHeader("Mcp-Session-Id") | ||
| 279 | if (!sessionId) { | ||
| 280 | response.setContentType("application/json") | ||
| 281 | response.setCharacterEncoding("UTF-8") | ||
| 282 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | ||
| 283 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 284 | error: "Missing sessionId parameter or header", | ||
| 285 | activeSessions: sessionManager.getActiveSessionCount() | ||
| 286 | ])) | ||
| 287 | return | ||
| 288 | } | ||
| 289 | |||
| 227 | // Get session from session manager | 290 | // Get session from session manager |
| 228 | VisitBasedMcpSession session = sessionManager.getSession(sessionId) | 291 | VisitBasedMcpSession session = sessionManager.getSession(sessionId) |
| 229 | if (session == null) { | 292 | if (session == null) { |
| ... | @@ -239,15 +302,68 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -239,15 +302,68 @@ class EnhancedMcpServlet extends HttpServlet { |
| 239 | 302 | ||
| 240 | try { | 303 | try { |
| 241 | // Read request body | 304 | // Read request body |
| 242 | BufferedReader reader = request.getReader() | ||
| 243 | StringBuilder body = new StringBuilder() | 305 | StringBuilder body = new StringBuilder() |
| 306 | try { | ||
| 307 | BufferedReader reader = request.getReader() | ||
| 244 | String line | 308 | String line |
| 245 | while ((line = reader.readLine()) != null) { | 309 | while ((line = reader.readLine()) != null) { |
| 246 | body.append(line) | 310 | body.append(line) |
| 247 | } | 311 | } |
| 312 | } catch (IOException e) { | ||
| 313 | logger.error("Failed to read request body: ${e.message}") | ||
| 314 | response.setContentType("application/json") | ||
| 315 | response.setCharacterEncoding("UTF-8") | ||
| 316 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | ||
| 317 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 318 | jsonrpc: "2.0", | ||
| 319 | error: [code: -32700, message: "Failed to read request body: " + e.message], | ||
| 320 | id: null | ||
| 321 | ])) | ||
| 322 | return | ||
| 323 | } | ||
| 324 | |||
| 325 | String requestBody = body.toString() | ||
| 326 | if (!requestBody.trim()) { | ||
| 327 | response.setContentType("application/json") | ||
| 328 | response.setCharacterEncoding("UTF-8") | ||
| 329 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | ||
| 330 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 331 | jsonrpc: "2.0", | ||
| 332 | error: [code: -32602, message: "Empty request body"], | ||
| 333 | id: null | ||
| 334 | ])) | ||
| 335 | return | ||
| 336 | } | ||
| 248 | 337 | ||
| 249 | // Parse JSON-RPC message | 338 | // Parse JSON-RPC message |
| 250 | def rpcRequest = jsonSlurper.parseText(body.toString()) | 339 | def rpcRequest |
| 340 | try { | ||
| 341 | rpcRequest = jsonSlurper.parseText(requestBody) | ||
| 342 | } catch (Exception e) { | ||
| 343 | logger.error("Failed to parse JSON-RPC message: ${e.message}") | ||
| 344 | response.setContentType("application/json") | ||
| 345 | response.setCharacterEncoding("UTF-8") | ||
| 346 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | ||
| 347 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 348 | jsonrpc: "2.0", | ||
| 349 | error: [code: -32700, message: "Invalid JSON: " + e.message], | ||
| 350 | id: null | ||
| 351 | ])) | ||
| 352 | return | ||
| 353 | } | ||
| 354 | |||
| 355 | // Validate JSON-RPC 2.0 structure | ||
| 356 | if (!rpcRequest?.jsonrpc || rpcRequest.jsonrpc != "2.0" || !rpcRequest?.method) { | ||
| 357 | response.setContentType("application/json") | ||
| 358 | response.setCharacterEncoding("UTF-8") | ||
| 359 | response.setStatus(HttpServletResponse.SC_BAD_REQUEST) | ||
| 360 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 361 | jsonrpc: "2.0", | ||
| 362 | error: [code: -32600, message: "Invalid JSON-RPC 2.0 request"], | ||
| 363 | id: rpcRequest?.id ?: null | ||
| 364 | ])) | ||
| 365 | return | ||
| 366 | } | ||
| 251 | 367 | ||
| 252 | // Process the method | 368 | // Process the method |
| 253 | def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec) | 369 | def result = processMcpMethod(rpcRequest.method, rpcRequest.params, ec) |
| ... | @@ -256,15 +372,24 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -256,15 +372,24 @@ class EnhancedMcpServlet extends HttpServlet { |
| 256 | def responseMessage = new McpSchema.JSONRPCMessage(result, rpcRequest.id) | 372 | def responseMessage = new McpSchema.JSONRPCMessage(result, rpcRequest.id) |
| 257 | session.sendMessage(responseMessage) | 373 | session.sendMessage(responseMessage) |
| 258 | 374 | ||
| 375 | response.setContentType("application/json") | ||
| 376 | response.setCharacterEncoding("UTF-8") | ||
| 259 | response.setStatus(HttpServletResponse.SC_OK) | 377 | response.setStatus(HttpServletResponse.SC_OK) |
| 378 | response.writer.write(groovy.json.JsonOutput.toJson([ | ||
| 379 | jsonrpc: "2.0", | ||
| 380 | id: rpcRequest.id, | ||
| 381 | result: [status: "processed", sessionId: sessionId] | ||
| 382 | ])) | ||
| 260 | 383 | ||
| 261 | } catch (Exception e) { | 384 | } catch (Exception e) { |
| 262 | logger.error("Error processing message: ${e.message}") | 385 | logger.error("Error processing message for session ${sessionId}: ${e.message}", e) |
| 263 | response.setContentType("application/json") | 386 | response.setContentType("application/json") |
| 264 | response.setCharacterEncoding("UTF-8") | 387 | response.setCharacterEncoding("UTF-8") |
| 265 | response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) | 388 | response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR) |
| 266 | response.writer.write(groovy.json.JsonOutput.toJson([ | 389 | response.writer.write(groovy.json.JsonOutput.toJson([ |
| 267 | error: e.message | 390 | jsonrpc: "2.0", |
| 391 | error: [code: -32603, message: "Internal error: " + e.message], | ||
| 392 | id: null | ||
| 268 | ])) | 393 | ])) |
| 269 | } | 394 | } |
| 270 | } | 395 | } |
| ... | @@ -368,6 +493,8 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -368,6 +493,8 @@ class EnhancedMcpServlet extends HttpServlet { |
| 368 | 493 | ||
| 369 | private Map<String, Object> processMcpMethod(String method, Map params, ExecutionContextImpl ec) { | 494 | private Map<String, Object> processMcpMethod(String method, Map params, ExecutionContextImpl ec) { |
| 370 | logger.info("Enhanced METHOD: ${method} with params: ${params}") | 495 | logger.info("Enhanced METHOD: ${method} with params: ${params}") |
| 496 | |||
| 497 | try { | ||
| 371 | switch (method) { | 498 | switch (method) { |
| 372 | case "initialize": | 499 | case "initialize": |
| 373 | return callMcpService("mcp#Initialize", params, ec) | 500 | return callMcpService("mcp#Initialize", params, ec) |
| ... | @@ -381,8 +508,24 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -381,8 +508,24 @@ class EnhancedMcpServlet extends HttpServlet { |
| 381 | return callMcpService("mcp#ResourcesList", params, ec) | 508 | return callMcpService("mcp#ResourcesList", params, ec) |
| 382 | case "resources/read": | 509 | case "resources/read": |
| 383 | return callMcpService("mcp#ResourcesRead", params, ec) | 510 | return callMcpService("mcp#ResourcesRead", params, ec) |
| 511 | case "notifications/initialized": | ||
| 512 | // Handle notification initialization - return success for now | ||
| 513 | return [initialized: true] | ||
| 514 | case "notifications/send": | ||
| 515 | // Handle notification sending - return success for now | ||
| 516 | return [sent: true] | ||
| 517 | case "notifications/subscribe": | ||
| 518 | // Handle notification subscription - return success for now | ||
| 519 | return [subscribed: true] | ||
| 520 | case "notifications/unsubscribe": | ||
| 521 | // Handle notification unsubscription - return success for now | ||
| 522 | return [unsubscribed: true] | ||
| 384 | default: | 523 | default: |
| 385 | throw new IllegalArgumentException("Unknown MCP method: ${method}") | 524 | throw new IllegalArgumentException("Method not found: ${method}") |
| 525 | } | ||
| 526 | } catch (Exception e) { | ||
| 527 | logger.error("Error processing MCP method ${method}: ${e.message}", e) | ||
| 528 | throw e | ||
| 386 | } | 529 | } |
| 387 | } | 530 | } |
| 388 | 531 | ||
| ... | @@ -390,7 +533,7 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -390,7 +533,7 @@ class EnhancedMcpServlet extends HttpServlet { |
| 390 | logger.info("Enhanced Calling MCP service: ${serviceName} with params: ${params}") | 533 | logger.info("Enhanced Calling MCP service: ${serviceName} with params: ${params}") |
| 391 | 534 | ||
| 392 | try { | 535 | try { |
| 393 | def result = ec.service.sync().name("org.moqui.mcp.McpServices.${serviceName}") | 536 | def result = ec.service.sync().name("McpServices.${serviceName}") |
| 394 | .parameters(params ?: [:]) | 537 | .parameters(params ?: [:]) |
| 395 | .call() | 538 | .call() |
| 396 | 539 | ||
| ... | @@ -402,7 +545,11 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -402,7 +545,11 @@ class EnhancedMcpServlet extends HttpServlet { |
| 402 | } | 545 | } |
| 403 | } | 546 | } |
| 404 | 547 | ||
| 405 | private void sendSseEvent(PrintWriter writer, String eventType, String data) throws IOException { | 548 | private void sendSseEvent(PrintWriter writer, String eventType, String data, long eventId = -1) throws IOException { |
| 549 | try { | ||
| 550 | if (eventId >= 0) { | ||
| 551 | writer.write("id: " + eventId + "\n") | ||
| 552 | } | ||
| 406 | writer.write("event: " + eventType + "\n") | 553 | writer.write("event: " + eventType + "\n") |
| 407 | writer.write("data: " + data + "\n\n") | 554 | writer.write("data: " + data + "\n\n") |
| 408 | writer.flush() | 555 | writer.flush() |
| ... | @@ -410,6 +557,9 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -410,6 +557,9 @@ class EnhancedMcpServlet extends HttpServlet { |
| 410 | if (writer.checkError()) { | 557 | if (writer.checkError()) { |
| 411 | throw new IOException("Client disconnected") | 558 | throw new IOException("Client disconnected") |
| 412 | } | 559 | } |
| 560 | } catch (Exception e) { | ||
| 561 | throw new IOException("Failed to send SSE event: " + e.message, e) | ||
| 562 | } | ||
| 413 | } | 563 | } |
| 414 | 564 | ||
| 415 | // CORS handling based on MoquiServlet pattern | 565 | // CORS handling based on MoquiServlet pattern |
| ... | @@ -439,11 +589,6 @@ class EnhancedMcpServlet extends HttpServlet { | ... | @@ -439,11 +589,6 @@ class EnhancedMcpServlet extends HttpServlet { |
| 439 | 589 | ||
| 440 | super.destroy() | 590 | super.destroy() |
| 441 | } | 591 | } |
| 442 | } | ||
| 443 | sessions.clear() | ||
| 444 | |||
| 445 | super.destroy() | ||
| 446 | } | ||
| 447 | 592 | ||
| 448 | /** | 593 | /** |
| 449 | * Broadcast message to all active sessions | 594 | * Broadcast message to all active sessions | ... | ... |
| ... | @@ -22,7 +22,7 @@ import org.slf4j.Logger | ... | @@ -22,7 +22,7 @@ import org.slf4j.Logger |
| 22 | import org.slf4j.LoggerFactory | 22 | import org.slf4j.LoggerFactory |
| 23 | 23 | ||
| 24 | import javax.servlet.AsyncContext | 24 | import javax.servlet.AsyncContext |
| 25 | import javax.servlet.AsyncContextListener | 25 | import javax.servlet.AsyncListener |
| 26 | import javax.servlet.AsyncEvent | 26 | import javax.servlet.AsyncEvent |
| 27 | import javax.servlet.ServletConfig | 27 | import javax.servlet.ServletConfig |
| 28 | import javax.servlet.ServletException | 28 | import javax.servlet.ServletException |
| ... | @@ -205,7 +205,7 @@ class ServiceBasedMcpServlet extends HttpServlet { | ... | @@ -205,7 +205,7 @@ class ServiceBasedMcpServlet extends HttpServlet { |
| 205 | ]) | 205 | ]) |
| 206 | 206 | ||
| 207 | // Set up connection close handling | 207 | // Set up connection close handling |
| 208 | asyncContext.addListener(new AsyncContextListener() { | 208 | asyncContext.addListener(new AsyncListener() { |
| 209 | @Override | 209 | @Override |
| 210 | void onComplete(AsyncEvent event) throws IOException { | 210 | void onComplete(AsyncEvent event) throws IOException { |
| 211 | sseConnections.remove(sessionId) | 211 | sseConnections.remove(sessionId) | ... | ... |
web-sdk-services.xml
deleted
100644 → 0
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" | ||
| 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 4 | xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee | ||
| 5 | http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" | ||
| 6 | version="4.0"> | ||
| 7 | |||
| 8 | <!-- MCP SDK Services Servlet Configuration --> | ||
| 9 | <servlet> | ||
| 10 | <servlet-name>McpSdkServicesServlet</servlet-name> | ||
| 11 | <servlet-class>org.moqui.mcp.McpSdkServicesServlet</servlet-class> | ||
| 12 | |||
| 13 | <!-- Configuration parameters --> | ||
| 14 | <init-param> | ||
| 15 | <param-name>moqui-name</param-name> | ||
| 16 | <param-value>moqui-mcp-2</param-value> | ||
| 17 | </init-param> | ||
| 18 | |||
| 19 | <!-- Enable async support (required for SSE) --> | ||
| 20 | <async-supported>true</async-supported> | ||
| 21 | |||
| 22 | <!-- Load on startup to initialize MCP server --> | ||
| 23 | <load-on-startup>1</load-on-startup> | ||
| 24 | </servlet> | ||
| 25 | |||
| 26 | <!-- Servlet mappings for MCP SDK Services endpoints --> | ||
| 27 | <!-- The MCP SDK will handle both SSE and message endpoints through this servlet --> | ||
| 28 | <servlet-mapping> | ||
| 29 | <servlet-name>McpSdkServicesServlet</servlet-name> | ||
| 30 | <url-pattern>/mcp/*</url-pattern> | ||
| 31 | </servlet-mapping> | ||
| 32 | |||
| 33 | <!-- Session configuration --> | ||
| 34 | <session-config> | ||
| 35 | <session-timeout>30</session-timeout> | ||
| 36 | <cookie-config> | ||
| 37 | <http-only>true</http-only> | ||
| 38 | <secure>false</secure> | ||
| 39 | </cookie-config> | ||
| 40 | </session-config> | ||
| 41 | |||
| 42 | </web-app> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
web-sdk-sse.xml
deleted
100644 → 0
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" | ||
| 3 | xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| 4 | xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee | ||
| 5 | http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" | ||
| 6 | version="4.0"> | ||
| 7 | |||
| 8 | <!-- MCP SDK SSE Servlet Configuration --> | ||
| 9 | <servlet> | ||
| 10 | <servlet-name>McpSdkSseServlet</servlet-name> | ||
| 11 | <servlet-class>org.moqui.mcp.McpSdkSseServlet</servlet-class> | ||
| 12 | |||
| 13 | <!-- Configuration parameters --> | ||
| 14 | <init-param> | ||
| 15 | <param-name>moqui-name</param-name> | ||
| 16 | <param-value>moqui-mcp-2</param-value> | ||
| 17 | </init-param> | ||
| 18 | |||
| 19 | <!-- Enable async support (required for SSE) --> | ||
| 20 | <async-supported>true</async-supported> | ||
| 21 | |||
| 22 | <!-- Load on startup to initialize MCP server --> | ||
| 23 | <load-on-startup>1</load-on-startup> | ||
| 24 | </servlet> | ||
| 25 | |||
| 26 | <!-- Servlet mappings for MCP SDK SSE endpoints --> | ||
| 27 | <!-- The MCP SDK will handle both SSE and message endpoints through this servlet --> | ||
| 28 | <servlet-mapping> | ||
| 29 | <servlet-name>McpSdkSseServlet</servlet-name> | ||
| 30 | <url-pattern>/mcp/*</url-pattern> | ||
| 31 | </servlet-mapping> | ||
| 32 | |||
| 33 | <!-- Session configuration --> | ||
| 34 | <session-config> | ||
| 35 | <session-timeout>30</session-timeout> | ||
| 36 | <cookie-config> | ||
| 37 | <http-only>true</http-only> | ||
| 38 | <secure>false</secure> | ||
| 39 | </cookie-config> | ||
| 40 | </session-config> | ||
| 41 | |||
| 42 | </web-app> | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or sign in to post a comment