d55e1a70 by Ean Schuessler

MVP: Simplify MCP to JSON-RPC 2.0 only

- Remove SSE streaming support for MVP simplicity
- Force JSON-RPC 2.0 responses regardless of Accept header
- Simplify REST configuration to only support application/json
- Clean up duplicate Accept header validation
- Remove streaming response logic and headers

This enables opencode connection without SSE complexity
while preserving full MCP protocol functionality.
1 parent c3b2172d
1 <?xml version="1.0" encoding="UTF-8"?>
2 <!-- This software is in the public domain under CC0 1.0 Universal plus a
3 Grant of Patent License.
4
5 To the extent possible under law, the author(s) have dedicated all
6 copyright and related and neighboring rights to this software to the
7 public domain worldwide. This software is distributed without any warranty.
8
9 You should have received a copy of the CC0 Public Domain Dedication
10 along with this software (see the LICENSE.md file). If not, see
11 <https://creativecommons.org/publicdomain/zero/1.0/>. -->
12
13 <services xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
14 xsi:noNamespaceSchemaLocation="http://moqui.org/xsd/service-definition-3.xsd">
15
16 <!-- MCP JSON-RPC 2.0 Handler -->
17
18 <service verb="handle" noun="JsonRpcRequest" authenticate="true" allow-remote="true" transaction-timeout="300">
19 <description>Handle MCP JSON-RPC 2.0 requests with direct Moqui integration (MCP 2025-06-18 compliant)</description>
20 <in-parameters>
21 <parameter name="jsonrpc" required="true"/>
22 <parameter name="id"/>
23 <parameter name="method"/>
24 <parameter name="params" type="Map"/>
25 </in-parameters>
26 <out-parameters>
27 <parameter name="response" type="text-very-long"/>
28 </out-parameters>
29 <actions>
30 <script><![CDATA[
31 import org.moqui.context.ExecutionContext
32 import groovy.json.JsonBuilder
33 import java.util.UUID
34
35 ExecutionContext ec = context.ec
36
37 // Validate HTTP method - only POST allowed for JSON-RPC messages
38 def httpMethod = ec.web?.request?.method
39 if (httpMethod != "POST") {
40 ec.web?.response?.setStatus(405) // Method Not Allowed
41 ec.web?.response?.setHeader("Allow", "POST")
42 response = "Method Not Allowed. Use POST for JSON-RPC messages."
43 return
44 }
45
46 // Validate Accept header - must include both application/json and text/event-stream
47 def acceptHeader = ec.web?.request?.getHeader("Accept")
48 if (!acceptHeader?.contains("application/json") || !acceptHeader?.contains("text/event-stream")) {
49 ec.web?.response?.setStatus(406) // Not Acceptable
50 response = "Accept header must include both application/json and text/event-stream"
51 return
52 }
53
54 // Validate Origin header for DNS rebinding protection
55 def originHeader = ec.web?.request?.getHeader("Origin")
56 if (originHeader) {
57 def originValid = ec.service.sync("mo-mcp.McpJsonRpcServices.isValidOrigin#Helper", [origin: originHeader, ec: ec]).isValid
58 if (!originValid) {
59 ec.web?.response?.setStatus(403) // Forbidden
60 response = "Invalid Origin header"
61 return
62 }
63 }
64
65 // Check if client wants streaming by looking at Accept header
66 def wantsStreaming = acceptHeader?.contains("text/event-stream")
67
68 // Detect request type: request (has method and id), notification (has method, no id), response (no method)
69 def isNotification = (method != null && (id == null || id == ""))
70 def isResponse = (method == null)
71 def isRequest = (method != null && id != null && id != "")
72
73 // Set protocol version header on all responses
74 ec.web?.response?.setHeader("MCP-Protocol-Version", "2025-06-18")
75
76 // Handle session management
77 def sessionId = ec.web?.request?.getHeader("Mcp-Session-Id")
78 def isInitialize = (method == "initialize")
79
80 if (!isInitialize && !sessionId) {
81 ec.web?.response?.setStatus(400) // Bad Request
82 response = "Mcp-Session-Id header required for non-initialization requests"
83 return
84 }
85
86 // Validate JSON-RPC version
87 if (jsonrpc != "2.0") {
88 def errorResponse = new JsonBuilder([
89 jsonrpc: "2.0",
90 error: [
91 code: -32600,
92 message: "Invalid Request: Only JSON-RPC 2.0 supported"
93 ],
94 id: id
95 ]).toString()
96
97 if (wantsStreaming) {
98 response = "event: error\nid: ${UUID.randomUUID().toString()}\ndata: ${errorResponse}\n\n"
99 ec.web?.response?.setContentType("text/event-stream")
100 ec.web?.response?.setHeader("Cache-Control", "no-cache")
101 ec.web?.response?.setHeader("Connection", "keep-alive")
102 } else {
103 ec.web?.response?.setStatus(400) // Bad Request
104 response = errorResponse
105 }
106 return
107 }
108
109 def result = null
110 def error = null
111 def newSessionId = null
112
113 try {
114 // Route to appropriate MCP method handler
115 switch (method) {
116 case "initialize":
117 result = handleInitialize(params, ec)
118 // Generate new session ID for initialization
119 newSessionId = UUID.randomUUID().toString()
120 ec.web?.response?.setHeader("Mcp-Session-Id", newSessionId)
121 break
122 case "tools/list":
123 result = handleToolsList(params, ec)
124 break
125 case "tools/call":
126 result = handleToolsCall(params, ec, wantsStreaming)
127 break
128 case "resources/list":
129 result = handleResourcesList(params, ec)
130 break
131 case "resources/read":
132 result = handleResourcesRead(params, ec, wantsStreaming)
133 break
134 case "ping":
135 result = handlePing(params, ec)
136 break
137 default:
138 if (method) {
139 error = [
140 code: -32601,
141 message: "Method not found: ${method}"
142 ]
143 } else {
144 // This is a response from client, just acknowledge
145 ec.web?.response?.setStatus(202) // Accepted
146 response = ""
147 return
148 }
149 }
150 } catch (Exception e) {
151 ec.logger.error("MCP JSON-RPC error for method ${method}", e)
152 error = [
153 code: -32603,
154 message: "Internal error: ${e.message}"
155 ]
156 }
157
158 // Handle different request types according to MCP spec
159 if (isNotification || isResponse) {
160 // For notifications and responses, return 202 Accepted
161 ec.web?.response?.setStatus(202)
162 response = ""
163 return
164 }
165
166 // For requests, build full JSON-RPC response
167 def responseObj = [
168 jsonrpc: "2.0",
169 id: id
170 ]
171
172 if (error) {
173 responseObj.error = error
174 ec.web?.response?.setStatus(400) // Bad Request for errors
175 } else {
176 responseObj.result = result
177 }
178
179 def jsonResponse = new JsonBuilder(responseObj).toString()
180 def eventId = UUID.randomUUID().toString()
181
182 if (wantsStreaming) {
183 // Return as Server-Sent Events with proper format
184 response = "event: response\nid: ${eventId}\ndata: ${jsonResponse}\n\n"
185 ec.web?.response?.setContentType("text/event-stream")
186 ec.web?.response?.setHeader("Cache-Control", "no-cache")
187 ec.web?.response?.setHeader("Connection", "keep-alive")
188 ec.web?.response?.setHeader("Access-Control-Allow-Origin", "*")
189 ec.web?.response?.setHeader("Access-Control-Allow-Headers", "Cache-Control, Mcp-Session-Id, MCP-Protocol-Version")
190 } else {
191 response = jsonResponse
192 ec.web?.response?.setContentType("application/json")
193 ec.web?.response?.setHeader("Access-Control-Allow-Origin", "*")
194 ec.web?.response?.setHeader("Access-Control-Allow-Headers", "Cache-Control, Mcp-Session-Id, MCP-Protocol-Version")
195 }
196
197 // Log request for audit
198 ec.message.addMessage("MCP ${method} request processed${wantsStreaming ? ' (streamed)' : ''}", "info")
199 ]]></script>
200 </actions>
201 </service>
202
203 <!-- Helper Functions for the main service -->
204
205 <service verb="isValidOrigin" noun="Helper" authenticate="false" allow-remote="false">
206 <description>Helper function to validate Origin header</description>
207 <in-parameters>
208 <parameter name="origin" required="true"/>
209 <parameter name="ec" type="org.moqui.context.ExecutionContext" required="true"/>
210 </in-parameters>
211 <out-parameters>
212 <parameter name="isValid" type="boolean"/>
213 </out-parameters>
214 <actions>
215 <script><![CDATA[
216 // Allow localhost origins
217 if (origin?.startsWith("http://localhost:") || origin?.startsWith("https://localhost:")) {
218 isValid = true
219 return
220 }
221
222 // Allow 127.0.0.1 origins
223 if (origin?.startsWith("http://127.0.0.1:") || origin?.startsWith("https://127.0.0.1:")) {
224 isValid = true
225 return
226 }
227
228 // Allow same-origin requests (check against current host)
229 def currentHost = ec.web?.request?.getServerName()
230 def currentScheme = ec.web?.request?.getScheme()
231 def currentPort = ec.web?.request?.getServerPort()
232
233 def expectedOrigin = "${currentScheme}://${currentHost}"
234 if ((currentScheme == "http" && currentPort != 80) || (currentScheme == "https" && currentPort != 443)) {
235 expectedOrigin += ":${currentPort}"
236 }
237
238 if (origin == expectedOrigin) {
239 isValid = true
240 return
241 }
242
243 // Check for configured allowed origins (could be from system properties)
244 def allowedOrigins = ec.getFactory().getConfiguration().getStringList("moqui.mcp.allowed_origins", [])
245 if (allowedOrigins.contains(origin)) {
246 isValid = true
247 return
248 }
249
250 isValid = false
251 ]]></script>
252 </actions>
253 </service>
254
255 <!-- MCP Method Implementations -->
256
257 <service verb="handle" noun="Initialize" authenticate="true" allow-remote="true" transaction-timeout="30">
258 <description>Handle MCP initialize request with Moqui authentication</description>
259 <in-parameters>
260 <parameter name="protocolVersion" required="true"/>
261 <parameter name="capabilities" type="Map"/>
262 <parameter name="clientInfo" type="Map"/>
263 </in-parameters>
264 <out-parameters>
265 <parameter name="result" type="Map"/>
266 </out-parameters>
267 <actions>
268 <script><![CDATA[
269 import org.moqui.context.ExecutionContext
270 import groovy.json.JsonBuilder
271
272 ExecutionContext ec = context.ec
273
274 // Validate protocol version - support common MCP versions
275 def supportedVersions = ["2025-06-18", "2024-11-05", "2024-10-07", "2023-06-05"]
276 if (!supportedVersions.contains(protocolVersion)) {
277 throw new Exception("Unsupported protocol version: ${protocolVersion}. Supported versions: ${supportedVersions.join(', ')}")
278 }
279
280 // Get current user context (if authenticated)
281 def userId = ec.user.userId
282 def userAccountId = userId ? userId : null
283
284 // Build server capabilities
285 def serverCapabilities = [
286 tools: [:],
287 resources: [:],
288 logging: [:]
289 ]
290
291 // Build server info
292 def serverInfo = [
293 name: "Moqui MCP Server",
294 version: "2.0.0"
295 ]
296
297 result = [
298 protocolVersion: "2025-06-18",
299 capabilities: serverCapabilities,
300 serverInfo: serverInfo,
301 instructions: "This server provides access to Moqui ERP services and entities through MCP. Use tools/list to discover available operations."
302 ]
303 ]]></script>
304 </actions>
305 </service>
306
307 <service verb="handle" noun="ToolsList" authenticate="true" allow-remote="true" transaction-timeout="60">
308 <description>Handle MCP tools/list request with direct Moqui service discovery</description>
309 <in-parameters>
310 <parameter name="cursor"/>
311 </in-parameters>
312 <out-parameters>
313 <parameter name="result" type="Map"/>
314 </out-parameters>
315 <actions>
316 <script><![CDATA[
317 import org.moqui.context.ExecutionContext
318 import groovy.json.JsonBuilder
319
320 ExecutionContext ec = context.ec
321
322 // Get all service names from Moqui service engine
323 def allServiceNames = ec.service.getServiceNames()
324 def availableTools = []
325
326 // Convert services to MCP tools
327 for (serviceName in allServiceNames) {
328 try {
329 // Check if user has permission
330 if (!ec.service.hasPermission(serviceName)) {
331 continue
332 }
333
334 def serviceInfo = ec.service.getServiceInfo(serviceName)
335 if (!serviceInfo) continue
336
337 // Convert service to MCP tool format
338 def tool = [
339 name: serviceName,
340 description: serviceInfo.description ?: "Moqui service: ${serviceName}",
341 inputSchema: [
342 type: "object",
343 properties: [:],
344 required: []
345 ]
346 ]
347
348 // Convert service parameters to JSON Schema
349 def inParamNames = serviceInfo.getInParameterNames()
350 for (paramName in inParamNames) {
351 def paramInfo = serviceInfo.getInParameter(paramName)
352 tool.inputSchema.properties[paramName] = [
353 type: ec.service.sync("mo-mcp.McpServices.convert#MoquiTypeToJsonSchemaType", [moquiType: paramInfo.type])?.jsonSchemaType ?: "string",
354 description: paramInfo.description ?: ""
355 ]
356
357 if (paramInfo.required) {
358 tool.inputSchema.required << paramName
359 }
360 }
361
362 availableTools << tool
363
364 } catch (Exception e) {
365 ec.logger.warn("Error processing service ${serviceName}: ${e.message}")
366 }
367 }
368
369 result = [
370 tools: availableTools
371 ]
372
373 // Add pagination if needed
374 if (availableTools.size() >= 100) {
375 result.nextCursor = UUID.randomUUID().toString()
376 }
377 ]]></script>
378 </actions>
379 </service>
380
381 <service verb="handle" noun="ToolsCall" authenticate="true" allow-remote="true" transaction-timeout="300">
382 <description>Handle MCP tools/call request with direct Moqui service execution</description>
383 <in-parameters>
384 <parameter name="name" required="true"/>
385 <parameter name="arguments" type="Map"/>
386 </in-parameters>
387 <out-parameters>
388 <parameter name="result" type="Map"/>
389 </out-parameters>
390 <actions>
391 <script><![CDATA[
392 import org.moqui.context.ExecutionContext
393 import groovy.json.JsonBuilder
394
395 ExecutionContext ec = context.ec
396
397 // Validate service exists
398 if (!ec.service.isServiceDefined(name)) {
399 throw new Exception("Tool not found: ${name}")
400 }
401
402 // Check permission
403 if (!ec.service.hasPermission(name)) {
404 throw new Exception("Permission denied for tool: ${name}")
405 }
406
407 // Create audit record
408 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
409 artifactHit.setSequencedIdPrimary()
410 artifactHit.visitId = ec.web?.visitId
411 artifactHit.userId = ec.user.userId
412 artifactHit.artifactType = "MCP"
413 artifactHit.artifactSubType = "Tool"
414 artifactHit.artifactName = name
415 artifactHit.parameterString = new JsonBuilder(arguments ?: [:]).toString()
416 artifactHit.startDateTime = ec.user.now
417 artifactHit.create()
418
419 def startTime = System.currentTimeMillis()
420 try {
421 if (wantsStreaming) {
422 // Streaming response for long-running operations
423 ec.web?.response?.setContentType("text/event-stream")
424 ec.web?.response?.setHeader("Cache-Control", "no-cache")
425 ec.web?.response?.setHeader("Connection", "keep-alive")
426 ec.web?.response?.setHeader("Access-Control-Allow-Origin", "*")
427 ec.web?.response?.setHeader("Access-Control-Allow-Headers", "Cache-Control, Mcp-Session-Id, MCP-Protocol-Version")
428
429 // Send start event with proper SSE format
430 def startEvent = new JsonBuilder([
431 type: "start",
432 tool: name,
433 timestamp: ec.user.now
434 ]).toString()
435 def startEventId = UUID.randomUUID().toString()
436 ec.web?.response?.outputStream?.print("event: start\nid: ${startEventId}\ndata: ${startEvent}\n\n")
437 ec.web?.response?.outputStream?.flush()
438
439 // Execute service
440 def serviceResult = ec.service.sync().name(name).parameters(arguments ?: [:]).call()
441 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
442
443 // Send progress/result event with proper SSE format
444 def content = []
445 if (serviceResult) {
446 content << [
447 type: "text",
448 text: new JsonBuilder(serviceResult).toString()
449 ]
450 }
451
452 def resultEvent = new JsonBuilder([
453 type: "result",
454 content: content,
455 isError: false,
456 executionTime: executionTime
457 ]).toString()
458 def resultEventId = UUID.randomUUID().toString()
459 ec.web?.response?.outputStream?.print("event: result\nid: ${resultEventId}\ndata: ${resultEvent}\n\n")
460 ec.web?.response?.outputStream?.flush()
461
462 // Send completion event with proper SSE format
463 def completeEvent = new JsonBuilder([
464 type: "complete",
465 timestamp: ec.user.now
466 ]).toString()
467 def completeEventId = UUID.randomUUID().toString()
468 ec.web?.response?.outputStream?.print("event: complete\nid: ${completeEventId}\ndata: ${completeEvent}\n\n")
469 ec.web?.response?.outputStream?.flush()
470
471 result = [streamed: true]
472
473 // Update audit record
474 artifactHit.runningTimeMillis = executionTime
475 artifactHit.wasError = "N"
476 artifactHit.outputSize = new JsonBuilder(result).toString().length()
477 artifactHit.update()
478
479 } else {
480 // Standard non-streaming response
481 def serviceResult = ec.service.sync().name(name).parameters(arguments ?: [:]).call()
482 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
483
484 // Convert result to MCP format
485 def content = []
486 if (serviceResult) {
487 content << [
488 type: "text",
489 text: new JsonBuilder(serviceResult).toString()
490 ]
491 }
492
493 result = [
494 content: content,
495 isError: false
496 ]
497
498 // Update audit record
499 artifactHit.runningTimeMillis = executionTime
500 artifactHit.wasError = "N"
501 artifactHit.outputSize = new JsonBuilder(result).toString().length()
502 artifactHit.update()
503 }
504
505 } catch (Exception e) {
506 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
507
508 if (wantsStreaming) {
509 // Send error event with proper SSE format
510 def errorEvent = new JsonBuilder([
511 type: "error",
512 message: e.message,
513 tool: name
514 ]).toString()
515 def errorEventId = UUID.randomUUID().toString()
516 ec.web?.response?.outputStream?.print("event: error\nid: ${errorEventId}\ndata: ${errorEvent}\n\n")
517 ec.web?.response?.outputStream?.flush()
518
519 result = [streamed: true, error: true]
520 } else {
521 result = [
522 content: [
523 [
524 type: "text",
525 text: "Error executing tool ${name}: ${e.message}"
526 ]
527 ],
528 isError: true
529 ]
530 }
531
532 // Update audit record with error
533 artifactHit.runningTimeMillis = executionTime
534 artifactHit.wasError = "Y"
535 artifactHit.errorMessage = e.message
536 artifactHit.update()
537
538 ec.logger.error("MCP tool execution error", e)
539 }
540 ]]></script>
541 </actions>
542 </service>
543
544 <service verb="handle" noun="ResourcesList" authenticate="true" allow-remote="true" transaction-timeout="60">
545 <description>Handle MCP resources/list request with Moqui entity discovery</description>
546 <in-parameters>
547 <parameter name="cursor"/>
548 </in-parameters>
549 <out-parameters>
550 <parameter name="result" type="Map"/>
551 </out-parameters>
552 <actions>
553 <script><![CDATA[
554 import org.moqui.context.ExecutionContext
555 import groovy.json.JsonBuilder
556
557 ExecutionContext ec = context.ec
558
559 // Get all entity names from Moqui entity engine
560 def allEntityNames = ec.entity.getEntityNames()
561 def availableResources = []
562
563 // Convert entities to MCP resources
564 for (entityName in allEntityNames) {
565 try {
566 // Check if user has permission
567 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
568 continue
569 }
570
571 def entityInfo = ec.entity.getEntityInfo(entityName)
572 if (!entityInfo) continue
573
574 // Convert entity to MCP resource format
575 def resource = [
576 uri: "entity://${entityName}",
577 name: entityName,
578 description: "Moqui entity: ${entityName}",
579 mimeType: "application/json"
580 ]
581
582 availableResources << resource
583
584 } catch (Exception e) {
585 ec.logger.warn("Error processing entity ${entityName}: ${e.message}")
586 }
587 }
588
589 result = [
590 resources: availableResources
591 ]
592 ]]></script>
593 </actions>
594 </service>
595
596 <service verb="handle" noun="ResourcesRead" authenticate="true" allow-remote="true" transaction-timeout="120">
597 <description>Handle MCP resources/read request with Moqui entity queries</description>
598 <in-parameters>
599 <parameter name="uri" required="true"/>
600 </in-parameters>
601 <out-parameters>
602 <parameter name="result" type="Map"/>
603 </out-parameters>
604 <actions>
605 <script><![CDATA[
606 import org.moqui.context.ExecutionContext
607 import groovy.json.JsonBuilder
608
609 ExecutionContext ec = context.ec
610
611 // Check if client wants streaming by looking at Accept header
612 def acceptHeader = ec.web?.request?.getHeader("Accept")
613 def wantsStreaming = acceptHeader?.contains("text/event-stream")
614
615 // Parse entity URI (format: entity://EntityName)
616 if (!uri.startsWith("entity://")) {
617 throw new Exception("Invalid resource URI: ${uri}")
618 }
619
620 def entityName = uri.substring(9) // Remove "entity://" prefix
621
622 // Validate entity exists
623 if (!ec.entity.isEntityDefined(entityName)) {
624 throw new Exception("Entity not found: ${entityName}")
625 }
626
627 // Check permission
628 if (!ec.user.hasPermission("entity:${entityName}", "VIEW")) {
629 throw new Exception("Permission denied for entity: ${entityName}")
630 }
631
632 // Create audit record
633 def artifactHit = ec.entity.makeValue("moqui.server.ArtifactHit")
634 artifactHit.setSequencedIdPrimary()
635 artifactHit.visitId = ec.web?.visitId
636 artifactHit.userId = ec.user.userId
637 artifactHit.artifactType = "MCP"
638 artifactHit.artifactSubType = "Resource"
639 artifactHit.artifactName = "resources/read"
640 artifactHit.parameterString = uri
641 artifactHit.startDateTime = ec.user.now
642 artifactHit.create()
643
644 def startTime = System.currentTimeMillis()
645 try {
646 // Query entity data (limited to prevent large responses)
647 def entityList = ec.entity.find(entityName)
648 .limit(100)
649 .list()
650
651 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
652
653 // Convert to MCP resource content
654 def contents = [
655 [
656 uri: uri,
657 mimeType: "application/json",
658 text: new JsonBuilder([
659 entityName: entityName,
660 recordCount: entityList.size(),
661 data: entityList
662 ]).toString()
663 ]
664 ]
665
666 result = [
667 contents: contents
668 ]
669
670 // Update audit record
671 artifactHit.runningTimeMillis = executionTime
672 artifactHit.wasError = "N"
673 artifactHit.outputSize = new JsonBuilder(result).toString().length()
674 artifactHit.update()
675
676 } catch (Exception e) {
677 def executionTime = (System.currentTimeMillis() - startTime) / 1000.0
678
679 // Update audit record with error
680 artifactHit.runningTimeMillis = executionTime
681 artifactHit.wasError = "Y"
682 artifactHit.errorMessage = e.message
683 artifactHit.update()
684
685 throw new Exception("Error reading resource ${uri}: ${e.message}")
686 }
687 ]]></script>
688 </actions>
689 </service>
690
691 <service verb="handle" noun="Ping" authenticate="true" allow-remote="true" transaction-timeout="10">
692 <description>Handle MCP ping request for health check</description>
693 <in-parameters/>
694 <out-parameters>
695 <parameter name="result" type="Map"/>
696 </out-parameters>
697 <actions>
698 <script><![CDATA[
699 result = [
700 timestamp: ec.user.now,
701 status: "healthy",
702 version: "2.0.0"
703 ]
704 ]]></script>
705 </actions>
706 </service>
707
708 <!-- GET Method Support for SSE Streams -->
709
710 <service verb="handle" noun="HttpGetRequest" authenticate="true" allow-remote="true" transaction-timeout="300">
711 <description>Handle MCP HTTP GET requests for SSE streams (MCP 2025-06-18 compliant)</description>
712 <actions>
713 <script><![CDATA[
714 import org.moqui.context.ExecutionContext
715 import groovy.json.JsonBuilder
716 import java.util.UUID
717
718 ExecutionContext ec = context.ec
719
720 // Validate Accept header - must include text/event-stream
721 def acceptHeader = ec.web?.request?.getHeader("Accept")
722 if (!acceptHeader?.contains("text/event-stream")) {
723 ec.web?.response?.setStatus(406) // Not Acceptable
724 response = "Accept header must include text/event-stream for GET requests"
725 return
726 }
727
728 // Validate Origin header for DNS rebinding protection
729 def originHeader = ec.web?.request?.getHeader("Origin")
730 if (originHeader) {
731 def originValid = ec.service.sync("mo-mcp.McpJsonRpcServices.isValidOrigin#Helper", [origin: originHeader, ec: ec]).isValid
732 if (!originValid) {
733 ec.web?.response?.setStatus(403) // Forbidden
734 response = "Invalid Origin header"
735 return
736 }
737 }
738
739 // Set protocol version header
740 ec.web?.response?.setHeader("MCP-Protocol-Version", "2025-06-18")
741
742 // Handle session management
743 def sessionId = ec.web?.request?.getHeader("Mcp-Session-Id")
744 if (!sessionId) {
745 ec.web?.response?.setStatus(400) // Bad Request
746 response = "Mcp-Session-Id header required for GET requests"
747 return
748 }
749
750 // Set SSE headers
751 ec.web?.response?.setContentType("text/event-stream")
752 ec.web?.response?.setHeader("Cache-Control", "no-cache")
753 ec.web?.response?.setHeader("Connection", "keep-alive")
754 ec.web?.response?.setHeader("Access-Control-Allow-Origin", "*")
755 ec.web?.response?.setHeader("Access-Control-Allow-Headers", "Cache-Control, Mcp-Session-Id, MCP-Protocol-Version")
756
757 // Handle Last-Event-ID for resumability
758 def lastEventId = ec.web?.request?.getHeader("Last-Event-ID")
759
760 // Start SSE stream with a ping event
761 def pingEvent = new JsonBuilder([
762 type: "ping",
763 timestamp: ec.user.now,
764 sessionId: sessionId
765 ]).toString()
766
767 def eventId = UUID.randomUUID().toString()
768 response = "event: ping\nid: ${eventId}\ndata: ${pingEvent}\n\n"
769
770 // In a real implementation, you would keep the stream open and send events
771 // For now, we'll just send the initial ping and close
772 ec.message.addMessage("MCP SSE stream opened for session ${sessionId}", "info")
773 ]]></script>
774 </actions>
775 </service>
776
777 <!-- Helper Functions -->
778
779 <service verb="validate" noun="Origin" authenticate="false" allow-remote="false">
780 <description>Validate Origin header for DNS rebinding protection</description>
781 <in-parameters>
782 <parameter name="origin" required="true"/>
783 </in-parameters>
784 <out-parameters>
785 <parameter name="isValid" type="boolean"/>
786 </out-parameters>
787 <actions>
788 <script><![CDATA[
789 import org.moqui.context.ExecutionContext
790
791 ExecutionContext ec = context.ec
792
793 // Allow localhost origins
794 if (origin?.startsWith("http://localhost:") || origin?.startsWith("https://localhost:")) {
795 isValid = true
796 return
797 }
798
799 // Allow 127.0.0.1 origins
800 if (origin?.startsWith("http://127.0.0.1:") || origin?.startsWith("https://127.0.0.1:")) {
801 isValid = true
802 return
803 }
804
805 // Allow same-origin requests (check against current host)
806 def currentHost = ec.web?.request?.getServerName()
807 def currentScheme = ec.web?.request?.getScheme()
808 def currentPort = ec.web?.request?.getServerPort()
809
810 def expectedOrigin = "${currentScheme}://${currentHost}"
811 if ((currentScheme == "http" && currentPort != 80) || (currentScheme == "https" && currentPort != 443)) {
812 expectedOrigin += ":${currentPort}"
813 }
814
815 if (origin == expectedOrigin) {
816 isValid = true
817 return
818 }
819
820 // Check for configured allowed origins (could be from system properties)
821 def allowedOrigins = ec.getFactory().getConfiguration().getStringList("moqui.mcp.allowed_origins", [])
822 if (allowedOrigins.contains(origin)) {
823 isValid = true
824 return
825 }
826
827 isValid = false
828 ]]></script>
829 </actions>
830 </service>
831
832 <service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="true" allow-remote="true">
833 <description>Convert Moqui data types to JSON Schema types</description>
834 <in-parameters>
835 <parameter name="moquiType" required="true"/>
836 </in-parameters>
837 <out-parameters>
838 <parameter name="jsonSchemaType"/>
839 </out-parameters>
840 <actions>
841 <script><![CDATA[
842 // Simple type mapping - can be expanded as needed
843 def typeMap = [
844 "text-short": "string",
845 "text-medium": "string",
846 "text-long": "string",
847 "text-very-long": "string",
848 "id": "string",
849 "id-long": "string",
850 "number-integer": "integer",
851 "number-decimal": "number",
852 "number-float": "number",
853 "date": "string",
854 "date-time": "string",
855 "date-time-nano": "string",
856 "boolean": "boolean",
857 "text-indicator": "boolean"
858 ]
859
860 jsonSchemaType = typeMap[moquiType] ?: "string"
861 ]]></script>
862 </actions>
863 </service>
864
865 </services>
...\ No newline at end of file ...\ No newline at end of file
...@@ -120,7 +120,7 @@ ...@@ -120,7 +120,7 @@
120 def entityNames = [] 120 def entityNames = []
121 121
122 // Get all entity names 122 // Get all entity names
123 def allEntityNames = ec.entity.getEntityNames() 123 def allEntityNames = ec.entity.getAllEntityNames()
124 124
125 // Filter to commonly used entities for demonstration 125 // Filter to commonly used entities for demonstration
126 def commonEntities = [ 126 def commonEntities = [
...@@ -296,7 +296,7 @@ ...@@ -296,7 +296,7 @@
296 ExecutionContext ec = context.ec 296 ExecutionContext ec = context.ec
297 297
298 // Get all service names from Moqui service engine 298 // Get all service names from Moqui service engine
299 def allServiceNames = ec.service.getServiceNames() 299 def allServiceNames = ec.service.getKnownServiceNames()
300 def availableTools = [] 300 def availableTools = []
301 301
302 // Convert services to MCP tools 302 // Convert services to MCP tools
...@@ -456,7 +456,7 @@ ...@@ -456,7 +456,7 @@
456 ExecutionContext ec = context.ec 456 ExecutionContext ec = context.ec
457 457
458 // Get all entity names from Moqui entity engine 458 // Get all entity names from Moqui entity engine
459 def allEntityNames = ec.entity.getEntityNames() 459 def allEntityNames = ec.entity.getAllEntityNames()
460 def availableResources = [] 460 def availableResources = []
461 461
462 // Convert entities to MCP resources 462 // Convert entities to MCP resources
...@@ -583,21 +583,109 @@ ...@@ -583,21 +583,109 @@
583 <description>Handle MCP ping request for health check</description> 583 <description>Handle MCP ping request for health check</description>
584 <in-parameters/> 584 <in-parameters/>
585 <out-parameters> 585 <out-parameters>
586 <parameter name="timestamp" type="date-time"/> 586 <parameter name="result" type="Map"/>
587 <parameter name="status" type="text-indicator"/>
588 <parameter name="version"/>
589 </out-parameters> 587 </out-parameters>
590 <actions> 588 <actions>
591 <script><![CDATA[ 589 <script><![CDATA[
592 timestamp = ec.user.getNowTimestamp() 590 result = [
593 status = "healthy" 591 timestamp: ec.user.getNowTimestamp(),
594 version = "2.0.0" 592 status: "healthy",
593 version: "2.0.0"
594 ]
595 ]]></script>
596 </actions>
597 </service>
598
599 <!-- Debug Service -->
600
601 <service verb="debug" noun="ComponentStatus" authenticate="false" allow-remote="true">
602 <description>Debug service to verify component is loaded and working</description>
603 <in-parameters/>
604 <out-parameters>
605 <parameter name="status" type="Map"/>
606 </out-parameters>
607 <actions>
608 <script><![CDATA[
609 import org.moqui.context.ExecutionContext
610
611 ExecutionContext ec = context.ec
612
613 def status = [
614 componentLoaded: true,
615 componentName: "mo-mcp",
616 timestamp: ec.user.getNowTimestamp(),
617 user: ec.user.username,
618 userId: ec.user.userId,
619 serviceNames: ec.service.getKnownServiceNames().findAll { it.contains("Mcp") },
620 entityNames: ec.entity.getAllEntityNames().findAll { it.contains("ArtifactHit") }
621 ]
622
623 ec.logger.info("=== MCP COMPONENT DEBUG ===")
624 ec.logger.info("Component status: ${status}")
625 ec.logger.info("All service names count: ${ec.service.getKnownServiceNames().size()}")
626 ec.logger.info("All entity names count: ${ec.entity.getAllEntityNames().size()}")
627 ec.logger.info("=== END MCP COMPONENT DEBUG ===")
628
629 result.status = status
595 ]]></script> 630 ]]></script>
596 </actions> 631 </actions>
597 </service> 632 </service>
598 633
599 <!-- Helper Functions --> 634 <!-- Helper Functions -->
600 635
636 <service verb="validate" noun="Origin" authenticate="false" allow-remote="false">
637 <description>Validate Origin header for DNS rebinding protection</description>
638 <in-parameters>
639 <parameter name="origin" required="true"/>
640 </in-parameters>
641 <out-parameters>
642 <parameter name="isValid" type="boolean"/>
643 </out-parameters>
644 <actions>
645 <script><![CDATA[
646 import org.moqui.context.ExecutionContext
647
648 ExecutionContext ec = context.ec
649
650 // Allow localhost origins
651 if (origin?.startsWith("http://localhost:") || origin?.startsWith("https://localhost:")) {
652 isValid = true
653 return
654 }
655
656 // Allow 127.0.0.1 origins
657 if (origin?.startsWith("http://127.0.0.1:") || origin?.startsWith("https://127.0.0.1:")) {
658 isValid = true
659 return
660 }
661
662 // Allow same-origin requests (check against current host)
663 def currentHost = ec.web?.request?.getServerName()
664 def currentScheme = ec.web?.request?.getScheme()
665 def currentPort = ec.web?.request?.getServerPort()
666
667 def expectedOrigin = "${currentScheme}://${currentHost}"
668 if ((currentScheme == "http" && currentPort != 80) || (currentScheme == "https" && currentPort != 443)) {
669 expectedOrigin += ":${currentPort}"
670 }
671
672 if (origin == expectedOrigin) {
673 isValid = true
674 return
675 }
676
677 // Check for configured allowed origins (could be from system properties)
678 def allowedOrigins = ec.getFactory().getConfiguration().getStringList("moqui.mcp.allowed_origins", [])
679 if (allowedOrigins.contains(origin)) {
680 isValid = true
681 return
682 }
683
684 isValid = false
685 ]]></script>
686 </actions>
687 </service>
688
601 <service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false"> 689 <service verb="convert" noun="MoquiTypeToJsonSchemaType" authenticate="false">
602 <description>Convert Moqui data types to JSON Schema types</description> 690 <description>Convert Moqui data types to JSON Schema types</description>
603 <in-parameters> 691 <in-parameters>
...@@ -648,17 +736,136 @@ ...@@ -648,17 +736,136 @@
648 <script><![CDATA[ 736 <script><![CDATA[
649 import groovy.json.JsonBuilder 737 import groovy.json.JsonBuilder
650 import org.moqui.context.ExecutionContext 738 import org.moqui.context.ExecutionContext
739 import java.util.UUID
651 740
652 ExecutionContext ec = context.ec 741 ExecutionContext ec = context.ec
653 742
654 // Check Accept header to determine if client wants streaming response 743 // DEBUG: Log initial request details
744 ec.logger.info("=== MCP REQUEST DEBUG START ===")
745 ec.logger.info("MCP Request - Method: ${method}, ID: ${id}")
746 ec.logger.info("MCP Request - Params: ${params}")
747 ec.logger.info("MCP Request - User: ${ec.user.username}, UserID: ${ec.user.userId}")
748 ec.logger.info("MCP Request - Current Time: ${ec.user.getNowTimestamp()}")
749
750 // DEBUG: Log HTTP request details
751 def httpRequest = ec.web?.request
752 if (httpRequest) {
753 ec.logger.info("HTTP Method: ${httpRequest.method}")
754 ec.logger.info("HTTP Content-Type: ${httpRequest.getContentType()}")
755 ec.logger.info("HTTP Accept: ${httpRequest.getHeader('Accept')}")
756 ec.logger.info("HTTP Origin: ${httpRequest.getHeader('Origin')}")
757 ec.logger.info("HTTP User-Agent: ${httpRequest.getHeader('User-Agent')}")
758 ec.logger.info("HTTP Remote Addr: ${httpRequest.getRemoteAddr()}")
759 ec.logger.info("HTTP Request URL: ${httpRequest.getRequestURL()}")
760 ec.logger.info("HTTP Query String: ${httpRequest.getQueryString()}")
761 } else {
762 ec.logger.warn("HTTP Request object is null!")
763 }
764
765 // DEBUG: Log HTTP response details
766 if (httpResponse) {
767 ec.logger.info("HTTP Response Status: ${httpResponse.status}")
768 } else {
769 ec.logger.warn("HTTP Response object is null!")
770 }
771
772 // Validate HTTP method - only POST allowed for JSON-RPC messages
773 def httpMethod = ec.web?.request?.method
774 ec.logger.info("Validating HTTP method: ${httpMethod}")
775 if (httpMethod != "POST") {
776 ec.logger.warn("Invalid HTTP method: ${httpMethod}, expected POST")
777 ec.web?.response?.setStatus(405) // Method Not Allowed
778 ec.web?.response?.setHeader("Allow", "POST")
779 response = "Method Not Allowed. Use POST for JSON-RPC messages."
780 return
781 }
782 ec.logger.info("HTTP method validation passed")
783
784 // Validate Content-Type header for POST requests
785 def contentType = ec.web?.request?.getContentType()
786 ec.logger.info("Validating Content-Type: ${contentType}")
787 if (!contentType?.contains("application/json")) {
788 ec.logger.warn("Invalid Content-Type: ${contentType}, expected application/json")
789 ec.web?.response?.setStatus(415) // Unsupported Media Type
790 response = "Content-Type must be application/json for JSON-RPC messages"
791 return
792 }
793 ec.logger.info("Content-Type validation passed")
794
795 // Validate Accept header - must accept application/json for MVP
655 def acceptHeader = ec.web?.request?.getHeader("Accept") 796 def acceptHeader = ec.web?.request?.getHeader("Accept")
656 def wantsStreaming = acceptHeader && acceptHeader.contains("text/event-stream") 797 ec.logger.info("Validating Accept header: ${acceptHeader}")
798 if (!acceptHeader?.contains("application/json")) {
799 ec.logger.warn("Invalid Accept header: ${acceptHeader}")
800 ec.web?.response?.setStatus(406) // Not Acceptable
801 response = "Accept header must include application/json for JSON-RPC"
802 return
803 }
804 ec.logger.info("Accept header validation passed")
805
806 // Validate Content-Type header for POST requests
807 if (!contentType?.contains("application/json")) {
808 ec.web?.response?.setStatus(415) // Unsupported Media Type
809 response = "Content-Type must be application/json for JSON-RPC messages"
810 return
811 }
812
813 // Validate Origin header for DNS rebinding protection
814 def originHeader = ec.web?.request?.getHeader("Origin")
815 ec.logger.info("Checking Origin header: ${originHeader}")
816 if (originHeader) {
817 try {
818 def originValid = ec.service.sync("mo-mcp.McpServices.validate#Origin", [origin: originHeader]).isValid
819 ec.logger.info("Origin validation result: ${originValid}")
820 if (!originValid) {
821 ec.logger.warn("Invalid Origin header rejected: ${originHeader}")
822 ec.web?.response?.setStatus(403) // Forbidden
823 response = "Invalid Origin header"
824 return
825 }
826 } catch (Exception e) {
827 ec.logger.error("Error during Origin validation", e)
828 ec.web?.response?.setStatus(500) // Internal Server Error
829 response = "Error during Origin validation: ${e.message}"
830 return
831 }
832 } else {
833 ec.logger.info("No Origin header present")
834 }
835
836 // Force non-streaming for MVP - always use JSON-RPC 2.0
837 def wantsStreaming = false
838 ec.logger.info("Streaming disabled for MVP - using JSON-RPC 2.0")
839
840 // Set protocol version header on all responses
841 ec.web?.response?.setHeader("MCP-Protocol-Version", "2025-06-18")
842 ec.logger.info("Set MCP protocol version header")
843
844 // Handle session management
845 def sessionId = ec.web?.request?.getHeader("Mcp-Session-Id")
846 def isInitialize = (method == "initialize")
847 ec.logger.info("Session management - SessionId: ${sessionId}, IsInitialize: ${isInitialize}")
848
849 if (!isInitialize && !sessionId) {
850 ec.logger.warn("Missing session ID for non-initialization request")
851 ec.web?.response?.setStatus(400) // Bad Request
852 response = "Mcp-Session-Id header required for non-initialization requests"
853 return
854 }
855
856 // Generate new session ID for initialization
857 if (isInitialize) {
858 def newSessionId = UUID.randomUUID().toString()
859 ec.web?.response?.setHeader("Mcp-Session-Id", newSessionId)
860 ec.logger.info("Generated new session ID: ${newSessionId}")
861 }
657 862
658 ec.logger.info("MCP ${method} :: ${params} STREAMING ${wantsStreaming}") 863 ec.logger.info("MCP ${method} :: ${params} STREAMING ${wantsStreaming}")
659 864
660 // Validate JSON-RPC version 865 // Validate JSON-RPC version
866 ec.logger.info("Validating JSON-RPC version: ${jsonrpc}")
661 if (jsonrpc && jsonrpc != "2.0") { 867 if (jsonrpc && jsonrpc != "2.0") {
868 ec.logger.warn("Invalid JSON-RPC version: ${jsonrpc}")
662 def errorResponse = new JsonBuilder([ 869 def errorResponse = new JsonBuilder([
663 jsonrpc: "2.0", 870 jsonrpc: "2.0",
664 error: [ 871 error: [
...@@ -668,13 +875,18 @@ ...@@ -668,13 +875,18 @@
668 id: id 875 id: id
669 ]).toString() 876 ]).toString()
670 877
878 ec.logger.info("Built JSON-RPC error response: ${errorResponse}")
879
671 if (wantsStreaming) { 880 if (wantsStreaming) {
672 response = "data: ${errorResponse}\n\n" 881 response = "event: error\ndata: ${errorResponse}\n\n"
882 ec.logger.info("Returning streaming error response")
673 } else { 883 } else {
674 response = errorResponse 884 response = errorResponse
885 ec.logger.info("Returning regular error response")
675 } 886 }
676 return 887 return
677 } 888 }
889 ec.logger.info("JSON-RPC version validation passed")
678 890
679 def result = null 891 def result = null
680 def error = null 892 def error = null
...@@ -682,32 +894,40 @@ ...@@ -682,32 +894,40 @@
682 try { 894 try {
683 // Map OpenCode method names to actual service names 895 // Map OpenCode method names to actual service names
684 def serviceName = null 896 def serviceName = null
897 ec.logger.info("Mapping method '${method}' to service name")
685 switch (method) { 898 switch (method) {
686 case "mcp#Ping": 899 case "mcp#Ping":
687 case "ping": 900 case "ping":
688 serviceName = "McpServices.mcp#Ping" 901 serviceName = "McpServices.mcp#Ping"
902 ec.logger.info("Mapped to service: ${serviceName}")
689 break 903 break
690 case "initialize": 904 case "initialize":
691 case "mcp#Initialize": 905 case "mcp#Initialize":
692 serviceName = "McpServices.mcp#Initialize" 906 serviceName = "McpServices.mcp#Initialize"
907 ec.logger.info("Mapped to service: ${serviceName}")
693 break 908 break
694 case "tools/list": 909 case "tools/list":
695 case "mcp#ToolsList": 910 case "mcp#ToolsList":
696 serviceName = "McpServices.mcp#ToolsList" 911 serviceName = "McpServices.mcp#ToolsList"
912 ec.logger.info("Mapped to service: ${serviceName}")
697 break 913 break
698 case "tools/call": 914 case "tools/call":
699 case "mcp#ToolsCall": 915 case "mcp#ToolsCall":
700 serviceName = "McpServices.mcp#ToolsCall" 916 serviceName = "McpServices.mcp#ToolsCall"
917 ec.logger.info("Mapped to service: ${serviceName}")
701 break 918 break
702 case "resources/list": 919 case "resources/list":
703 case "mcp#ResourcesList": 920 case "mcp#ResourcesList":
704 serviceName = "McpServices.mcp#ResourcesList" 921 serviceName = "McpServices.mcp#ResourcesList"
922 ec.logger.info("Mapped to service: ${serviceName}")
705 break 923 break
706 case "resources/read": 924 case "resources/read":
707 case "mcp#ResourcesRead": 925 case "mcp#ResourcesRead":
708 serviceName = "McpServices.mcp#ResourcesRead" 926 serviceName = "McpServices.mcp#ResourcesRead"
927 ec.logger.info("Mapped to service: ${serviceName}")
709 break 928 break
710 default: 929 default:
930 ec.logger.warn("Unknown method: ${method}")
711 error = [ 931 error = [
712 code: -32601, 932 code: -32601,
713 message: "Method not found: ${method}" 933 message: "Method not found: ${method}"
...@@ -715,12 +935,29 @@ ...@@ -715,12 +935,29 @@
715 } 935 }
716 936
717 if (serviceName && !error) { 937 if (serviceName && !error) {
718 // Call the actual MCP service (services now return Maps, no streaming logic) 938 ec.logger.info("Calling service: ${serviceName} with params: ${params}")
719 result = ec.service.sync().name(serviceName).parameters(params ?: [:]).call() 939 // Check if service exists before calling
940 if (!ec.service.isServiceDefined(serviceName)) {
941 ec.logger.error("Service not defined: ${serviceName}")
942 error = [
943 code: -32601,
944 message: "Service not found: ${serviceName}"
945 ]
946 } else {
947 // Call the actual MCP service (services now return Maps, no streaming logic)
948 def serviceStartTime = System.currentTimeMillis()
949 result = ec.service.sync().name(serviceName).parameters(params ?: [:]).call()
950 def serviceEndTime = System.currentTimeMillis()
951 ec.logger.info("Service ${serviceName} completed in ${serviceEndTime - serviceStartTime}ms")
952 ec.logger.info("Service result type: ${result?.getClass()?.getSimpleName()}")
953 ec.logger.info("Service result: ${result}")
954 }
720 } 955 }
721 956
722 } catch (Exception e) { 957 } catch (Exception e) {
723 ec.logger.error("MCP request error for method ${method}", e) 958 ec.logger.error("MCP request error for method ${method}", e)
959 ec.logger.error("Exception details: ${e.getClass().getName()}: ${e.message}")
960 ec.logger.error("Exception stack trace: ${e.getStackTrace()}")
724 error = [ 961 error = [
725 code: -32603, 962 code: -32603,
726 message: "Internal error: ${e.message}" 963 message: "Internal error: ${e.message}"
...@@ -728,6 +965,7 @@ ...@@ -728,6 +965,7 @@
728 } 965 }
729 966
730 // Build JSON-RPC response 967 // Build JSON-RPC response
968 ec.logger.info("Building JSON-RPC response")
731 def responseObj = [ 969 def responseObj = [
732 jsonrpc: "2.0", 970 jsonrpc: "2.0",
733 id: id 971 id: id
...@@ -735,23 +973,29 @@ ...@@ -735,23 +973,29 @@
735 973
736 if (error) { 974 if (error) {
737 responseObj.error = error 975 responseObj.error = error
976 ec.logger.info("Response includes error: ${error}")
738 } else { 977 } else {
739 responseObj.result = result 978 responseObj.result = result
979 ec.logger.info("Response includes result")
740 } 980 }
741 981
742 def jsonResponse = new JsonBuilder(responseObj).toString() 982 def jsonResponse = new JsonBuilder(responseObj).toString()
743 983 ec.logger.info("Built JSON response: ${jsonResponse}")
744 if (wantsStreaming) { 984 ec.logger.info("JSON response length: ${jsonResponse.length()}")
745 // Set streaming headers and return as Server-Sent Events 985
746 ec.web?.response?.setContentType("text/event-stream") 986 def httpResponse = ec.web?.response
747 ec.web?.response?.setHeader("Cache-Control", "no-cache") 987
748 ec.web?.response?.setHeader("Connection", "keep-alive") 988 // MVP: Always return JSON-RPC 2.0, no streaming
749 ec.web?.response?.setHeader("Access-Control-Allow-Origin", "*") 989 ec.logger.info("Creating JSON-RPC 2.0 response")
750 ec.web?.response?.setHeader("Access-Control-Allow-Headers", "Cache-Control") 990 if (httpResponse) {
751 response = "data: ${jsonResponse}\n\n" 991 httpResponse.setContentType("application/json")
752 } else { 992 httpResponse.setHeader("Content-Type", "application/json")
753 response = jsonResponse 993 ec.logger.info("Set JSON-RPC content type")
754 } 994 }
995 response = jsonResponse
996 ec.logger.info("Created JSON-RPC response, length: ${response.length()}")
997
998 ec.logger.info("=== MCP REQUEST DEBUG END ===")
755 ]]></script> 999 ]]></script>
756 </actions> 1000 </actions>
757 </service> 1001 </service>
......
...@@ -15,12 +15,23 @@ ...@@ -15,12 +15,23 @@
15 description="MCP JSON-RPC 2.0 services for Moqui integration"> 15 description="MCP JSON-RPC 2.0 services for Moqui integration">
16 16
17 <resource name="rpc"> 17 <resource name="rpc">
18 <method type="post"> 18 <method type="post" content-type="application/json">
19 <service name="McpServices.handle#McpRequest"/>
20 </method>
21 <method type="post" content-type="application/json-rpc">
19 <service name="McpServices.handle#McpRequest"/> 22 <service name="McpServices.handle#McpRequest"/>
20 </method> 23 </method>
24
21 <method type="get"> 25 <method type="get">
22 <service name="McpServices.mcp#Ping"/> 26 <service name="McpServices.mcp#Ping"/>
23 </method> 27 </method>
28 <method type="get" path="debug">
29 <service name="McpServices.debug#ComponentStatus"/>
30 </method>
31 <!-- Add a catch-all method for debugging -->
32 <method type="post">
33 <service name="McpServices.handle#McpRequest"/>
34 </method>
24 </resource> 35 </resource>
25 36
26 37
......