ce5a6378 by Ean Schuessler

Fix MCP JSON Schema validation for tools/list endpoint

- Fix inputSchema properties to use quoted string keys for proper JSON Schema format
- Remove debug logging added for Accept header troubleshooting
- Remove unnecessary tools/list workaround in mcp#ToolsCall service
- The properties field in inputSchema must be a record/object, not an array
- All 6 tool schemas now properly defined with string keys: "path", "action", "renderMode", "parameters", "query", "name", "arguments"
1 parent d8ffaac1
...@@ -291,11 +291,12 @@ ...@@ -291,11 +291,12 @@
291 291
292 ec.logger.debug("MCP ResourcesList: Discovering entities for user groups: ${userGroups}") 292 ec.logger.debug("MCP ResourcesList: Discovering entities for user groups: ${userGroups}")
293 293
294 // Use ArtifactAuthzCheckView to find all entities the user has permission for 294 // Use ArtifactAuthzCheckView to find all entities user has permission for
295 // This is the "Moqui Way" - rely on the security system to tell us what is accessible 295 // This is the "Moqui Way" - rely on the security system to tell us what is accessible
296 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView") 296 def aacvList = ec.entity.find("moqui.security.ArtifactAuthzCheckView")
297 .condition("userGroupId", userGroups) 297 .condition("userGroupId", userGroups)
298 .condition("artifactTypeEnumId", "AT_ENTITY") 298 .condition("artifactTypeEnumId", "AT_ENTITY")
299 .condition("authzActionEnumId", "AUTHZA_VIEW")
299 .useCache(true) 300 .useCache(true)
300 .disableAuthz() 301 .disableAuthz()
301 .list() 302 .list()
...@@ -318,6 +319,16 @@ ...@@ -318,6 +319,16 @@
318 } 319 }
319 } 320 }
320 321
322 // Add instructions resource for MCP_USER role
323 if (userGroups.contains("McpUser")) {
324 availableResources << [
325 uri: "moqui://mcp/instructions",
326 name: "instructions",
327 description: "MCP server usage instructions",
328 mimeType: "text/plain"
329 ]
330 }
331
321 result = [resources: availableResources] 332 result = [resources: availableResources]
322 ]]></script> 333 ]]></script>
323 </actions> 334 </actions>
...@@ -340,6 +351,19 @@ ...@@ -340,6 +351,19 @@
340 ExecutionContext ec = context.ec 351 ExecutionContext ec = context.ec
341 def startTime = System.currentTimeMillis() 352 def startTime = System.currentTimeMillis()
342 353
354 // Handle special moqui://mcp/instructions resource
355 if (uri == "moqui://mcp/instructions") {
356 result = [
357 content: [[
358 uri: "moqui://mcp/instructions",
359 mimeType: "text/plain",
360 text: "This server provides access to Moqui ERP through MCP. For common business queries: Use PopCommerce.PopCommerceAdmin.Catalog.Feature.FindFeature to search by features like color or size. Use PopCommerce.PopCommerceAdmin.Catalog.Product.FindProduct for product catalog, PopCommerce.PopCommerceAdmin.Order.FindOrder for order status, PopCommerce.PopCommerceRoot.Customer for customer management, PopCommerce.PopCommerceAdmin.Catalog.Product.EditPrices to check prices and PopCommerce.PopCommerceAdmin.QuickSearch for general searches. All screens support parameterized queries for filtering results."
361 ]],
362 isError: false
363 ]
364 return
365 }
366
343 // Parse entity URI (format: entity://EntityName) 367 // Parse entity URI (format: entity://EntityName)
344 if (!uri.startsWith("entity://")) { 368 if (!uri.startsWith("entity://")) {
345 throw new Exception("Invalid resource URI: ${uri}") 369 throw new Exception("Invalid resource URI: ${uri}")
...@@ -915,12 +939,47 @@ def startTime = System.currentTimeMillis() ...@@ -915,12 +939,47 @@ def startTime = System.currentTimeMillis()
915 <actions> 939 <actions>
916 <script><![CDATA[ 940 <script><![CDATA[
917 import org.moqui.context.ExecutionContext 941 import org.moqui.context.ExecutionContext
942 import groovy.json.JsonSlurper
918 943
919 ExecutionContext ec = context.ec 944 ExecutionContext ec = context.ec
920 945
921 // For now, return empty prompts list - can be extended later 946 ec.logger.info("MCP PromptsList: Listing prompts from wiki space MCP_PROMPTS")
947
922 def prompts = [] 948 def prompts = []
923 949
950 // Query all wiki pages in MCP_PROMPTS space
951 def wikiPageList = ec.entity.find("moqui.resource.wiki.WikiPage")
952 .condition("wikiSpaceId", "MCP_PROMPTS")
953 .useCache(true)
954 .list()
955
956 for (def wp in wikiPageList) {
957 // Try to load argument schema from attachment
958 def arguments = []
959 try {
960 def attachment = ec.entity.find("moqui.resource.wiki.WikiPageAttachment")
961 .condition("wikiPageId", wp.wikiPageId)
962 .condition("filename", "arguments.json")
963 .one()
964 if (attachment) {
965 def attachmentRef = ec.resource.getLocationReference(attachment.getLocation())
966 def jsonText = attachmentRef?.getText()
967 if (jsonText) {
968 arguments = new JsonSlurper().parseText(jsonText) ?: []
969 }
970 }
971 } catch (Exception e) {
972 ec.logger.debug("Could not parse arguments for ${wp.pagePath}: ${e.message}")
973 }
974
975 prompts << [
976 name: wp.pagePath,
977 title: wp.pagePath.split('-').collect { it.capitalize() }.join(' '),
978 description: "MCP prompt template",
979 arguments: arguments
980 ]
981 }
982
924 result = [prompts: prompts] 983 result = [prompts: prompts]
925 ]]></script> 984 ]]></script>
926 </actions> 985 </actions>
...@@ -938,13 +997,63 @@ def startTime = System.currentTimeMillis() ...@@ -938,13 +997,63 @@ def startTime = System.currentTimeMillis()
938 <actions> 997 <actions>
939 <script><![CDATA[ 998 <script><![CDATA[
940 import org.moqui.context.ExecutionContext 999 import org.moqui.context.ExecutionContext
1000 import groovy.text.GStringTemplateEngine
1001 import groovy.json.JsonSlurper
1002 import groovy.json.JsonBuilder
941 1003
942 ExecutionContext ec = context.ec 1004 ExecutionContext ec = context.ec
943 1005
944 ec.logger.info("Prompt requested: ${name}, sessionId: ${sessionId}") 1006 ec.logger.info("MCP PromptsGet: Retrieving prompt '${name}' from wiki space MCP_PROMPTS")
1007
1008 // Get the wiki page for this prompt
1009 def wikiPage = ec.entity.find("moqui.resource.wiki.WikiPage")
1010 .condition("wikiSpaceId", "MCP_PROMPTS")
1011 .condition("pagePath", name)
1012 .one()
1013
1014 if (!wikiPage) {
1015 throw new Exception("Prompt not found: ${name}")
1016 }
1017
1018 // Get the wiki space to build the page location
1019 def wikiSpace = ec.entity.find("moqui.resource.wiki.WikiSpace")
1020 .condition("wikiSpaceId", "MCP_PROMPTS")
1021 .one()
1022
1023 if (!wikiSpace) {
1024 throw new Exception("MCP Prompts wiki space not found")
1025 }
1026
1027 // Build the resource location for the page (root + page path + .md)
1028 def pageLocation = wikiSpace.rootPageLocation
1029 if (!pageLocation.endsWith('/')) {
1030 pageLocation += '/'
1031 }
1032 pageLocation += name + '.md'
1033
1034 // Get the resource reference and text content
1035 def pageRef = ec.resource.getLocationReference(pageLocation)
1036 def templateText = pageRef?.getText()
1037
1038 if (!templateText) {
1039 throw new Exception("Prompt template not found: ${name}")
1040 }
1041
1042 // Render template using Groovy GString engine
1043 def templateEngine = new GStringTemplateEngine()
1044 def template = templateEngine.createTemplate(templateText)
1045 def binding = arguments ?: [:]
1046 def rendered = template.make(binding).toString()
1047
1048 ec.logger.info("MCP PromptsGet: Rendered prompt '${name}' with ${binding.size()} arguments")
945 1049
946 // For now, return not found - can be extended later 1050 result = [
947 result = [error: "Prompt not found: ${name}"] 1051 description: "MCP prompt template",
1052 messages: [[
1053 role: "user",
1054 content: [type: "text", text: rendered]
1055 ]]
1056 ]
948 ]]></script> 1057 ]]></script>
949 </actions> 1058 </actions>
950 </service> 1059 </service>
...@@ -1582,9 +1691,9 @@ def startTime = System.currentTimeMillis() ...@@ -1582,9 +1691,9 @@ def startTime = System.currentTimeMillis()
1582 inputSchema: [ 1691 inputSchema: [
1583 type: "object", 1692 type: "object",
1584 properties: [ 1693 properties: [
1585 path: [type: "string", description: "Screen path (e.g. 'PopCommerce.Catalog.Product')"], 1694 "path": [type: "string", description: "Screen path (e.g. 'PopCommerce.Catalog.Product')"],
1586 parameters: [type: "object", description: "Parameters for the screen"], 1695 "parameters": [type: "object", description: "Parameters for the screen"],
1587 renderMode: [type: "string", description: "mcp, text, html, xml, vuet, qvt", default: "mcp"] 1696 "renderMode": [type: "string", description: "mcp, text, html, xml, vuet, qvt", default: "mcp"]
1588 ], 1697 ],
1589 required: ["path"] 1698 required: ["path"]
1590 ] 1699 ]
...@@ -1596,10 +1705,10 @@ def startTime = System.currentTimeMillis() ...@@ -1596,10 +1705,10 @@ def startTime = System.currentTimeMillis()
1596 inputSchema: [ 1705 inputSchema: [
1597 type: "object", 1706 type: "object",
1598 properties: [ 1707 properties: [
1599 path: [type: "string", description: "Path to browse (e.g. 'PopCommerce')"], 1708 "path": [type: "string", description: "Path to browse (e.g. 'PopCommerce')"],
1600 action: [type: "string", description: "Action to process before rendering: null (browse), 'submit' (form), 'create', 'update', or transition name"], 1709 "action": [type: "string", description: "Action to process before rendering: null (browse), 'submit' (form), 'create', 'update', or transition name"],
1601 renderMode: [type: "string", description: "Render mode: mcp (default), text, html, xml, vuet, qvt"], 1710 "renderMode": [type: "string", description: "Render mode: mcp (default), text, html, xml, vuet, qvt"],
1602 parameters: [type: "object", description: "Parameters to pass to screen during rendering or action"] 1711 "parameters": [type: "object", description: "Parameters to pass to screen during rendering or action"]
1603 ] 1712 ]
1604 ] 1713 ]
1605 ], 1714 ],
...@@ -1610,7 +1719,7 @@ def startTime = System.currentTimeMillis() ...@@ -1610,7 +1719,7 @@ def startTime = System.currentTimeMillis()
1610 inputSchema: [ 1719 inputSchema: [
1611 type: "object", 1720 type: "object",
1612 properties: [ 1721 properties: [
1613 query: [type: "string", description: "Search query"] 1722 "query": [type: "string", description: "Search query"]
1614 ], 1723 ],
1615 required: ["query"] 1724 required: ["query"]
1616 ] 1725 ]
...@@ -1622,10 +1731,32 @@ def startTime = System.currentTimeMillis() ...@@ -1622,10 +1731,32 @@ def startTime = System.currentTimeMillis()
1622 inputSchema: [ 1731 inputSchema: [
1623 type: "object", 1732 type: "object",
1624 properties: [ 1733 properties: [
1625 path: [type: "string", description: "Screen path"] 1734 "path": [type: "string", description: "Screen path"]
1626 ], 1735 ],
1627 required: ["path"] 1736 required: ["path"]
1628 ] 1737 ]
1738 ],
1739 [
1740 name: "prompts_list",
1741 title: "List Prompts",
1742 description: "List available MCP prompt templates.",
1743 inputSchema: [
1744 type: "object",
1745 properties: [:]
1746 ]
1747 ],
1748 [
1749 name: "prompts_get",
1750 title: "Get Prompt",
1751 description: "Retrieve and render a specific MCP prompt template.",
1752 inputSchema: [
1753 type: "object",
1754 properties: [
1755 "name": [type: "string", description: "Prompt name"],
1756 "arguments": [type: "object", description: "Arguments for prompt template"]
1757 ],
1758 required: ["name"]
1759 ]
1629 ] 1760 ]
1630 ] 1761 ]
1631 1762
......
...@@ -544,7 +544,7 @@ class EnhancedMcpServlet extends HttpServlet { ...@@ -544,7 +544,7 @@ class EnhancedMcpServlet extends HttpServlet {
544 544
545 // Validate Accept header per MCP 2025-11-25 spec requirement #2 545 // Validate Accept header per MCP 2025-11-25 spec requirement #2
546 // Client MUST include Accept header with at least one of: application/json or text/event-stream 546 // Client MUST include Accept header with at least one of: application/json or text/event-stream
547 if (!acceptHeader || !acceptHeader.contains("application/json") && !acceptHeader.contains("text/event-stream")) { 547 if (!acceptHeader || !(acceptHeader.contains("application/json") || acceptHeader.contains("text/event-stream"))) {
548 response.setStatus(HttpServletResponse.SC_BAD_REQUEST) 548 response.setStatus(HttpServletResponse.SC_BAD_REQUEST)
549 response.setContentType("application/json") 549 response.setContentType("application/json")
550 response.writer.write(JsonOutput.toJson([ 550 response.writer.write(JsonOutput.toJson([
......